Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
0ca6b04
feat: add DD_TRACE_PROPAGATION_BEHAVIOR_EXTRACT config
MilanGarnier Jun 17, 2026
5a63578
feat: implement DD_TRACE_PROPAGATION_BEHAVIOR_EXTRACT behavior
MilanGarnier Jun 17, 2026
6914d02
test: add phpt tests for DD_TRACE_PROPAGATION_BEHAVIOR_EXTRACT
MilanGarnier Jun 17, 2026
68ddc93
fix: set implementation to B for DD_TRACE_PROPAGATION_BEHAVIOR_EXTRACT
MilanGarnier Jun 17, 2026
3ca8428
Remove breaking include
MilanGarnier Jun 17, 2026
f5db737
Fix: prevent sampling to propagate in restart mode
MilanGarnier Jun 18, 2026
6374dad
Own references to property attributes when creating attributes
MilanGarnier Jun 18, 2026
602e2be
Apply suggestion from @bwoebi
MilanGarnier Jun 18, 2026
a39e0a4
emalloc instead of statically sizer array for deleting tags
MilanGarnier Jun 18, 2026
cb5cc57
add check for trace id
MilanGarnier Jun 18, 2026
c7674bd
clean meta tags in one iteration
MilanGarnier Jun 18, 2026
fa54461
Also pass exctraction style, refactor bextract behavior logic
MilanGarnier Jun 18, 2026
5d0fc77
update test to match behavior
MilanGarnier Jun 18, 2026
ea4bf80
Fix parent id check, sampling cannot be tested here
MilanGarnier Jun 19, 2026
5832c73
sampling priority cannot be tested here
MilanGarnier Jun 19, 2026
19c7daf
Split config check test for different scenarios
MilanGarnier Jun 19, 2026
2bca52d
update otel bridge to support extract behavior=ignore & restart (with
MilanGarnier Jun 29, 2026
3a09adc
Merge branch 'master' into MilanGarnier/propagation-behavior-extract
MilanGarnier Jun 29, 2026
7e17c78
php 7 compatible test
MilanGarnier Jun 29, 2026
57a6961
Remove data being unwillingly propagated according to spec
MilanGarnier Jul 2, 2026
a25a0dd
Proper extract result cleanup in restart path
MilanGarnier Jul 2, 2026
da401ba
Merge branch 'master' into MilanGarnier/propagation-behavior-extract
MilanGarnier Jul 3, 2026
7cd5bf1
meta tags are necessary when building the link in
MilanGarnier Jul 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions metadata/supported-configurations.json
Original file line number Diff line number Diff line change
Expand Up @@ -1995,6 +1995,13 @@
"default": "false"
}
],
"DD_TRACE_PROPAGATION_BEHAVIOR_EXTRACT": [
{
"implementation": "B",
"type": "string",
"default": "continue"
}
],
"DD_TRACE_PROPAGATION_STYLE": [
{
"implementation": "D",
Expand Down
32 changes: 24 additions & 8 deletions src/DDTrace/OpenTelemetry/SpanBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -184,8 +184,13 @@ public function startSpan(): SpanInterface
$parentSpan = Span::fromContext($parentContext);
$parentSpanContext = $parentSpan->getContext();

$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);

$span = ($parentSpanContext->isValid() && !$restartOrIgnore) ? null : \DDTrace\start_trace_span($this->startEpochNanos);
$traceId = ($parentSpanContext->isValid() && !$restartOrIgnore)
? $parentSpanContext->getTraceId()
: \DDTrace\root_span()->traceId;

$samplingResult = $this
->tracerSharedState
Expand All @@ -205,7 +210,7 @@ public function startSpan(): SpanInterface
$sampled = SamplingResult::RECORD_AND_SAMPLE === $samplingDecision;
$samplingResultTraceState = $samplingResult->getTraceState();

if ($parentSpanContext->isValid()) {
if ($parentSpanContext->isValid() && !$restartOrIgnore) {
// Traceparent: {2:version}-{32:hex trace id}-{16:hex parent id}-{2:trace_flags}, version always being '00'
// Since parentSpanContext is valid, the trace identifiers are guaranteed to be in hexadecimal format
$parentId = $parentSpanContext->getSpanId();
Expand All @@ -215,6 +220,14 @@ public function startSpan(): SpanInterface
'traceparent' => $traceParent,
'tracestate' => (string) $samplingResultTraceState, // __toString() is implemented in TraceState
]);
} elseif ($parentSpanContext->isValid() && $behaviorExtract === 1) {
// RESTART: pass upstream W3C context so C code creates a span link and starts fresh trace
$parentId = $parentSpanContext->getSpanId();
$traceParent = "00-{$parentSpanContext->getTraceId()}-$parentId-01";
\DDTrace\consume_distributed_tracing_headers([
'traceparent' => $traceParent,
'tracestate' => (string) $samplingResultTraceState,
]);
} elseif ($samplingResultTraceState) {
$samplingResultTraceState = $samplingResultTraceState->without('dd');
\DDTrace\root_span()->tracestate = (string) $samplingResultTraceState;
Expand All @@ -234,10 +247,13 @@ public function startSpan(): SpanInterface
$this->attributes[$key] = $value;
}

