Skip to content

fix(tracing): correct otel thread context for active contexts, sampling and switches - #19704

Draft
florentinl wants to merge 2 commits into
mainfrom
florentin.labelle/otel-thread-context-correctness
Draft

fix(tracing): correct otel thread context for active contexts, sampling and switches#19704
florentinl wants to merge 2 commits into
mainfrom
florentin.labelle/otel-thread-context-correctness

Conversation

@florentinl

@florentinl florentinl commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Description

Three correctness fixes to the OTel thread-context publisher:

  • An active Context published nothing, so executor worker threads reported no trace for
    their whole lifetime. Now published.
  • The context-switch listener called active(), which repairs the contextvar and re-dispatches
    the activate event — mutating async context state mid-switch and publishing twice per switch.
    It now uses a read-only _peek_active().
  • trace_flags never reflected the sampling decision, which lands long after the record is
    published, so locally-initiated traces stayed marked unsampled. Now republished on the decision.

Costs active() +1.7 ns (per span finish) and Context.sampling_priority

Testing

Risks

Worker threads that used to publish nothing now publish the submitting span.

Additional Notes

…ng and switches

Three correctness issues in the OTel thread-context publisher:

- An active Context published nothing. It was treated as "no trace", but a
  Context is a span the execution runs inside without owning it -- a remote
  parent, or the submitter of offloaded work -- so all executor worker threads
  reported no trace. Adds update_otel_thread_context_ids for the case where
  there is no SpanData to read ids off.

- The switch listener called active(), which repairs the contextvar and
  re-dispatches the activate event. From a CPython context-switch watcher that
  writes to whichever context the switch just made current and publishes twice
  for one switch. The finished-span walk moves into a shared resolve_active,
  and a new _peek_active reports it without applying it; active() keeps its
  repair.

- trace_flags never reflected the sampling decision, which normally lands at
  trace-chunk finish, long after the record was published. The
  Context.sampling_priority setter now dispatches an event the publisher
  resyncs on.
@cit-pr-commenter-54b7da

cit-pr-commenter-54b7da Bot commented Aug 14, 2026

Copy link
Copy Markdown

Codeowners resolved as

Resolved from the full PR diff against main using the target branch CODEOWNERS file.
CODEOWNERS team requests not listed below are not required by the current file set.

ddtrace/_trace/context.py                                               @DataDog/apm-sdk-capabilities-python
ddtrace/internal/ci_visibility/context.py                               @DataDog/ci-app-libraries
ddtrace/internal/native/_native.pyi                                     @DataDog/apm-core-python
ddtrace/internal/opentelemetry/thread_context.py                        @DataDog/apm-sdk-capabilities-python @DataDog/asm-python @DataDog/profiling-python
ddtrace/llmobs/_context.py                                              @DataDog/ml-observability
releasenotes/notes/fix-otel-thread-context-correctness-0fb65fa39ac88d25.yaml  @DataDog/apm-python
src/native/context_provider.rs                                          @DataDog/apm-core-python
src/native/lib.rs                                                       @DataDog/apm-core-python
src/native/otel_thread_ctx.rs                                           @DataDog/apm-core-python
tests/tracer/test_otel_thread_context.py                                @DataDog/apm-sdk-capabilities-python @DataDog/apm-core-python @DataDog/asm-python
tests/tracer/test_tracer.py                                             @DataDog/apm-sdk-capabilities-python

@cit-pr-commenter-54b7da

cit-pr-commenter-54b7da Bot commented Aug 14, 2026

Copy link
Copy Markdown

Circular import analysis

⚠️ Existing circular imports

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

ddtrace.contrib.internal.pytorch._distributed -> ddtrace.contrib.internal.pytorch._rank_root -> ddtrace.contrib.internal.pytorch._distributed
ddtrace.contrib.internal.django.patch -> ddtrace.contrib.internal.django.response -> ddtrace.contrib.internal.django.patch
ddtrace.errortracking._handled_exceptions.bytecode_injector -> ddtrace.errortracking._handled_exceptions.callbacks -> ddtrace.errortracking._handled_exceptions.collector -> ddtrace.errortracking._handled_exceptions.bytecode_reporting -> ddtrace.errortracking._handled_exceptions.bytecode_injector
ddtrace.llmobs -> ddtrace.llmobs._evaluators -> ddtrace.llmobs._evaluators.format -> ddtrace.llmobs._experiment -> ddtrace.llmobs
ddtrace.appsec._asm_request_context -> ddtrace.appsec._iast._iast_request_context_base -> ddtrace.appsec._iast._iast_env -> ddtrace.appsec._iast.reporter -> ddtrace.appsec._exploit_prevention.stack_traces -> ddtrace.appsec._asm_request_context

@cit-pr-commenter-54b7da

cit-pr-commenter-54b7da Bot commented Aug 14, 2026

Copy link
Copy Markdown

Dependency direction analysis

⚠️ Existing dependency direction violations

There are 255 dependency direction violations that already exist on the base branch and have not been changed by this PR.

Show existing violations (showing 5 of 255 highest severity)
ddtrace.internal.tracemethods -×-> ddtrace.trace  (internal-core -> product:tracing, score=134)
ddtrace.llmobs._integrations.vertexai -×-> ddtrace.trace  (product:llmobs -> product:tracing, score=132)
ddtrace.internal.opentelemetry.context -×-> ddtrace.trace  (product:opentelemetry -> product:tracing, score=132)
ddtrace.llmobs._integrations.mcp -×-> ddtrace.trace  (product:llmobs -> product:tracing, score=132)
ddtrace.profiling.collector.stack -×-> ddtrace.trace  (product:profiling -> product:tracing, score=132)

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

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

@datadog-prod-us1-3

datadog-prod-us1-3 Bot commented Aug 14, 2026

Copy link
Copy Markdown

Pipelines  Tests

⚠️ Warnings

🚦 9 Pipeline jobs failed

DataDog/apm-reliability/dd-trace-py | build linux serverless: [amd64, cp315-cp315, v126532274-233089d-musllinux_1_2_x86_64, 1]   View in Datadog   GitLab

DataDog/apm-reliability/dd-trace-py | build linux serverless: [arm64, cp315-cp315, v113741357-d2b8243-manylinux2014_aarch64, 1]   View in Datadog   GitLab

DataDog/apm-reliability/dd-trace-py | build linux: [amd64, cp315-cp315, v113741238-d2b8243-manylinux2014_x86_64]   View in Datadog   GitLab

View all 9 failed jobs.

ℹ️ Info

No other issues found (see more)

🧪 All tests passed
❄️ No new flaky tests detected

Useful? React with 👍 / 👎

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

…e read-only peek

_peek_active guarded on the exact DefaultContextProvider type, so a subclass that
overrides nothing relevant -- tornado's TracerStackContext, which is installed on the
global tracer -- fell back to the mutating active(). The guard is replaced by the
requirement that providers with their own storage override _peek_active, which
CIContextProvider and LLMObsContextProvider now do explicitly.
@pr-commenter

pr-commenter Bot commented Aug 14, 2026

Copy link
Copy Markdown

Benchmarks

Benchmark execution time: 2026-08-14 15:20:26

Comparing candidate commit b48acd9 in PR branch florentin.labelle/otel-thread-context-correctness with baseline commit 5fe261d in branch main.

📊 Benchmarking dashboard

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

Explanation

This is an A/B test comparing a candidate commit's performance against that of a baseline commit. Performance changes are noted in the tables below as:

  • 🟩 = significantly better candidate vs. baseline
  • 🟥 = significantly worse candidate vs. baseline

We compute a confidence interval (CI) over the relative difference of means between metrics from the candidate and baseline commits, considering the baseline as the reference.

If the CI is entirely outside the configured SIGNIFICANT_IMPACT_THRESHOLD (or the deprecated UNCONFIDENCE_THRESHOLD), the change is considered significant.

Feel free to reach out to #apm-benchmarking-platform on Slack if you have any questions.

More details about the CI and significant changes

You can imagine this CI as a range of values that is likely to contain the true difference of means between the candidate and baseline commits.

CIs of the difference of means are often centered around 0%, because often changes are not that big:

---------------------------------(------|---^--------)-------------------------------->
                              -0.6%    0%  0.3%     +1.2%
                                 |          |        |
         lower bound of the CI --'          |        |
sample mean (center of the CI) -------------'        |
         upper bound of the CI ----------------------'

As described above, a change is considered significant if the CI is entirely outside the configured SIGNIFICANT_IMPACT_THRESHOLD (or the deprecated UNCONFIDENCE_THRESHOLD).

For instance, for an execution time metric, this confidence interval indicates a significantly worse performance:

----------------------------------------|---------|---(---------^---------)---------->
                                       0%        1%  1.3%      2.2%      3.1%
                                                  |   |         |         |
       significant impact threshold --------------'   |         |         |
                      lower bound of CI --------------'         |         |
       sample mean (center of the CI) --------------------------'         |
                      upper bound of CI ----------------------------------'

scenario:httppropagationinject-ids_only

  • 🟥 execution_time [+2.417µs; +2.595µs] or [+11.412%; +12.257%]

scenario:iastaspects-add_aspect

  • 🟥 execution_time [+7.521µs; +8.690µs] or [+7.621%; +8.806%]

scenario:iastaspects-lower_aspect

  • 🟥 execution_time [+40.726µs; +44.448µs] or [+17.527%; +19.129%]

scenario:iastaspectsospath-ospathbasename_aspect

  • 🟥 execution_time [+107.556µs; +112.747µs] or [+26.169%; +27.433%]

scenario:iastaspectssplit-rsplit_aspect

  • 🟥 execution_time [+18.846µs; +21.777µs] or [+13.432%; +15.521%]

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

  • 🟥 execution_time [+398.008ns; +440.042ns] or [+15.174%; +16.776%]

scenario:tracer-small

  • 🟥 execution_time [+36.985µs; +39.681µs] or [+10.583%; +11.354%]

Unstable benchmarks

These benchmarks have a confidence interval too wide to call a change; treat them as noise rather than signal.

scenario:coreapiscenario-context_with_data_listeners

  • unstable execution_time [-647.486ns; +869.719ns] or [-5.796%; +7.785%]

scenario:coreapiscenario-core_dispatch_1_listener

  • unstable execution_time [-39.240ns; +30.046ns] or [-6.042%; +4.626%]

scenario:coreapiscenario-core_dispatch_50_listeners

  • unstable execution_time [-1752.933ns; +1742.663ns] or [-9.734%; +9.677%]

scenario:coreapiscenario-core_dispatch_exception_listeners

  • unstable execution_time [-1582.812ns; +1041.794ns] or [-11.421%; +7.517%]

scenario:coreapiscenario-core_dispatch_listeners

  • unstable execution_time [-348.948ns; +340.425ns] or [-9.011%; +8.790%]

scenario:coreapiscenario-core_dispatch_no_args_listeners

  • unstable execution_time [-251.330ns; +269.926ns] or [-8.308%; +8.923%]

scenario:coreapiscenario-core_dispatch_with_results_1_listener

  • unstable execution_time [-76.507ns; +108.618ns] or [-5.572%; +7.910%]

scenario:coreapiscenario-core_dispatch_with_results_50_listeners

  • unstable execution_time [-3.633µs; +6.070µs] or [-7.499%; +12.527%]

scenario:coreapiscenario-core_dispatch_with_results_listeners

  • unstable execution_time [-839.393ns; +1020.572ns] or [-8.638%; +10.503%]

scenario:packagesupdateimporteddependencies-import_many_stdlib_cached

  • unstable execution_time [-63.742µs; +58.009µs] or [-9.877%; +8.988%]

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant