Skip to content

Commit 8f7e04e

Browse files
authored
feat(tracing): add DD_TRACE_PROPAGATION_BEHAVIOR_EXTRACT (#3997)
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 Co-authred/reviewed by: Bob Weinand <bob.weinand@datadoghq.com>
1 parent 262f8b8 commit 8f7e04e

20 files changed

Lines changed: 514 additions & 27 deletions

.gitlab/generate-package.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1569,7 +1569,8 @@ function appsec_image_from_tag_mapping(string $tag): string
15691569
rules:
15701570
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
15711571
when: never
1572-
- when: on_success
1572+
- when: always
1573+
allow_failure: true
15731574
variables:
15741575
GIT_STRATEGY: none
15751576
script:
@@ -1589,7 +1590,7 @@ function appsec_image_from_tag_mapping(string $tag): string
15891590
rules:
15901591
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
15911592
when: never
1592-
- when: on_success
1593+
- when: always
15931594
needs:
15941595
- job: "publish docker image for system tests (token)"
15951596
artifacts: true
@@ -1601,8 +1602,7 @@ function appsec_image_from_tag_mapping(string $tag): string
16011602
artifacts: true
16021603
variables:
16031604
GIT_STRATEGY: none
1604-
allow_failure:
1605-
exit_codes: 3
1605+
allow_failure: true
16061606
script: |
16071607
set -e
16081608
IMAGE="ghcr.io/datadog/dd-trace-php/dd-library-php:${CI_COMMIT_REF_SLUG}"

metadata/supported-configurations.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2002,6 +2002,13 @@
20022002
"default": "false"
20032003
}
20042004
],
2005+
"DD_TRACE_PROPAGATION_BEHAVIOR_EXTRACT": [
2006+
{
2007+
"implementation": "B",
2008+
"type": "string",
2009+
"default": "continue"
2010+
}
2011+
],
20052012
"DD_TRACE_PROPAGATION_STYLE": [
20062013
{
20072014
"implementation": "D",

src/DDTrace/OpenTelemetry/SpanBuilder.php

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -184,14 +184,21 @@ public function startSpan(): SpanInterface
184184
$parentSpan = Span::fromContext($parentContext);
185185
$parentSpanContext = $parentSpan->getContext();
186186

187-
$span = $parentSpanContext->isValid() ? null : \DDTrace\start_trace_span($this->startEpochNanos);
188-
$traceId = $parentSpanContext->isValid() ? $parentSpanContext->getTraceId() : \DDTrace\root_span()->traceId;
187+
$behaviorExtract = \dd_trace_env_config('DD_TRACE_PROPAGATION_BEHAVIOR_EXTRACT');
188+
$restartOrIgnore = $parentSpanContext->isValid() && ($behaviorExtract === 1 || $behaviorExtract === 2);
189+
190+
$span = ($parentSpanContext->isValid() && !$restartOrIgnore) ? null : \DDTrace\start_trace_span($this->startEpochNanos);
191+
$traceId = ($parentSpanContext->isValid() && !$restartOrIgnore)
192+
? $parentSpanContext->getTraceId()
193+
: \DDTrace\root_span()->traceId;
194+
195+
$samplingContext = $restartOrIgnore ? Context::getRoot() : $parentContext;
189196

190197
$samplingResult = $this
191198
->tracerSharedState
192199
->getSampler()
193200
->shouldSample(
194-
$parentContext,
201+
$samplingContext,
195202
$traceId,
196203
$this->spanName,
197204
$this->spanKind,
@@ -205,7 +212,7 @@ public function startSpan(): SpanInterface
205212
$sampled = SamplingResult::RECORD_AND_SAMPLE === $samplingDecision;
206213
$samplingResultTraceState = $samplingResult->getTraceState();
207214

208-
if ($parentSpanContext->isValid()) {
215+
if ($parentSpanContext->isValid() && !$restartOrIgnore) {
209216
// Traceparent: {2:version}-{32:hex trace id}-{16:hex parent id}-{2:trace_flags}, version always being '00'
210217
// Since parentSpanContext is valid, the trace identifiers are guaranteed to be in hexadecimal format
211218
$parentId = $parentSpanContext->getSpanId();
@@ -215,6 +222,15 @@ public function startSpan(): SpanInterface
215222
'traceparent' => $traceParent,
216223
'tracestate' => (string) $samplingResultTraceState, // __toString() is implemented in TraceState
217224
]);
225+
} elseif ($parentSpanContext->isValid() && $behaviorExtract === 1) {
226+
// RESTART: pass upstream W3C context so C code creates a span link and starts fresh trace
227+
$parentId = $parentSpanContext->getSpanId();
228+
$traceFlags = $parentSpanContext->isSampled() ? '01' : '00';
229+
$traceParent = "00-{$parentSpanContext->getTraceId()}-$parentId-$traceFlags";
230+
\DDTrace\consume_distributed_tracing_headers([
231+
'traceparent' => $traceParent,
232+
'tracestate' => (string) $parentSpanContext->getTraceState(),
233+
]);
218234
} elseif ($samplingResultTraceState) {
219235
$samplingResultTraceState = $samplingResultTraceState->without('dd');
220236
\DDTrace\root_span()->tracestate = (string) $samplingResultTraceState;
@@ -234,10 +250,13 @@ public function startSpan(): SpanInterface
234250
$this->attributes[$key] = $value;
235251
}
236252

237-
$parentSpanContextBaggage = $parentContext->get(ContextKeys::baggage());
238-
if ($parentSpanContextBaggage) {
239-
foreach($parentSpanContextBaggage->getAll() as $baggageKey => $baggageEntry) {
240-
$span->baggage[$baggageKey] = $baggageEntry->getValue();
253+
if ($behaviorExtract !== 2) {
254+
// `restart` or `continue` : propagate baggage
255+
$parentSpanContextBaggage = $parentContext->get(ContextKeys::baggage());
256+
if ($parentSpanContextBaggage) {
257+
foreach ($parentSpanContextBaggage->getAll() as $baggageKey => $baggageEntry) {
258+
$span->baggage[$baggageKey] = $baggageEntry->getValue();
259+
}
241260
}
242261
}
243262

@@ -249,7 +268,7 @@ public function startSpan(): SpanInterface
249268
$parentSpan,
250269
$parentContext,
251270
$this->tracerSharedState->getSpanProcessor(),
252-
$parentSpanContext->isValid() ? ResourceInfoFactory::emptyResource() : $this->tracerSharedState->getResource(),
271+
($parentSpanContext->isValid() && !$restartOrIgnore) ? ResourceInfoFactory::emptyResource() : $this->tracerSharedState->getResource(),
253272
$this->attributes,
254273
$this->links,
255274
$this->totalNumberOfLinksAdded,

tests/OpenTelemetry/Integration/InteroperabilityTest.php

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -609,6 +609,76 @@ public function testMixingMultipleTraces()
609609
], true, false);
610610
}
611611

612+
public function testW3CInteroperabilityWithPropagationBehaviorRestart()
613+
{
614+
self::putEnvAndReloadConfig(['DD_TRACE_PROPAGATION_BEHAVIOR_EXTRACT=restart']);
615+
try {
616+
$this->isolateTracer(function () {
617+
$tracer = self::getTracer();
618+
$propagator = TraceContextPropagator::getInstance();
619+
620+
$carrier = [
621+
TraceContextPropagator::TRACEPARENT => '00-ff0000000000051791e0000000000041-ff00051791e00041-01',
622+
];
623+
624+
$context = $propagator->extract($carrier);
625+
626+
$OTelRootSpan = $tracer->spanBuilder("otel.root.span")
627+
->setParent($context)
628+
->startSpan();
629+
630+
$root = \DDTrace\root_span();
631+
632+
// Fresh trace: different from upstream trace ID
633+
$this->assertNotSame('ff0000000000051791e0000000000041', $root->traceId);
634+
635+
// Span link capturing the upstream context
636+
$this->assertCount(1, $root->links);
637+
$link = $root->links[0];
638+
$this->assertSame('ff0000000000051791e0000000000041', $link->traceId);
639+
$this->assertSame('ff00051791e00041', $link->spanId);
640+
$this->assertSame('propagation_behavior_extract', $link->attributes['reason'] ?? null);
641+
642+
$OTelRootSpan->end();
643+
});
644+
} finally {
645+
self::putEnvAndReloadConfig(['DD_TRACE_PROPAGATION_BEHAVIOR_EXTRACT=continue']);
646+
}
647+
}
648+
649+
public function testW3CInteroperabilityWithPropagationBehaviorIgnore()
650+
{
651+
self::putEnvAndReloadConfig(['DD_TRACE_PROPAGATION_BEHAVIOR_EXTRACT=ignore']);
652+
try {
653+
$this->isolateTracer(function () {
654+
$tracer = self::getTracer();
655+
$propagator = TraceContextPropagator::getInstance();
656+
657+
$carrier = [
658+
TraceContextPropagator::TRACEPARENT => '00-ff0000000000051791e0000000000041-ff00051791e00041-01',
659+
];
660+
661+
$context = $propagator->extract($carrier);
662+
663+
$OTelRootSpan = $tracer->spanBuilder("otel.root.span")
664+
->setParent($context)
665+
->startSpan();
666+
667+
$root = \DDTrace\root_span();
668+
669+
// Fresh trace: no parent ID
670+
$this->assertSame(0, $root->parentId ?? 0);
671+
672+
// No span links
673+
$this->assertCount(0, $root->links);
674+
675+
$OTelRootSpan->end();
676+
});
677+
} finally {
678+
self::putEnvAndReloadConfig(['DD_TRACE_PROPAGATION_BEHAVIOR_EXTRACT=continue']);
679+
}
680+
}
681+
612682
public function testW3CInteroperability()
613683
{
614684
$traces = $this->isolateTracer(function () {
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
--TEST--
2+
DD_TRACE_PROPAGATION_BEHAVIOR_EXTRACT: invalid value falls back to continue
3+
--ENV--
4+
DD_TRACE_GENERATE_ROOT_SPAN=0
5+
DD_TRACE_PROPAGATION_BEHAVIOR_EXTRACT=invalid_value
6+
--FILE--
7+
<?php
8+
9+
DDTrace\consume_distributed_tracing_headers([
10+
"x-datadog-trace-id" => 42,
11+
"x-datadog-parent-id" => 10,
12+
]);
13+
14+
$span = DDTrace\start_span();
15+
$root = DDTrace\root_span();
16+
17+
// invalid value falls back to default (continue): inherits upstream trace id
18+
echo "same_as_upstream: " . ($root->traceId === "0000000000000000000000000000002a" ? "yes" : "no") . "\n";
19+
20+
DDTrace\close_span();
21+
?>
22+
--EXPECT--
23+
same_as_upstream: yes
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
--TEST--
2+
DD_TRACE_PROPAGATION_BEHAVIOR_EXTRACT: Ignore (mixed case) is accepted
3+
--ENV--
4+
DD_TRACE_GENERATE_ROOT_SPAN=0
5+
DD_TRACE_PROPAGATION_BEHAVIOR_EXTRACT=Ignore
6+
--FILE--
7+
<?php
8+
9+
DDTrace\consume_distributed_tracing_headers([
10+
"x-datadog-trace-id" => 42,
11+
"x-datadog-parent-id" => 10,
12+
]);
13+
14+
$span = DDTrace\start_span();
15+
$root = DDTrace\root_span();
16+
17+
echo "same_as_upstream: " . ($root->traceId === "0000000000000000000000000000002a" ? "yes" : "no") . "\n";
18+
echo "links_count: " . count($root->links) . "\n";
19+
20+
DDTrace\close_span();
21+
?>
22+
--EXPECT--
23+
same_as_upstream: no
24+
links_count: 0
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
--TEST--
2+
DD_TRACE_PROPAGATION_BEHAVIOR_EXTRACT: CONTINUE (uppercase) is accepted
3+
--ENV--
4+
DD_TRACE_GENERATE_ROOT_SPAN=0
5+
DD_TRACE_PROPAGATION_BEHAVIOR_EXTRACT=CONTINUE
6+
--FILE--
7+
<?php
8+
9+
DDTrace\consume_distributed_tracing_headers([
10+
"x-datadog-trace-id" => 42,
11+
"x-datadog-parent-id" => 10,
12+
]);
13+
14+
$span = DDTrace\start_span();
15+
$root = DDTrace\root_span();
16+
17+
echo "same_as_upstream: " . ($root->traceId === "0000000000000000000000000000002a" ? "yes" : "no") . "\n";
18+
19+
DDTrace\close_span();
20+
?>
21+
--EXPECT--
22+
same_as_upstream: yes
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
--TEST--
2+
DD_TRACE_PROPAGATION_BEHAVIOR_EXTRACT: RESTART (uppercase) is accepted
3+
--ENV--
4+
DD_TRACE_GENERATE_ROOT_SPAN=0
5+
DD_TRACE_PROPAGATION_BEHAVIOR_EXTRACT=RESTART
6+
--FILE--
7+
<?php
8+
9+
DDTrace\consume_distributed_tracing_headers([
10+
"x-datadog-trace-id" => 42,
11+
"x-datadog-parent-id" => 10,
12+
]);
13+
14+
$span = DDTrace\start_span();
15+
$root = DDTrace\root_span();
16+
17+
echo "same_as_upstream: " . ($root->traceId === "0000000000000000000000000000002a" ? "yes" : "no") . "\n";
18+
echo "links_count: " . count($root->links) . "\n";
19+
20+
DDTrace\close_span();
21+
?>
22+
--EXPECT--
23+
same_as_upstream: no
24+
links_count: 1
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
--TEST--
2+
DD_TRACE_PROPAGATION_BEHAVIOR_EXTRACT=continue inherits upstream context
3+
--ENV--
4+
DD_TRACE_GENERATE_ROOT_SPAN=0
5+
DD_TRACE_PROPAGATION_BEHAVIOR_EXTRACT=continue
6+
--FILE--
7+
<?php
8+
9+
DDTrace\consume_distributed_tracing_headers([
10+
"x-datadog-trace-id" => 42,
11+
"x-datadog-parent-id" => 10,
12+
"x-datadog-sampling-priority" => 1,
13+
"baggage" => "user.id=123",
14+
]);
15+
16+
$span = DDTrace\start_span();
17+
$root = DDTrace\root_span();
18+
19+
echo "trace_id: " . $root->traceId . "\n";
20+
echo "parent_id: " . $root->parentId . "\n";
21+
echo "links_count: " . count($root->links) . "\n";
22+
23+
$headers = DDTrace\generate_distributed_tracing_headers(['baggage']);
24+
echo "baggage: " . ($headers['baggage'] ?? 'none') . "\n";
25+
26+
DDTrace\close_span();
27+
?>
28+
--EXPECT--
29+
trace_id: 0000000000000000000000000000002a
30+
parent_id: 10
31+
links_count: 0
32+
baggage: user.id=123
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
--TEST--
2+
DD_TRACE_PROPAGATION_BEHAVIOR_EXTRACT=ignore drops all incoming context including baggage
3+
--ENV--
4+
DD_TRACE_GENERATE_ROOT_SPAN=0
5+
DD_TRACE_PROPAGATION_BEHAVIOR_EXTRACT=ignore
6+
--FILE--
7+
<?php
8+
9+
DDTrace\consume_distributed_tracing_headers([
10+
"x-datadog-trace-id" => 42,
11+
"x-datadog-parent-id" => 10,
12+
"x-datadog-sampling-priority" => 2,
13+
"x-datadog-tags" => "_dd.p.dm=-4",
14+
"baggage" => "user.id=123",
15+
]);
16+
17+
$span = DDTrace\start_span();
18+
$root = DDTrace\root_span();
19+
20+
// fresh trace: not from upstream
21+
echo "same_as_upstream: " . ($root->traceId === "0000000000000000000000000000002a" ? "yes" : "no") . "\n";
22+
echo "parent_id: " . ($root->parentId ?? 0) . "\n";
23+
24+
// no span link (context discarded entirely)
25+
echo "links_count: " . count($root->links) . "\n";
26+
27+
// baggage dropped
28+
$headers = DDTrace\generate_distributed_tracing_headers(['baggage']);
29+
echo "baggage: " . ($headers['baggage'] ?? 'none') . "\n";
30+
31+
DDTrace\close_span();
32+
?>
33+
--EXPECT--
34+
same_as_upstream: no
35+
parent_id: 0
36+
links_count: 0
37+
baggage: none

0 commit comments

Comments
 (0)