-
Notifications
You must be signed in to change notification settings - Fork 81
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
Changes from 13 commits
Commits
Show all changes
16 commits
Select commit
Hold shift + click to select a range
839982b
Separate sending to logfire from using standard OTEL env vars
alexmojaki 90d4942
Only send pending spans to logfire exporters
alexmojaki 1d27ca2
test_default_span_processors
alexmojaki ccaa7c6
test other processors
alexmojaki ef248b1
get_span_processors
alexmojaki 15adf69
test metric readers
alexmojaki 0a4c44d
test specific endpoint env vars
alexmojaki a8a8587
Test OTEL_TRACES_EXPORTER and OTEL_METRICS_EXPORTER
alexmojaki 8c5fdb7
comments
alexmojaki bf9a8f0
refactor detecting processors that need pending spans
alexmojaki 62d3882
Merge branch 'main' of github.com:pydantic/logfire into alex/otlp-env…
alexmojaki 45034df
docs
alexmojaki dd363c8
docs
alexmojaki 65e92c7
Merge branch 'main' into alex/otlp-env-vars
alexmojaki e56a99f
Merge branch 'main' into alex/otlp-env-vars
Kludex 8d9d2c5
Add note about how we send data
Kludex File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. | ||
|
||
## 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`. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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, | ||
|
@@ -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 | ||
|
@@ -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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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`. | ||
|
@@ -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) | ||
|
@@ -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, | ||
|
@@ -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: | ||
|
@@ -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, | ||
|
@@ -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, | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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.