Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Separate sending to logfire from using standard OTEL env vars #351

Merged
merged 16 commits into from
Aug 6, 2024
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions docs/guides/advanced/alternative_backends.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# Alternative backends

**Logfire** uses the OpenTelemetry standard. This means that you can configure the SDK to export to any backend that supports OpenTelemetry.

The easiest way is to set the `OTEL_EXPORTER_OTLP_ENDPOINT` environment variable to a URL that points to your backend.
This will be used as a base, and the SDK will append `/v1/traces` and `/v1/metrics` to the URL to send traces and metrics, respectively.

Alternatively, you can use the `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT` and `OTEL_EXPORTER_OTLP_METRICS_ENDPOINT` environment variables to specify the URLs for traces and metrics separately. These URLs should include the full path, including `/v1/traces` and `/v1/metrics`.

The data will be encoded using Protobuf (not JSON) and sent over HTTP (not gRPC), so make sure that your backend supports this.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would add an info admonition or something to catch attention here.


## Example with Jaeger

Run this minimal command to start a [Jaeger](https://www.jaegertracing.io/) container:

```
docker run --rm \
-p 16686:16686 \
-p 4318:4318 \
jaegertracing/all-in-one:latest
```

Then run this code:

```python
import os

import logfire

# Jaeger only supports traces, not metrics, so only set the traces endpoint
# to avoid errors about failing to export metrics.
# Use port 4318 for HTTP, not 4317 for gRPC.
traces_endpoint = 'http://localhost:4318/v1/traces'
os.environ['OTEL_EXPORTER_OTLP_TRACES_ENDPOINT'] = traces_endpoint

logfire.configure(
# Setting a service name is good practice in general, but especially
# important for Jaeger, otherwise spans will be labeled as 'unknown_service'
service_name='my_logfire_service',

# Sending to Logfire is on by default regardless of the OTEL env vars.
# Keep this line here if you don't want to send to both Jaeger and Logfire.
send_to_logfire=False,
)

with logfire.span('This is a span'):
logfire.info('Logfire logs are also actually just spans!')
```

Finally open [http://localhost:16686/search?service=my_logfire_service](http://localhost:16686/search?service=my_logfire_service) to see the traces in the Jaeger UI.

## Other environment variables

If `OTEL_TRACES_EXPORTER` and/or `OTEL_METRICS_EXPORTER` are set to any non-empty value other than `otlp`, then **Logfire** will ignore the corresponding `OTEL_EXPORTER_OTLP_*` variables. This is because **Logfire** doesn't support other exporters, so we assume that the environment variables are intended to be used by something else. Normally you don't need to worry about this, and you don't need to set these variables at all unless you want to prevent **Logfire** from setting up these exporters.

See the [OpenTelemetry documentation](https://opentelemetry-python.readthedocs.io/en/latest/exporter/otlp/otlp.html) for information about the other headers you can set, such as `OTEL_EXPORTER_OTLP_HEADERS`.
64 changes: 37 additions & 27 deletions logfire/_internal/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,12 @@

import requests
from opentelemetry import metrics, trace
from opentelemetry.environment_variables import OTEL_TRACES_EXPORTER
from opentelemetry.environment_variables import OTEL_METRICS_EXPORTER, OTEL_TRACES_EXPORTER
from opentelemetry.exporter.otlp.proto.http.metric_exporter import OTLPMetricExporter
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
from opentelemetry.sdk.environment_variables import (
OTEL_BSP_SCHEDULE_DELAY,
OTEL_EXPORTER_OTLP_ENDPOINT,
OTEL_EXPORTER_OTLP_METRICS_ENDPOINT,
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT,
OTEL_RESOURCE_ATTRIBUTES,
Expand Down Expand Up @@ -51,6 +52,7 @@
from logfire.exceptions import LogfireConfigError
from logfire.version import VERSION

from ..testing import TestExporter
from .auth import DEFAULT_FILE, DefaultFile, is_logged_in
from .collect_system_info import collect_package_info
from .config_params import ParamManager, PydanticPluginRecordValues
Expand Down Expand Up @@ -203,8 +205,6 @@ def configure(
metric_readers: Legacy argument, use `additional_metric_readers` instead.
additional_metric_readers: Sequence of metric readers to be used in addition to the default reader
which exports metrics to Logfire's API.
Ensure that `preferred_temporality=logfire.METRICS_PREFERRED_TEMPORALITY`
is passed to the constructor of metric readers/exporters that accept the `preferred_temporality` argument.
Comment on lines -206 to -207
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thinking about exporting to other backends, I realize that this is somewhat opinionated, and we shouldn't recommend it for all alternative metric readers.

pydantic_plugin: Configuration for the Pydantic plugin. If `None` uses the `LOGFIRE_PYDANTIC_PLUGIN_*` environment
variables, otherwise defaults to `PydanticPlugin(record='off')`.
fast_shutdown: Whether to shut down exporters and providers quickly, mostly used for tests. Defaults to `False`.
Expand Down Expand Up @@ -375,9 +375,6 @@ def _load_configuration(
param_manager = ParamManager.create(config_dir)

self.base_url = param_manager.load_param('base_url', base_url)
self.metrics_endpoint = os.getenv(OTEL_EXPORTER_OTLP_METRICS_ENDPOINT) or urljoin(self.base_url, '/v1/metrics')
self.traces_endpoint = os.getenv(OTEL_EXPORTER_OTLP_TRACES_ENDPOINT) or urljoin(self.base_url, '/v1/traces')

self.send_to_logfire = param_manager.load_param('send_to_logfire', send_to_logfire)
self.token = param_manager.load_param('token', token)
self.project_name = param_manager.load_param('project_name', project_name)
Expand Down Expand Up @@ -625,12 +622,17 @@ def _initialize(self) -> ProxyTracerProvider:
self._tracer_provider.shutdown()
self._tracer_provider.set_provider(tracer_provider) # do we need to shut down the existing one???

processors: list[SpanProcessor] = []
processors_with_pending_spans: list[SpanProcessor] = []

def add_span_processor(span_processor: SpanProcessor) -> None:
# Most span processors added to the tracer provider should also be recorded in the `processors` list
# so that they can be used by the final pending span processor.
# Some span processors added to the tracer provider should also be recorded in
# `processors_with_pending_spans` so that they can be used by the final pending span processor.
# This means that `tracer_provider.add_span_processor` should only appear in two places.
has_pending = isinstance(
getattr(span_processor, 'span_exporter', None),
(TestExporter, RemovePendingSpansExporter, SimpleConsoleSpanExporter),
)

if self.tail_sampling:
span_processor = TailSamplingProcessor(
span_processor,
Expand All @@ -642,7 +644,8 @@ def add_span_processor(span_processor: SpanProcessor) -> None:
)
span_processor = MainSpanProcessorWrapper(span_processor, self.scrubber)
tracer_provider.add_span_processor(span_processor)
processors.append(span_processor)
if has_pending:
processors_with_pending_spans.append(span_processor)

if self.additional_span_processors is not None:
for processor in self.additional_span_processors:
Expand Down Expand Up @@ -696,27 +699,19 @@ def check_token():
headers = {'User-Agent': f'logfire/{VERSION}', 'Authorization': self.token}
session = OTLPExporterHttpSession(max_body_size=OTLP_MAX_BODY_SIZE)
session.headers.update(headers)
otel_traces_exporter_env = os.getenv(OTEL_TRACES_EXPORTER)
otel_traces_exporter_env = otel_traces_exporter_env.lower() if otel_traces_exporter_env else None
if otel_traces_exporter_env is None or otel_traces_exporter_env == 'otlp':
span_exporter = OTLPSpanExporter(endpoint=self.traces_endpoint, session=session)
span_exporter = RetryFewerSpansSpanExporter(span_exporter)
span_exporter = FallbackSpanExporter(
span_exporter, FileSpanExporter(self.data_dir / DEFAULT_FALLBACK_FILE_NAME, warn=True)
)
span_exporter = RemovePendingSpansExporter(span_exporter)
add_span_processor(self.default_span_processor(span_exporter))

elif otel_traces_exporter_env != 'none': # pragma: no cover
raise ValueError(
'OTEL_TRACES_EXPORTER must be "otlp", "none" or unset. Logfire does not support other exporters.'
)
span_exporter = OTLPSpanExporter(endpoint=urljoin(self.base_url, '/v1/traces'), session=session)
span_exporter = RetryFewerSpansSpanExporter(span_exporter)
span_exporter = FallbackSpanExporter(
span_exporter, FileSpanExporter(self.data_dir / DEFAULT_FALLBACK_FILE_NAME, warn=True)
)
span_exporter = RemovePendingSpansExporter(span_exporter)
add_span_processor(self.default_span_processor(span_exporter))

metric_readers += [
PeriodicExportingMetricReader(
QuietMetricExporter(
OTLPMetricExporter(
endpoint=self.metrics_endpoint,
endpoint=urljoin(self.base_url, '/v1/metrics'),
headers=headers,
session=session,
# I'm pretty sure that this line here is redundant,
Expand All @@ -729,7 +724,22 @@ def check_token():
)
]

tracer_provider.add_span_processor(PendingSpanProcessor(self.id_generator, tuple(processors)))
if processors_with_pending_spans:
tracer_provider.add_span_processor(
PendingSpanProcessor(self.id_generator, tuple(processors_with_pending_spans))
)

otlp_endpoint = os.getenv(OTEL_EXPORTER_OTLP_ENDPOINT)
otlp_traces_endpoint = os.getenv(OTEL_EXPORTER_OTLP_TRACES_ENDPOINT)
otlp_metrics_endpoint = os.getenv(OTEL_EXPORTER_OTLP_METRICS_ENDPOINT)
otlp_traces_exporter = os.getenv(OTEL_TRACES_EXPORTER, '').lower()
otlp_metrics_exporter = os.getenv(OTEL_METRICS_EXPORTER, '').lower()

if (otlp_endpoint or otlp_traces_endpoint) and otlp_traces_exporter in ('otlp', ''):
add_span_processor(BatchSpanProcessor(OTLPSpanExporter()))

if (otlp_endpoint or otlp_metrics_endpoint) and otlp_metrics_exporter in ('otlp', ''):
metric_readers += [PeriodicExportingMetricReader(OTLPMetricExporter())]

meter_provider = MeterProvider(
metric_readers=metric_readers,
Expand Down
4 changes: 2 additions & 2 deletions logfire/_internal/config_params.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from pathlib import Path
from typing import Any, Callable, Literal, Set, TypeVar

from opentelemetry.sdk.environment_variables import OTEL_EXPORTER_OTLP_ENDPOINT, OTEL_SERVICE_NAME
from opentelemetry.sdk.environment_variables import OTEL_SERVICE_NAME
from typing_extensions import get_args, get_origin

from logfire.exceptions import LogfireConfigError
Expand Down Expand Up @@ -61,7 +61,7 @@ class _DefaultCallback:
"""When running under pytest, don't send spans to Logfire by default."""

# fmt: off
BASE_URL = ConfigParam(env_vars=['LOGFIRE_BASE_URL', OTEL_EXPORTER_OTLP_ENDPOINT], allow_file_config=True, default=LOGFIRE_BASE_URL)
BASE_URL = ConfigParam(env_vars=['LOGFIRE_BASE_URL'], allow_file_config=True, default=LOGFIRE_BASE_URL)
"""Use to set the base URL of the Logfire backend."""
SEND_TO_LOGFIRE = ConfigParam(env_vars=['LOGFIRE_SEND_TO_LOGFIRE'], allow_file_config=True, default=_send_to_logfire_default, tp=bool)
"""Whether to send spans to Logfire."""
Expand Down
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ nav:
- SQL Explorer: guides/web_ui/explore.md
- Advanced User Guide:
- Advanced User Guide: guides/advanced/index.md
- Alternative Backends: guides/advanced/alternative_backends.md
- Sampling: guides/advanced/sampling.md
- Scrubbing: guides/advanced/scrubbing.md
- Testing: guides/advanced/testing.md
Expand Down
Loading