Skip to content

fix(opentelemetry): prevent OTLP logs self-export loop#19266

Open
bm1549 wants to merge 2 commits into
mainfrom
brian.marks/fix-otlp-logs-self-telemetry-loop
Open

fix(opentelemetry): prevent OTLP logs self-export loop#19266
bm1549 wants to merge 2 commits into
mainfrom
brian.marks/fix-otlp-logs-self-telemetry-loop

Conversation

@bm1549

@bm1549 bm1549 commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Description

When DD_LOGS_OTEL_ENABLED=true, the OTLP logs pipeline attaches an OpenTelemetry LoggingHandler to the Python root logger. ddtrace's own loggers (ddtrace.*) and the OpenTelemetry exporter's loggers (opentelemetry.*) propagate to root, so the handler captured the tracer's own log records: the per-export debug line and the exporter's export-failure warnings/errors. Exporting those records emits more log records, which were captured and exported again. The result is a self-amplifying export loop. This happens whenever those records reach the effective log level: with DD_TRACE_DEBUG=true (the per-export debug line), or on export failures (warnings/errors at the default level).

The fix attaches a self-telemetry exclusion filter to the OTLP LoggingHandler that drops records whose logger name is in the ddtrace or opentelemetry namespaces. Matching is exact-or-dotted-prefix (ddtrace / ddtrace.*, opentelemetry / opentelemetry.*), so application loggers such as ddtrace_myapp are not affected. The filter is attached only to the handler ddtrace itself installs; application-configured handlers are not modified. Application logs are still captured and exported.

This was discovered during cross-tracer startup-log work. It is an independent, pre-existing bug, so it ships on its own branch.

Testing

Adds tests/opentelemetry/test_logs.py::test_otel_logs_exporter_excludes_self_telemetry. With DD_LOGS_OTEL_ENABLED=true, it emits a ddtrace.* record, an opentelemetry.* record, and an application record, flushes the exporter to a mock gRPC log service, and asserts the application record is exported while the two self-telemetry records are not. The test fails without the fix and passes with it. It runs across the OpenTelemetry versions already covered by the opentelemetry test venv.

Risks

Low. The change only adds a logging filter to the handler ddtrace creates. Records from the ddtrace and opentelemetry logger namespaces are no longer exported through this pipeline; application logs are unchanged. The filter binds to the handler instance, so it persists across logging.basicConfig reconfiguration, which is the reconfiguration API the OpenTelemetry SDK patches to re-add the same handler instance (dictConfig/fileConfig are not patched by the SDK).

Additional Notes

Includes a reno release note. No configuration or public API changes.


Context

Independent, pre-existing bug surfaced while adding OTLP export-status fields to the tracer startup log across dd-trace-* (the Python system-test for the logs field hung on this loop). Related:

…r's own logs

When DD_LOGS_OTEL_ENABLED=true, the OpenTelemetry SDK attaches a LoggingHandler
to the Python root logger that captures every propagated log record. Because
ddtrace's internal loggers (ddtrace.*) and the OpenTelemetry exporter's loggers
(opentelemetry.*) propagate to root, the exporter's own log records -- the
per-batch export debug line and the exporter's export-failure warnings/errors --
were captured, exported, and captured again, producing a self-amplifying export
loop (triggered by DD_TRACE_DEBUG or by export failures at the default log level).

Attach a logging.Filter to the OTLP LoggingHandler that rejects records whose
logger name is in the ddtrace or opentelemetry namespaces, so the tracer never
feeds its own telemetry-pipeline logs back into its own log export. Application
logs are still captured and exported. The filter is attached only to the handler
ddtrace itself installs (identified by diffing the root logger's handlers), so
application-configured handlers are left untouched. Because it binds to the
handler instance, it also persists across logging.basicConfig reconfiguration,
which the OpenTelemetry SDK patches to re-add that same handler instance
(dictConfig/fileConfig are not patched by the SDK).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@datadog-datadog-prod-us1

datadog-datadog-prod-us1 Bot commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Tests

🎉 All green!

🧪 All tests passed
❄️ No new flaky tests detected

🔄 Datadog auto-retried 1 job - 1 passed on retry View in Datadog

This comment will be updated automatically if new data arrives.
🔗 Commit SHA: b35341a | Docs | Datadog PR Page | Give us feedback!

@pr-commenter

pr-commenter Bot commented Jul 24, 2026

Copy link
Copy Markdown

Benchmarks

Benchmark execution time: 2026-07-24 02:13:52

Comparing candidate commit 31b6da5 in PR branch brian.marks/fix-otlp-logs-self-telemetry-loop with baseline commit e083509 in branch main.

Found 0 performance improvements and 4 performance regressions! Performance is the same for 616 metrics, 10 unstable metrics.

scenario:iastaspects-lstrip_aspect

  • 🟥 execution_time [+63.223µs; +67.915µs] or [+19.810%; +21.280%]

scenario:iastaspects-translate_aspect

  • 🟥 execution_time [+43.302µs; +49.934µs] or [+8.687%; +10.018%]

scenario:iastaspectsospath-ospathbasename_aspect

  • 🟥 execution_time [+90.530µs; +97.592µs] or [+21.563%; +23.245%]

scenario:telemetryaddmetric-1-count-metric-1-times

  • 🟥 execution_time [+193.880ns; +237.394ns] or [+8.968%; +10.980%]

@cit-pr-commenter-54b7da

Copy link
Copy Markdown

Circular import analysis

⚠️ Existing circular imports

There are 18 circular imports that already exist on the base branch and have not been changed by this PR.

Show existing cycles (showing 5 of 18 shortest)
ddtrace.internal.core -> ddtrace._trace.span -> ddtrace.internal.core
ddtrace.trace -> ddtrace._trace.tracer -> ddtrace.internal.debug -> ddtrace.trace
ddtrace -> ddtrace.trace -> ddtrace._trace.tracer -> ddtrace.internal.debug -> ddtrace
ddtrace.internal.telemetry -> ddtrace.internal.telemetry.writer -> ddtrace.internal.core -> ddtrace._trace.span -> ddtrace.internal.settings._config -> ddtrace.internal.telemetry
ddtrace.internal.core -> ddtrace._trace.span -> ddtrace._trace.context -> ddtrace.internal.utils.http -> ddtrace.internal.utils -> ddtrace.internal.core

To see all cycles, download the cycles-base.json and cycles-pr.json artifacts from this CI job and run:

uv run --script scripts/import-analysis/cycles.py compare cycles-base.json cycles-pr.json

@bm1549
bm1549 marked this pull request as ready for review July 24, 2026 20:21
@bm1549
bm1549 requested review from a team as code owners July 24, 2026 20:21
@bm1549
bm1549 requested review from brettlangdon and mabdinur July 24, 2026 20:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

AI Generated Largely based on code generated by an AI or LLM. This label is the same across all dd-trace-* repos

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant