Skip to content

Commit be319da

Browse files
alexmojakiKludex
andauthored
Separate sending to logfire from using standard OTEL env vars (#351)
Co-authored-by: Marcelo Trylesinski <[email protected]>
1 parent 24b95f1 commit be319da

File tree

5 files changed

+238
-32
lines changed

5 files changed

+238
-32
lines changed
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# Alternative backends
2+
3+
**Logfire** uses the OpenTelemetry standard. This means that you can configure the SDK to export to any backend that supports OpenTelemetry.
4+
5+
The easiest way is to set the `OTEL_EXPORTER_OTLP_ENDPOINT` environment variable to a URL that points to your backend.
6+
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.
7+
8+
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`.
9+
10+
!!! note
11+
The data will be encoded using **Protobuf** (not JSON) and sent over **HTTP** (not gRPC).
12+
13+
Make sure that your backend supports this! :nerd_face:
14+
15+
## Example with Jaeger
16+
17+
Run this minimal command to start a [Jaeger](https://www.jaegertracing.io/) container:
18+
19+
```
20+
docker run --rm \
21+
-p 16686:16686 \
22+
-p 4318:4318 \
23+
jaegertracing/all-in-one:latest
24+
```
25+
26+
Then run this code:
27+
28+
```python
29+
import os
30+
31+
import logfire
32+
33+
# Jaeger only supports traces, not metrics, so only set the traces endpoint
34+
# to avoid errors about failing to export metrics.
35+
# Use port 4318 for HTTP, not 4317 for gRPC.
36+
traces_endpoint = 'http://localhost:4318/v1/traces'
37+
os.environ['OTEL_EXPORTER_OTLP_TRACES_ENDPOINT'] = traces_endpoint
38+
39+
logfire.configure(
40+
# Setting a service name is good practice in general, but especially
41+
# important for Jaeger, otherwise spans will be labeled as 'unknown_service'
42+
service_name='my_logfire_service',
43+
44+
# Sending to Logfire is on by default regardless of the OTEL env vars.
45+
# Keep this line here if you don't want to send to both Jaeger and Logfire.
46+
send_to_logfire=False,
47+
)
48+
49+
with logfire.span('This is a span'):
50+
logfire.info('Logfire logs are also actually just spans!')
51+
```
52+
53+
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.
54+
55+
## Other environment variables
56+
57+
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.
58+
59+
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`.

logfire/_internal/config.py

Lines changed: 37 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,12 @@
1818

1919
import requests
2020
from opentelemetry import metrics, trace
21-
from opentelemetry.environment_variables import OTEL_TRACES_EXPORTER
21+
from opentelemetry.environment_variables import OTEL_METRICS_EXPORTER, OTEL_TRACES_EXPORTER
2222
from opentelemetry.exporter.otlp.proto.http.metric_exporter import OTLPMetricExporter
2323
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
2424
from opentelemetry.sdk.environment_variables import (
2525
OTEL_BSP_SCHEDULE_DELAY,
26+
OTEL_EXPORTER_OTLP_ENDPOINT,
2627
OTEL_EXPORTER_OTLP_METRICS_ENDPOINT,
2728
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT,
2829
OTEL_RESOURCE_ATTRIBUTES,
@@ -51,6 +52,7 @@
5152
from logfire.exceptions import LogfireConfigError
5253
from logfire.version import VERSION
5354

55+
from ..testing import TestExporter
5456
from .auth import DEFAULT_FILE, DefaultFile, is_logged_in
5557
from .collect_system_info import collect_package_info
5658
from .config_params import ParamManager, PydanticPluginRecordValues
@@ -203,8 +205,6 @@ def configure(
203205
metric_readers: Legacy argument, use `additional_metric_readers` instead.
204206
additional_metric_readers: Sequence of metric readers to be used in addition to the default reader
205207
which exports metrics to Logfire's API.
206-
Ensure that `preferred_temporality=logfire.METRICS_PREFERRED_TEMPORALITY`
207-
is passed to the constructor of metric readers/exporters that accept the `preferred_temporality` argument.
208208
pydantic_plugin: Configuration for the Pydantic plugin. If `None` uses the `LOGFIRE_PYDANTIC_PLUGIN_*` environment
209209
variables, otherwise defaults to `PydanticPlugin(record='off')`.
210210
fast_shutdown: Whether to shut down exporters and providers quickly, mostly used for tests. Defaults to `False`.
@@ -375,9 +375,6 @@ def _load_configuration(
375375
param_manager = ParamManager.create(config_dir)
376376

377377
self.base_url = param_manager.load_param('base_url', base_url)
378-
self.metrics_endpoint = os.getenv(OTEL_EXPORTER_OTLP_METRICS_ENDPOINT) or urljoin(self.base_url, '/v1/metrics')
379-
self.traces_endpoint = os.getenv(OTEL_EXPORTER_OTLP_TRACES_ENDPOINT) or urljoin(self.base_url, '/v1/traces')
380-
381378
self.send_to_logfire = param_manager.load_param('send_to_logfire', send_to_logfire)
382379
self.token = param_manager.load_param('token', token)
383380
self.project_name = param_manager.load_param('project_name', project_name)
@@ -625,12 +622,17 @@ def _initialize(self) -> ProxyTracerProvider:
625622
self._tracer_provider.shutdown()
626623
self._tracer_provider.set_provider(tracer_provider) # do we need to shut down the existing one???
627624

628-
processors: list[SpanProcessor] = []
625+
processors_with_pending_spans: list[SpanProcessor] = []
629626

630627
def add_span_processor(span_processor: SpanProcessor) -> None:
631-
# Most span processors added to the tracer provider should also be recorded in the `processors` list
632-
# so that they can be used by the final pending span processor.
628+
# Some span processors added to the tracer provider should also be recorded in
629+
# `processors_with_pending_spans` so that they can be used by the final pending span processor.
633630
# This means that `tracer_provider.add_span_processor` should only appear in two places.
631+
has_pending = isinstance(
632+
getattr(span_processor, 'span_exporter', None),
633+
(TestExporter, RemovePendingSpansExporter, SimpleConsoleSpanExporter),
634+
)
635+
634636
if self.tail_sampling:
635637
span_processor = TailSamplingProcessor(
636638
span_processor,
@@ -642,7 +644,8 @@ def add_span_processor(span_processor: SpanProcessor) -> None:
642644
)
643645
span_processor = MainSpanProcessorWrapper(span_processor, self.scrubber)
644646
tracer_provider.add_span_processor(span_processor)
645-
processors.append(span_processor)
647+
if has_pending:
648+
processors_with_pending_spans.append(span_processor)
646649

647650
if self.additional_span_processors is not None:
648651
for processor in self.additional_span_processors:
@@ -696,27 +699,19 @@ def check_token():
696699
headers = {'User-Agent': f'logfire/{VERSION}', 'Authorization': self.token}
697700
session = OTLPExporterHttpSession(max_body_size=OTLP_MAX_BODY_SIZE)
698701
session.headers.update(headers)
699-
otel_traces_exporter_env = os.getenv(OTEL_TRACES_EXPORTER)
700-
otel_traces_exporter_env = otel_traces_exporter_env.lower() if otel_traces_exporter_env else None
701-
if otel_traces_exporter_env is None or otel_traces_exporter_env == 'otlp':
702-
span_exporter = OTLPSpanExporter(endpoint=self.traces_endpoint, session=session)
703-
span_exporter = RetryFewerSpansSpanExporter(span_exporter)
704-
span_exporter = FallbackSpanExporter(
705-
span_exporter, FileSpanExporter(self.data_dir / DEFAULT_FALLBACK_FILE_NAME, warn=True)
706-
)
707-
span_exporter = RemovePendingSpansExporter(span_exporter)
708-
add_span_processor(self.default_span_processor(span_exporter))
709-
710-
elif otel_traces_exporter_env != 'none': # pragma: no cover
711-
raise ValueError(
712-
'OTEL_TRACES_EXPORTER must be "otlp", "none" or unset. Logfire does not support other exporters.'
713-
)
702+
span_exporter = OTLPSpanExporter(endpoint=urljoin(self.base_url, '/v1/traces'), session=session)
703+
span_exporter = RetryFewerSpansSpanExporter(span_exporter)
704+
span_exporter = FallbackSpanExporter(
705+
span_exporter, FileSpanExporter(self.data_dir / DEFAULT_FALLBACK_FILE_NAME, warn=True)
706+
)
707+
span_exporter = RemovePendingSpansExporter(span_exporter)
708+
add_span_processor(self.default_span_processor(span_exporter))
714709

715710
metric_readers += [
716711
PeriodicExportingMetricReader(
717712
QuietMetricExporter(
718713
OTLPMetricExporter(
719-
endpoint=self.metrics_endpoint,
714+
endpoint=urljoin(self.base_url, '/v1/metrics'),
720715
headers=headers,
721716
session=session,
722717
# I'm pretty sure that this line here is redundant,
@@ -729,7 +724,22 @@ def check_token():
729724
)
730725
]
731726

732-
tracer_provider.add_span_processor(PendingSpanProcessor(self.id_generator, tuple(processors)))
727+
if processors_with_pending_spans:
728+
tracer_provider.add_span_processor(
729+
PendingSpanProcessor(self.id_generator, tuple(processors_with_pending_spans))
730+
)
731+
732+
otlp_endpoint = os.getenv(OTEL_EXPORTER_OTLP_ENDPOINT)
733+
otlp_traces_endpoint = os.getenv(OTEL_EXPORTER_OTLP_TRACES_ENDPOINT)
734+
otlp_metrics_endpoint = os.getenv(OTEL_EXPORTER_OTLP_METRICS_ENDPOINT)
735+
otlp_traces_exporter = os.getenv(OTEL_TRACES_EXPORTER, '').lower()
736+
otlp_metrics_exporter = os.getenv(OTEL_METRICS_EXPORTER, '').lower()
737+
738+
if (otlp_endpoint or otlp_traces_endpoint) and otlp_traces_exporter in ('otlp', ''):
739+
add_span_processor(BatchSpanProcessor(OTLPSpanExporter()))
740+
741+
if (otlp_endpoint or otlp_metrics_endpoint) and otlp_metrics_exporter in ('otlp', ''):
742+
metric_readers += [PeriodicExportingMetricReader(OTLPMetricExporter())]
733743

734744
meter_provider = MeterProvider(
735745
metric_readers=metric_readers,

logfire/_internal/config_params.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from pathlib import Path
88
from typing import Any, Callable, Literal, Set, TypeVar
99

10-
from opentelemetry.sdk.environment_variables import OTEL_EXPORTER_OTLP_ENDPOINT, OTEL_SERVICE_NAME
10+
from opentelemetry.sdk.environment_variables import OTEL_SERVICE_NAME
1111
from typing_extensions import get_args, get_origin
1212

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

6363
# fmt: off
64-
BASE_URL = ConfigParam(env_vars=['LOGFIRE_BASE_URL', OTEL_EXPORTER_OTLP_ENDPOINT], allow_file_config=True, default=LOGFIRE_BASE_URL)
64+
BASE_URL = ConfigParam(env_vars=['LOGFIRE_BASE_URL'], allow_file_config=True, default=LOGFIRE_BASE_URL)
6565
"""Use to set the base URL of the Logfire backend."""
6666
SEND_TO_LOGFIRE = ConfigParam(env_vars=['LOGFIRE_SEND_TO_LOGFIRE'], allow_file_config=True, default=_send_to_logfire_default, tp=bool)
6767
"""Whether to send spans to Logfire."""

mkdocs.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ nav:
8585
- SQL Explorer: guides/web_ui/explore.md
8686
- Advanced User Guide:
8787
- Advanced User Guide: guides/advanced/index.md
88+
- Alternative Backends: guides/advanced/alternative_backends.md
8889
- Sampling: guides/advanced/sampling.md
8990
- Scrubbing: guides/advanced/scrubbing.md
9091
- Testing: guides/advanced/testing.md

0 commit comments

Comments
 (0)