Skip to content

Samplers never receive the parent's tracestate, and the composite sampler erases it - #5579

Open
dwin-gharibi wants to merge 2 commits into
open-telemetry:mainfrom
dwin-gharibi:fix/sampler-tracestate-propagation
Open

Samplers never receive the parent's tracestate, and the composite sampler erases it#5579
dwin-gharibi wants to merge 2 commits into
open-telemetry:mainfrom
dwin-gharibi:fix/sampler-tracestate-propagation

Conversation

@dwin-gharibi

Copy link
Copy Markdown

Closes #5578.

Description

Two independent omissions on the same path.

Tracer.start_span calls should_sample(context, trace_id, name, kind, attributes, links) and never passes the seventh parameter, trace_state. Separately, ParentBased.should_sample accepts trace_state but does not forward it to its delegate. Between them, no sampler ever receives a tracestate, even though the Sampler ABC declares the parameter and the comment at the call site says the sampler may modify it.

A third, related bug: _update_trace_state returns early when the incoming tracestate is empty, so SamplingIntent.update_trace_state is silently skipped for every root span.

Root cause

  • opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py:1171 - the argument is simply not passed.
  • opentelemetry-sdk/src/opentelemetry/sdk/trace/sampling.py:356-363 - ParentBased.should_sample builds the delegate call without trace_state.
  • opentelemetry-sdk/src/opentelemetry/sdk/trace/_sampling_experimental/_sampler.py:71 - _update_trace_state returns before intent.update_trace_state is ever called when the incoming tracestate is falsy.

Approach

Pass parent_span_context.trace_state from Tracer.start_span, guarding for root spans where there is no parent; forward trace_state through ParentBased; and in _update_trace_state, apply intent.update_trace_state to an empty TraceState() before the emptiness check rather than after it.

All three are one-line changes at the point where the value was already available.

Files changed

  • opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py
  • opentelemetry-sdk/src/opentelemetry/sdk/trace/sampling.py
  • opentelemetry-sdk/src/opentelemetry/sdk/trace/_sampling_experimental/_sampler.py
  • opentelemetry-sdk/tests/trace/test_sampler_tracestate.py
  • .changelog/5567.fixed

Testing

A spy sampler records what it is handed when driven through a real TracerProvider with a remote parent, covering both the bare sampler and the ParentBased composition. A third test asserts a root span still receives None, so "always pass something" cannot be mistaken for the fix.

End-to-end coverage extracts a remote context carrying vendora=alpha,ot=th:8, starts a child through the composite sampler and re-injects it, asserting the vendor entry survives. The default sampler is exercised as a control, since it already behaved correctly - that contrast is what makes the composite sampler's behaviour identifiable as a bug rather than a propagation problem.

Two further tests cover SamplingIntent.update_trace_state for both the absent and present incoming-tracestate cases.

Four of the seven fail before the change.

Result: 862 passed in opentelemetry-sdk (855 baseline plus 7 new).

Risk / compatibility

Samplers now receive a parameter they always declared, so a custom sampler that ignores it is unaffected and one that reads it starts working. SamplingResult.trace_state may now be non-None for root spans where a composable sampler asked for it - which is the intended behaviour. No public signatures change.

Sampler.should_sample declares a trace_state parameter and the consistent
probability sampling design reads the parent threshold out of it. Assert that
a sampler driven through Tracer.start_span actually receives it, that
ParentBased forwards it to its delegate, and that a root span correctly gets
None.

Also assert the composite sampler preserves vendor tracestate entries rather
than replacing the whole tracestate, and that
SamplingIntent.update_trace_state is applied to root spans as well as
children.

These tests fail against the current implementation.
Tracer.start_span never passed the trace_state argument, and ParentBased
accepted it without forwarding it to its delegate, so no sampler ever received
a tracestate despite the Sampler ABC declaring the parameter.

This left the consistent probability sampling design inert:
_ComposableParentThreshold reads the parent threshold out of tracestate and
always saw None, falling back to the sampled flag with threshold_reliable
false. Worse, _CompositeSampler rebuilds the outgoing tracestate from that
parameter, so given None it emitted a fresh tracestate containing only `ot`
and discarded every vendor entry from the incoming request.

Pass it from start_span, guarding for root spans, and forward it through
ParentBased. Also apply SamplingIntent.update_trace_state before the emptiness
check in _update_trace_state, so a composable sampler that stamps tracestate
is no longer skipped for root spans.
@dwin-gharibi
dwin-gharibi requested a review from a team as a code owner August 23, 2026 17:13
Copilot AI lite review requested due to automatic review settings August 23, 2026 17:13

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@opentelemetry-pr-dashboard

Copy link
Copy Markdown

Pull request dashboard status

Waiting on reviewers · refreshed 2026-08-23 17:14 UTC

Review the latest changes.

Status above doesn't look right?
  • Just replied or pushed? Anything around or after the refresh time above may not be picked up yet — give it a few minutes.
  • Anything look wrong? Report it with what you expected; it helps us improve the dashboard.

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

Labels

None yet

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

Samplers never receive the parent's tracestate, and the composite sampler erases it

2 participants