$parentSpanContextBaggage = $parentContext->get(ContextKeys::baggage());
if ($parentSpanContextBaggage) {
foreach($parentSpanContextBaggage->getAll() as $baggageKey => $baggageEntry) {
$span->baggage[$baggageKey] = $baggageEntry->getValue();
if ($behaviorExtract !== 2) {
// `restart` or `continue` : propagate baggage
$parentSpanContextBaggage = $parentContext->get(ContextKeys::baggage());
if ($parentSpanContextBaggage) {
foreach ($parentSpanContextBaggage->getAll() as $baggageKey => $baggageEntry) {
$span->baggage[$baggageKey] = $baggageEntry->getValue();
}
}
}

Expand All @@ -249,7 +265,7 @@ public function startSpan(): SpanInterface
$parentSpan,
$parentContext,
$this->tracerSharedState->getSpanProcessor(),
$parentSpanContext->isValid() ? ResourceInfoFactory::emptyResource() : $this->tracerSharedState->getResource(),
($parentSpanContext->isValid() && !$restartOrIgnore) ? ResourceInfoFactory::emptyResource() : $this->tracerSharedState->getResource(),
$this->attributes,
$this->links,
$this->totalNumberOfLinksAdded,
Expand Down
70 changes: 70 additions & 0 deletions tests/OpenTelemetry/Integration/InteroperabilityTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -609,6 +609,76 @@ public function testMixingMultipleTraces()
], true, false);
}

public function testW3CInteroperabilityWithPropagationBehaviorRestart()
{
self::putEnvAndReloadConfig(['DD_TRACE_PROPAGATION_BEHAVIOR_EXTRACT=restart']);
try {
$this->isolateTracer(function () {
$tracer = self::getTracer();
$propagator = TraceContextPropagator::getInstance();

$carrier = [
TraceContextPropagator::TRACEPARENT => '00-ff0000000000051791e0000000000041-ff00051791e00041-01',
];

$context = $propagator->extract($carrier);

$OTelRootSpan = $tracer->spanBuilder("otel.root.span")
->setParent($context)
->startSpan();

$root = \DDTrace\root_span();

// Fresh trace: different from upstream trace ID
$this->assertNotSame('ff0000000000051791e0000000000041', $root->traceId);

// Span link capturing the upstream context
$this->assertCount(1, $root->links);
$link = $root->links[0];
$this->assertSame('ff0000000000051791e0000000000041', $link->traceId);
$this->assertSame('ff00051791e00041', $link->spanId);
$this->assertSame('propagation_behavior_extract', $link->attributes['reason'] ?? null);

$OTelRootSpan->end();
});
} finally {
self::putEnvAndReloadConfig(['DD_TRACE_PROPAGATION_BEHAVIOR_EXTRACT=continue']);
}
}

public function testW3CInteroperabilityWithPropagationBehaviorIgnore()
{
self::putEnvAndReloadConfig(['DD_TRACE_PROPAGATION_BEHAVIOR_EXTRACT=ignore']);
try {
$this->isolateTracer(function () {
$tracer = self::getTracer();
$propagator = TraceContextPropagator::getInstance();

$carrier = [
TraceContextPropagator::TRACEPARENT => '00-ff0000000000051791e0000000000041-ff00051791e00041-01',
];

$context = $propagator->extract($carrier);

$OTelRootSpan = $tracer->spanBuilder("otel.root.span")
->setParent($context)
->startSpan();

$root = \DDTrace\root_span();

// Fresh trace: no parent ID
$this->assertSame(0, $root->parentId ?? 0);

// No span links
$this->assertCount(0, $root->links);

$OTelRootSpan->end();
});
} finally {
self::putEnvAndReloadConfig(['DD_TRACE_PROPAGATION_BEHAVIOR_EXTRACT=continue']);
}
}

public function testW3CInteroperability()
{
$traces = $this->isolateTracer(function () {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
--TEST--
DD_TRACE_PROPAGATION_BEHAVIOR_EXTRACT: invalid value falls back to continue
--ENV--
DD_TRACE_GENERATE_ROOT_SPAN=0
DD_TRACE_PROPAGATION_BEHAVIOR_EXTRACT=invalid_value
--FILE--
<?php

DDTrace\consume_distributed_tracing_headers([
"x-datadog-trace-id" => 42,
"x-datadog-parent-id" => 10,
]);

$span = DDTrace\start_span();
$root = DDTrace\root_span();

// invalid value falls back to default (continue): inherits upstream trace id
echo "same_as_upstream: " . ($root->traceId === "0000000000000000000000000000002a" ? "yes" : "no") . "\n";

DDTrace\close_span();
?>
--EXPECT--
same_as_upstream: yes
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
--TEST--
DD_TRACE_PROPAGATION_BEHAVIOR_EXTRACT: Ignore (mixed case) is accepted
--ENV--
DD_TRACE_GENERATE_ROOT_SPAN=0
DD_TRACE_PROPAGATION_BEHAVIOR_EXTRACT=Ignore
--FILE--
<?php

DDTrace\consume_distributed_tracing_headers([
"x-datadog-trace-id" => 42,
"x-datadog-parent-id" => 10,
]);

$span = DDTrace\start_span();
$root = DDTrace\root_span();

echo "same_as_upstream: " . ($root->traceId === "0000000000000000000000000000002a" ? "yes" : "no") . "\n";
echo "links_count: " . count($root->links) . "\n";

DDTrace\close_span();
?>
--EXPECT--
same_as_upstream: no
links_count: 0
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
--TEST--
DD_TRACE_PROPAGATION_BEHAVIOR_EXTRACT: CONTINUE (uppercase) is accepted
--ENV--
DD_TRACE_GENERATE_ROOT_SPAN=0
DD_TRACE_PROPAGATION_BEHAVIOR_EXTRACT=CONTINUE
--FILE--
<?php

DDTrace\consume_distributed_tracing_headers([
"x-datadog-trace-id" => 42,
"x-datadog-parent-id" => 10,
]);

$span = DDTrace\start_span();
$root = DDTrace\root_span();

echo "same_as_upstream: " . ($root->traceId === "0000000000000000000000000000002a" ? "yes" : "no") . "\n";

DDTrace\close_span();
?>
--EXPECT--
same_as_upstream: yes
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
--TEST--
DD_TRACE_PROPAGATION_BEHAVIOR_EXTRACT: RESTART (uppercase) is accepted
--ENV--
DD_TRACE_GENERATE_ROOT_SPAN=0
DD_TRACE_PROPAGATION_BEHAVIOR_EXTRACT=RESTART
--FILE--
<?php

DDTrace\consume_distributed_tracing_headers([
"x-datadog-trace-id" => 42,
"x-datadog-parent-id" => 10,
]);

$span = DDTrace\start_span();
$root = DDTrace\root_span();

echo "same_as_upstream: " . ($root->traceId === "0000000000000000000000000000002a" ? "yes" : "no") . "\n";
echo "links_count: " . count($root->links) . "\n";

DDTrace\close_span();
?>
--EXPECT--
same_as_upstream: no
links_count: 1
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
--TEST--
DD_TRACE_PROPAGATION_BEHAVIOR_EXTRACT=continue inherits upstream context
--ENV--
DD_TRACE_GENERATE_ROOT_SPAN=0
DD_TRACE_PROPAGATION_BEHAVIOR_EXTRACT=continue
--FILE--
<?php

DDTrace\consume_distributed_tracing_headers([
"x-datadog-trace-id" => 42,
"x-datadog-parent-id" => 10,
"x-datadog-sampling-priority" => 1,
"baggage" => "user.id=123",
]);

$span = DDTrace\start_span();
$root = DDTrace\root_span();

echo "trace_id: " . $root->traceId . "\n";
echo "parent_id: " . $root->parentId . "\n";
echo "links_count: " . count($root->links) . "\n";

$headers = DDTrace\generate_distributed_tracing_headers(['baggage']);
echo "baggage: " . ($headers['baggage'] ?? 'none') . "\n";

DDTrace\close_span();
?>
--EXPECT--
trace_id: 0000000000000000000000000000002a
parent_id: 10
links_count: 0
baggage: user.id=123
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
--TEST--
DD_TRACE_PROPAGATION_BEHAVIOR_EXTRACT=ignore drops all incoming context including baggage
--ENV--
DD_TRACE_GENERATE_ROOT_SPAN=0
DD_TRACE_PROPAGATION_BEHAVIOR_EXTRACT=ignore
--FILE--
<?php

DDTrace\consume_distributed_tracing_headers([
"x-datadog-trace-id" => 42,
"x-datadog-parent-id" => 10,
"x-datadog-sampling-priority" => 2,
"x-datadog-tags" => "_dd.p.dm=-4",
"baggage" => "user.id=123",
]);

$span = DDTrace\start_span();
$root = DDTrace\root_span();

// fresh trace: not from upstream
echo "same_as_upstream: " . ($root->traceId === "0000000000000000000000000000002a" ? "yes" : "no") . "\n";
echo "parent_id: " . ($root->parentId ?? 0) . "\n";

// no span link (context discarded entirely)
echo "links_count: " . count($root->links) . "\n";

// baggage dropped
$headers = DDTrace\generate_distributed_tracing_headers(['baggage']);
echo "baggage: " . ($headers['baggage'] ?? 'none') . "\n";

DDTrace\close_span();
?>
--EXPECT--
same_as_upstream: no
parent_id: 0
links_count: 0
baggage: none
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
--TEST--
DD_TRACE_PROPAGATION_BEHAVIOR_EXTRACT=restart starts fresh trace with span link
--ENV--
DD_TRACE_GENERATE_ROOT_SPAN=0
DD_TRACE_PROPAGATION_BEHAVIOR_EXTRACT=restart
DD_TRACE_128_BIT_TRACEID_GENERATION_ENABLED=0
DD_TRACE_DEBUG_PRNG_SEED=42
--FILE--
<?php

DDTrace\consume_distributed_tracing_headers([
"x-datadog-trace-id" => 42,
"x-datadog-parent-id" => 10,
"x-datadog-sampling-priority" => 1,
"x-datadog-tags" => "_dd.p.foo=bar",
"baggage" => "user.id=123",
]);

$span = DDTrace\start_span();
$root = DDTrace\root_span();

// fresh trace: different from upstream trace_id 42
echo "same_as_upstream: " . ($root->traceId === "0000000000000000000000000000002a" ? "yes" : "no") . "\n";

// span link attached to root span
echo "links_count: " . count($root->links) . "\n";

$link = $root->links[0] ?? null;
if ($link !== null) {
// link captures upstream trace/span ids
echo "link_trace_id: " . $link->traceId . "\n";
echo "link_span_id: " . $link->spanId . "\n";
echo "link_reason: " . ($link->attributes['reason'] ?? 'missing') . "\n";
echo "link_context_headers: " . ($link->attributes['context_headers'] ?? 'missing') . "\n";
// _dd.p.foo captured in link attributes (upstream propagation context preserved in link)
echo "link_has_foo: " . (isset($link->attributes['_dd.p.foo']) ? "yes" : "no") . "\n";
}

$tid = $root->traceId;
echo "trace_id_valid: " . (preg_match('/^[0-9a-f]{32}$/', $tid) && $tid !== "00000000000000000000000000000000" ? "yes" : "no") . "\n";

// baggage preserved
$headers = DDTrace\generate_distributed_tracing_headers(['baggage']);
echo "baggage: " . ($headers['baggage'] ?? 'none') . "\n";

// upstream _dd.p.foo not in outbound tags
$dd_headers = DDTrace\generate_distributed_tracing_headers(['datadog']);
$tags = $dd_headers['x-datadog-tags'] ?? '';
echo "foo_in_tags: " . (strpos($tags, '_dd.p.foo') !== false ? "yes" : "no") . "\n";

DDTrace\close_span();
?>
--EXPECT--
same_as_upstream: no
links_count: 1
link_trace_id: 0000000000000000000000000000002a
link_span_id: 000000000000000a
link_reason: propagation_behavior_extract
link_context_headers: datadog
link_has_foo: yes
trace_id_valid: yes
baggage: user.id=123
foo_in_tags: no
Loading
Loading