Skip to content

feat(tracing): add DD_TRACE_PROPAGATION_BEHAVIOR_EXTRACT#3997

Open
MilanGarnier wants to merge 27 commits into
masterfrom
MilanGarnier/propagation-behavior-extract
Open

feat(tracing): add DD_TRACE_PROPAGATION_BEHAVIOR_EXTRACT#3997
MilanGarnier wants to merge 27 commits into
masterfrom
MilanGarnier/propagation-behavior-extract

Conversation

@MilanGarnier

Copy link
Copy Markdown
Contributor

Summary

Implements DD_TRACE_PROPAGATION_BEHAVIOR_EXTRACT (APMAPI-1941, feature-parity #353) for the PHP tracer, matching behavior already present in .NET, Python, Node, Java, Ruby, and Rust.

Three values:

  • continue (default): inherit upstream trace context unchanged — no behavior change
  • restart: start a fresh trace; the upstream context is captured as a span link with reason=propagation_behavior_extract
  • ignore: drop all extracted context including baggage and sampling priority

Design:

  • Single chokepoint: all extraction funnels through ddtrace_apply_distributed_tracing_result in distributed_tracing_headers.c, covering both request-init and consume_distributed_tracing_headers userland calls
  • restart path: when the root span doesn't yet exist at request-init time, the span link is queued in DDTRACE_G(pending_upstream_span_link) and consumed in ddtrace_open_span when the root span is created
  • Extracted ddtrace_build_span_link_from_result() from SpanLink::fromHeaders for reuse

Test plan

  • tests/ext/distributed_tracing/propagation_behavior_extract_continue.phpt — verifies trace_id inherited, no span link, baggage preserved
  • tests/ext/distributed_tracing/propagation_behavior_extract_restart.phpt — verifies fresh trace_id, span link with correct upstream ids and reason, baggage preserved, _dd.p.* not in link attributes
  • tests/ext/distributed_tracing/propagation_behavior_extract_ignore.phpt — verifies fresh trace_id, no span link, baggage dropped, sampling priority dropped
  • tests/ext/distributed_tracing/propagation_behavior_extract_config.phpt — verifies case-insensitive parsing, invalid value falls back to continue
  • CI phpt suite

Related

  • APMAPI-1941
  • Ruby: dd-trace-rb#5844
  • Rust: dd-trace-rs#248

🤖 Generated with Claude Code

@MilanGarnier MilanGarnier added the AI Generated Largely based on code generated by an AI or LLM. This label is the same across all dd-trace-* repos label Jun 17, 2026
@datadog-official

datadog-official Bot commented Jun 17, 2026

Copy link
Copy Markdown

Pipelines  Tests

Fix all issues with BitsAI

⚠️ Warnings

🚦 5 Pipeline jobs failed

DataDog/apm-reliability/dd-trace-php | ASAN test_c with multiple observers: [8.0]   View in Datadog   GitLab

DataDog/apm-reliability/dd-trace-php | Loader test on amd64 libc: [5.6, nts, buster]   View in Datadog   GitLab

DataDog/apm-reliability/dd-trace-php | merge-gate   View in Datadog   GitLab

View all 5 failed jobs.

ℹ️ Info

No other issues found (see more)

🧪 All tests passed
❄️ No new flaky tests detected

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

🎯 Code Coverage (details)
Patch Coverage: 100.00%
Overall Coverage: 54.08% (+0.00%)

Useful? React with 👍 / 👎

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

MilanGarnier and others added 5 commits June 17, 2026 17:11
Adds the new `DD_TRACE_PROPAGATION_BEHAVIOR_EXTRACT` config key (values:
continue, restart, ignore) with a CUSTOM(INT) parser, the corresponding
C enum, and the supported-configurations.json entry.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds three behaviors at the single extraction chokepoint
(ddtrace_apply_distributed_tracing_result):

- continue (default): inherit upstream trace context unchanged
- restart: start a fresh trace; upstream captured as a span link with
  reason=propagation_behavior_extract. When the root span doesn't exist
  yet at request-init time, the link is queued in
  DDTRACE_G(pending_upstream_span_link) and attached in ddtrace_open_span.
- ignore: drop all extracted context including baggage and sampling priority

Also extracts ddtrace_build_span_link_from_result() from the SpanLink
fromHeaders method so it can be reused by the restart path.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Covers the three behaviors (continue, restart, ignore) and config
parsing (case-insensitive values, invalid value falls back to default).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@MilanGarnier MilanGarnier force-pushed the MilanGarnier/propagation-behavior-extract branch from 6b7e259 to 3ca8428 Compare June 17, 2026 15:33
Comment thread tracer/distributed_tracing_headers.c Outdated
Comment thread tracer/distributed_tracing_headers.c Outdated
Comment thread tracer/distributed_tracing_headers.c Outdated
Comment thread tracer/functions.c
@MilanGarnier MilanGarnier marked this pull request as ready for review June 23, 2026 12:15
@MilanGarnier MilanGarnier requested a review from a team as a code owner June 23, 2026 12:15

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 19c7dafd9f

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread tracer/distributed_tracing_headers.c Outdated
Comment on lines +646 to +647
zend_hash_clean(&result->meta_tags);
zend_hash_clean(&result->propagated_tags);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Drop upstream tracestate on restart

When DD_TRACE_PROPAGATION_BEHAVIOR_EXTRACT=restart receives headers that include x-datadog-origin or W3C tracestate, this path only clears trace id/parent/sampling and _dd.p.*; the code below still moves result->origin, result->tracestate, and tracestate_unknown_dd_keys onto the new root/global context, and outbound injection later re-emits them. In that scenario the restarted trace continues propagating upstream trace context instead of preserving only baggage plus the span link.

Useful? React with 👍 / 👎.

@MilanGarnier MilanGarnier marked this pull request as draft June 29, 2026 11:41
@pr-commenter

pr-commenter Bot commented Jun 29, 2026

Copy link
Copy Markdown

Benchmarks [ tracer ]

Benchmark execution time: 2026-07-09 14:57:40

Comparing candidate commit e53c3ef in PR branch MilanGarnier/propagation-behavior-extract with baseline commit f99efe3 in branch master.

Found 1 performance improvements and 0 performance regressions! Performance is the same for 193 metrics, 0 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:MessagePackSerializationBench/benchMessagePackSerialization

  • 🟩 execution_time [-5.454µs; -3.146µs] or [-5.101%; -2.943%]

@MilanGarnier MilanGarnier removed the AI Generated Largely based on code generated by an AI or LLM. This label is the same across all dd-trace-* repos label Jul 6, 2026
@MilanGarnier MilanGarnier marked this pull request as ready for review July 9, 2026 13:29
@MilanGarnier MilanGarnier requested a review from a team as a code owner July 9, 2026 13:29
@MilanGarnier MilanGarnier requested review from LobeTia and removed request for a team July 9, 2026 13:29

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review


P2 Badge Do not sample restarted traces from the upstream parent

When restart or ignore is used with a remote OTel parent, the code starts a fresh trace but still passes the extracted $parentContext to shouldSample(). Parent-based samplers will continue to honor the upstream sampled/drop flags, so these modes do not actually discard the incoming sampling context even though the C extraction path clears propagated priority to UNKNOWN; use a root/empty parent context for sampling in these modes.

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +669 to +671
if (result->origin) { zend_string_release(result->origin); }
if (result->tracestate) { zend_string_release(result->tracestate); }
if (result->context_headers) { zend_string_release(result->context_headers); }

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Null released restart context before falling through

When restart extracts a valid upstream context that includes x-datadog-origin or W3C tracestate, this block releases those strings but then falls through to the normal apply path, which still checks and assigns result->origin/result->tracestate to the root span/global context; context_headers is also released again at function exit. Fresh evidence is that the current block releases these fields without nulling them or returning, so restart can store freed pointers and later double-free them instead of just dropping the upstream context.

Useful? React with 👍 / 👎.

$span = $parentSpanContext->isValid() ? null : \DDTrace\start_trace_span($this->startEpochNanos);
$traceId = $parentSpanContext->isValid() ? $parentSpanContext->getTraceId() : \DDTrace\root_span()->traceId;
$behaviorExtract = \dd_trace_env_config('DD_TRACE_PROPAGATION_BEHAVIOR_EXTRACT');
$restartOrIgnore = $parentSpanContext->isValid() && ($behaviorExtract === 1 || $behaviorExtract === 2);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Restrict extract behavior to remote OTel parents

With DD_TRACE_PROPAGATION_BEHAVIOR_EXTRACT=restart or ignore, this treats every valid OTel parent as extracted context. For ordinary in-process OTel children whose parent span is local, line 190 starts a new trace (and the restart branch can add a span link to the local parent) instead of preserving the parent/child relationship; the extract behavior should only apply when the parent context is remote/extracted, not for local spans created later in the same trace.

Useful? React with 👍 / 👎.

Comment on lines +226 to +229
$traceParent = "00-{$parentSpanContext->getTraceId()}-$parentId-01";
\DDTrace\consume_distributed_tracing_headers([
'traceparent' => $traceParent,
'tracestate' => (string) $samplingResultTraceState,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve upstream OTel tracestate in restart links

When restart is triggered from an extracted OTel W3C context that carries tracestate or an unsampled trace flag, this synthetic header hard-codes sampled (-01) and uses the local sampler's tracestate instead of the parent span context's trace flags/tracestate. The C restart path builds the span link from these headers, so the link loses upstream vendor tracestate and records local sampler state rather than the linked upstream context.

Useful? React with 👍 / 👎.

Comment thread tracer/span.c

ddtrace_set_root_span_properties(root);

if (primary_stack && Z_TYPE(DDTRACE_G(pending_upstream_span_link)) == IS_OBJECT) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Attach queued restart links to explicit trace roots

When request-init queues pending_upstream_span_link and the application creates its first root with DDTrace\start_trace_span() under DD_TRACE_GENERATE_ROOT_SPAN=0, start_trace_span() switches to a child root stack before ddtrace_open_span(), so primary_stack is false and this condition skips the queued link. In that flow restart starts a fresh trace but loses the upstream span link (or leaves it pending for a later unrelated primary-stack span), so explicit trace roots do not get the restart link promised by the new behavior.

Useful? React with 👍 / 👎.

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.

2 participants