Skip to content

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

Merged
gh-worker-dd-mergequeue-cf854d[bot] merged 2 commits into
mainfrom
brian.marks/fix-otlp-logs-self-telemetry-loop
Jul 27, 2026
Merged

gh-worker-dd-mergequeue-cf854d[bot] merged 2 commits into
mainfrom
brian.marks/fix-otlp-logs-self-telemetry-loop

Conversation

@bm1549

@bm1549 bm1549 commented Jul 24, 2026 •

Copy link
Copy Markdown
Collaborator

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

cit-pr-commenter-54b7da Bot commented Jul 24, 2026 •

Copy link
Copy Markdown

Circular import analysis

⚠️ Existing circular imports

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

ddtrace.trace -> ddtrace._trace.tracer -> ddtrace.internal.debug -> ddtrace.trace
ddtrace -> ddtrace.trace -> ddtrace._trace.tracer -> ddtrace.internal.debug -> ddtrace
ddtrace -> ddtrace.trace -> ddtrace._trace.tracer -> ddtrace.internal.debug -> ddtrace.internal.runtime.runtime_metrics -> ddtrace

@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
Comment thread ddtrace/internal/opentelemetry/logs.py
@gh-worker-dd-mergequeue-cf854d
gh-worker-dd-mergequeue-cf854d Bot merged commit 3e949c4 into main Jul 27, 2026
472 checks passed
@gh-worker-dd-mergequeue-cf854d
gh-worker-dd-mergequeue-cf854d Bot deleted the brian.marks/fix-otlp-logs-self-telemetry-loop branch July 27, 2026 15:46
@cit-pr-commenter-54b7da

Copy link
Copy Markdown

Codeowners resolved as

ddtrace/internal/opentelemetry/logs.py                                  @DataDog/apm-sdk-capabilities-python
releasenotes/notes/fix-otlp-logs-self-telemetry-export-loop-d62e76e236121d32.yaml  @DataDog/apm-python
tests/opentelemetry/test_logs.py                                        @DataDog/apm-sdk-capabilities-python @DataDog/apm-core-python

brettlangdon pushed a commit that referenced this pull request Aug 3, 2026
## 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:
- Startup-log feature (dd-trace-py): #19265
- system-tests coverage that surfaced it: DataDog/system-tests#7376


Co-authored-by: brian.marks <brian.marks@datadoghq.com>
gh-worker-dd-mergequeue-cf854d Bot pushed a commit to DataDog/dd-trace-go that referenced this pull request Aug 4, 2026
### What does this PR do?

Adds three boolean fields to the `DATADOG TRACER CONFIGURATION` startup log:

- `otlp_traces_export_enabled` reports whether the tracer selected the OTLP trace writer.
- `otlp_metrics_export_enabled` reports whether the tracer will start OTel runtime metrics.
- `otlp_logs_export_enabled` reports whether OTel logs are enabled in the tracer configuration.

Updates the startup-log expectations, clears the OTLP-related environment variables in `TestStartupLog`, and tests the metrics startup gate with and without the OTel metrics hook installed.

### Motivation

Part of the cross-tracer work to use the same OTLP export-status fields in each tracer's startup diagnostics. The fields show which signals a service exports over OTLP.

Related PRs:

- dd-trace-js: DataDog/dd-trace-js#9517
- dd-trace-py: DataDog/dd-trace-py#19265
- dd-trace-java: DataDog/dd-trace-java#12062
- dd-trace-dotnet: DataDog/dd-trace-dotnet#8936
- dd-trace-rb: DataDog/dd-trace-rb#6096
- dd-trace-php: DataDog/dd-trace-php#4056
- system-tests: DataDog/system-tests#7376
- dd-trace-py OTLP logs fix: DataDog/dd-trace-py#19266

### Reviewer's Checklist

- [x] Changed code has unit tests for its functionality at or near 100% coverage.
- [ ] [System-Tests](https://github.com/DataDog/system-tests/) covering this feature have been added and enabled with the va.b.c-dev version tag. (N/A: startup diagnostic-log fields only.)
- [ ] There is a benchmark for any new code, or changes to existing code. (N/A: no hot-path or performance-sensitive code.)
- [ ] If this interacts with the agent in a new way, a system test has been added. (N/A: no new agent interaction.)
- [ ] New code is free of linting errors. You can check this by running `make lint` locally. (Full lint not run; `gofmt` and `git diff --check` are clean.)
- [x] New code doesn't break existing tests. You can check this by running `make test` locally. (`go test ./ddtrace/tracer -count=1` passes.)
- [ ] Add an appropriate team label so this PR gets put in the right place for the release notes.
- [ ] All generated files are up to date. You can check this by running `make generate` locally. (N/A: no generated files changed.)
- [ ] Non-trivial go.mod changes, e.g. adding new modules, are reviewed by @DataDog/dd-trace-go-guild. Make sure all nested modules are up to date by running `make fix-modules` locally. (N/A: no module changes.)


Co-authored-by: dario.castane <dario.castane@datadoghq.com>
juanjux added a commit that referenced this pull request Sep 23, 2026
#20506)

## Description

Backport of #19266 to the 4.13 release line.

When OpenTelemetry logs are enabled (`DD_LOGS_OTEL_ENABLED=true`, or
`OTEL_SDK_DISABLED=false`), the OTLP logs handler is attached to the
root logger. ddtrace's own loggers and the OpenTelemetry exporter's
loggers propagate there, so the per-export debug line is captured and
exported again. With `DD_TRACE_DEBUG=true` that becomes a
self-amplifying loop: the flush never returns, and the process writes
logs as fast as it can.

Parametric system tests always set `DD_TRACE_DEBUG=true`.
`test_stable_false` sets `OTEL_SDK_DISABLED=false`, which enables logs
on 4.13. The test client's logs flush has no timeout, so the pytest
worker blocks. On the CI docker-in-docker runner the log flood also
stalls the other workers, and `system-tests parametric` sits at about
97% until the one-hour job timeout. 4.14 and 4.15 already have this
filter, which is why they finish the same suite.

The filter is attached only to the handler ddtrace installs. Application
logs are still exported. Loggers named `ddtrace_myapp` are not excluded.

## Testing

- `scripts/run-tests --venv 1ca3564 --
tests/opentelemetry/test_logs.py::test_otel_logs_exporter_excludes_self_telemetry`
passed on Python 3.11 (opentelemetry-exporter-otlp).
- Locally, the same parametric test against the 4.13.3 wheel hung in
`otel_logs_flush()` and the container log grew to ~142k lines in 25
seconds. That wheel does not include this change.

## Risks

Records from the `ddtrace` and `opentelemetry` logger namespaces are no
longer exported through the OTLP logs pipeline ddtrace installs.
Application logs are unchanged. The filter stays on the handler instance
across `logging.basicConfig`.

## Additional Notes

Original change: #19266 (`3e949c45d3`).
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.

2 participants