diff --git a/config.m4 b/config.m4 index de877e18c06..10ba080181e 100644 --- a/config.m4 +++ b/config.m4 @@ -331,6 +331,7 @@ if test "$PHP_DDTRACE" != "no" && test "$PHP_DDTRACE_PROFILING" = "no"; then tracer/live_debugger.c \ tracer/limiter/limiter.c \ tracer/memory_limit.c \ + tracer/otel_sampling.c \ tracer/tracer_otel_config.c \ tracer/priority_sampling/priority_sampling.c \ tracer/profiling.c \ diff --git a/config.w32 b/config.w32 index 76b15612f4e..ee7fb4237c7 100644 --- a/config.w32 +++ b/config.w32 @@ -70,6 +70,7 @@ if (PHP_DDTRACE != 'no') { DDTRACE_TRACER_SOURCES += " ip_extraction.c"; DDTRACE_TRACER_SOURCES += " live_debugger.c"; DDTRACE_TRACER_SOURCES += " memory_limit.c"; + DDTRACE_TRACER_SOURCES += " otel_sampling.c"; DDTRACE_TRACER_SOURCES += " tracer_otel_config.c"; DDTRACE_TRACER_SOURCES += " profiling.c"; DDTRACE_TRACER_SOURCES += " random.c"; diff --git a/tests/OpenTelemetry/Integration/API/TracerTest.php b/tests/OpenTelemetry/Integration/API/TracerTest.php index 025bc09539d..fcb598201bf 100644 --- a/tests/OpenTelemetry/Integration/API/TracerTest.php +++ b/tests/OpenTelemetry/Integration/API/TracerTest.php @@ -538,9 +538,9 @@ public function testGetSpanContextWithMultipleTraceStates() '_dd.p.congo' => 't61rcWkgMzE', '_dd.p.some_val' => 'tehehe' ]); - $this->assertRegularExpression('/^dd=p:[0-9a-f]{16};t.congo:t61rcWkgMzE;t.some_val:tehehe;t.dm:-0$/', (string)$span->getContext()->getTraceState()); + $this->assertRegularExpression('/^dd=p:[0-9a-f]{16};t.congo:t61rcWkgMzE;t.some_val:tehehe;t.dm:-0,ot=rv:[0-9a-f]{14};th:0$/', (string)$span->getContext()->getTraceState()); $span->end(); - $this->assertRegularExpression('/^dd=p:[0-9a-f]{16};t.congo:t61rcWkgMzE;t.some_val:tehehe;t.dm:-0$/', (string)$span->getContext()->getTraceState()); + $this->assertRegularExpression('/^dd=p:[0-9a-f]{16};t.congo:t61rcWkgMzE;t.some_val:tehehe;t.dm:-0,ot=rv:[0-9a-f]{14};th:0$/', (string)$span->getContext()->getTraceState()); }); $span = $traces[0][0]; @@ -659,11 +659,11 @@ public function getDescription(): string )))->getTracer('OpenTelemetry.TracerTest'); $parent = $tracer->spanBuilder("parent")->startSpan(); // root sampler will be used $scope = $parent->activate(); - $this->assertRegularExpression('/^dd=p:[0-9a-f]{16};t.dm:-0,root=yes,rojo=00f067aa0ba902b7,congo=t61rcWkgMzE$/', (string)$parent->getContext()->getTraceState()); + $this->assertRegularExpression('/^dd=p:[0-9a-f]{16};t.dm:-0,ot=rv:[0-9a-f]{14};th:0,root=yes,rojo=00f067aa0ba902b7,congo=t61rcWkgMzE$/', (string)$parent->getContext()->getTraceState()); $parent->setAttributes([ '_dd.p.some_val' => 'tehehe' ]); - $this->assertRegularExpression('/^dd=p:[0-9a-f]{16};t.dm:-0;t.some_val:tehehe,root=yes,rojo=00f067aa0ba902b7,congo=t61rcWkgMzE$/', (string)$parent->getContext()->getTraceState()); + $this->assertRegularExpression('/^dd=p:[0-9a-f]{16};t.dm:-0;t.some_val:tehehe,ot=rv:[0-9a-f]{14};th:0,root=yes,rojo=00f067aa0ba902b7,congo=t61rcWkgMzE$/', (string)$parent->getContext()->getTraceState()); try { $child = $tracer->spanBuilder("child")->startSpan(); // local parent sampler will be used @@ -673,7 +673,7 @@ public function getDescription(): string $this->assertFalse($childContext->isRemote()); // "When creating children from remote spans, their IsRemote flag MUST be set to false." $this->assertEquals(1, $childContext->getTraceFlags()); // RECORD_AND_SAMPLED ==> 01 (AlwaysOn sampler) $this->assertRegularExpression('/^dd=p:[0-9a-f]{16};t.dm:-0,localparent=yes,rojo=00f067aa0ba902b7,congo=t61rcWkgMzE$/', (string)$childContext->getTraceState()); - $this->assertRegularExpression('/^dd=p:[0-9a-f]{16};t.dm:-0;t.some_val:tehehe,root=yes,rojo=00f067aa0ba902b7,congo=t61rcWkgMzE$/', (string)$parent->getContext()->getTraceState()); + $this->assertRegularExpression('/^dd=p:[0-9a-f]{16};t.dm:-0;t.some_val:tehehe,ot=rv:[0-9a-f]{14};th:0,root=yes,rojo=00f067aa0ba902b7,congo=t61rcWkgMzE$/', (string)$parent->getContext()->getTraceState()); $grandChild = $tracer->spanBuilder("grandChild") ->setParent(Context::getCurrent()->withContextValue($child)) @@ -692,7 +692,7 @@ public function getDescription(): string $child->end(); } finally { - $this->assertRegularExpression('/^dd=p:[0-9a-f]{16};t.dm:-0;t.some_val:tehehe,root=yes,rojo=00f067aa0ba902b7,congo=t61rcWkgMzE$/', (string)$parent->getContext()->getTraceState()); + $this->assertRegularExpression('/^dd=p:[0-9a-f]{16};t.dm:-0;t.some_val:tehehe,ot=rv:[0-9a-f]{14};th:0,root=yes,rojo=00f067aa0ba902b7,congo=t61rcWkgMzE$/', (string)$parent->getContext()->getTraceState()); $scope->detach(); $parent->end(); } @@ -851,7 +851,7 @@ public function testAddItemToTracestate() '_dd.p.congo' => 't61rcWkgMzE', ]); - $this->assertRegularExpression('/^dd=p:[0-9a-f]{16};t.congo:t61rcWkgMzE;t.dm:-0$/', (string)$span->getContext()->getTraceState()); + $this->assertRegularExpression('/^dd=p:[0-9a-f]{16};t.congo:t61rcWkgMzE;t.dm:-0,ot=rv:[0-9a-f]{14};th:0$/', (string)$span->getContext()->getTraceState()); $traceState = $span->getContext()->getTraceState()->with('rojo', '00f067aa0ba902b7'); $context = SpanContext::create( @@ -865,7 +865,7 @@ public function testAddItemToTracestate() ->setParent(Context::getCurrent()->withContextValue(Span::wrap($context))) ->startSpan(); - $this->assertRegularExpression('/^dd=p:[0-9a-f]{16};t.congo:t61rcWkgMzE;t.dm:-0,rojo=00f067aa0ba902b7$/', (string)$child->getContext()->getTraceState()); + $this->assertRegularExpression('/^dd=p:[0-9a-f]{16};t.congo:t61rcWkgMzE;t.dm:-0,ot=rv:[0-9a-f]{14};th:0,rojo=00f067aa0ba902b7$/', (string)$child->getContext()->getTraceState()); $child->end(); $span->end(); diff --git a/tests/ext/distributed_tracing/distributed_trace_span_link_from_headers.phpt b/tests/ext/distributed_tracing/distributed_trace_span_link_from_headers.phpt index 0efc39bf45b..da8ac66001e 100644 --- a/tests/ext/distributed_tracing/distributed_trace_span_link_from_headers.phpt +++ b/tests/ext/distributed_tracing/distributed_trace_span_link_from_headers.phpt @@ -16,7 +16,7 @@ DDTrace\SpanLink Object ( [traceId] => 0000000000000000000000000000002a [spanId] => %s - [traceState] => dd=o:datadog;t.custom_tag:inherited;t.second_tag:bar;t.dm:-0 + [traceState] => dd=o:datadog;t.custom_tag:inherited;t.second_tag:bar;t.dm:-0,ot=rv:%x;th:0 [attributes] => Array ( [_dd.p.custom_tag] => inherited diff --git a/tests/ext/distributed_tracing/distributed_trace_span_link_from_headers_long_origin.phpt b/tests/ext/distributed_tracing/distributed_trace_span_link_from_headers_long_origin.phpt index 3375f9b35a4..75e68239d99 100644 --- a/tests/ext/distributed_tracing/distributed_trace_span_link_from_headers_long_origin.phpt +++ b/tests/ext/distributed_tracing/distributed_trace_span_link_from_headers_long_origin.phpt @@ -12,7 +12,7 @@ datadog.trace.propagation_style_extract=datadog --FILE-- "42", "x-datadog-parent-id" => "1", diff --git a/tests/ext/distributed_tracing/otel_sampling_caps.phpt b/tests/ext/distributed_tracing/otel_sampling_caps.phpt new file mode 100644 index 00000000000..1067c64dd3c --- /dev/null +++ b/tests/ext/distributed_tracing/otel_sampling_caps.phpt @@ -0,0 +1,65 @@ +--TEST-- +OpenTelemetry tracestate sampling honors value, member, and byte caps +--ENV-- +DD_TRACE_SAMPLE_RATE=0.5 +DD_TRACE_RATE_LIMIT=10000000 +--FILE-- +traceId = str_pad('1', 32, '0', STR_PAD_LEFT); + $root->tracestate = $tracestate; + $headers = DDTrace\generate_distributed_tracing_headers(['tracecontext']); + DDTrace\close_span(); + return $headers; +} + +$vendors = []; +for ($i = 0; $i < 32; ++$i) { + $vendors[] = "vendor{$i}=value"; +} +$tracestate = headersWithTracestate(implode(',', $vendors))['tracestate']; +$members = explode(',', $tracestate); +echo 'members=', count($members), ' leading=', implode(',', array_map( + function (string $member): string { + return strstr($member, '=', true); + }, + array_slice($members, 0, 2) +)), PHP_EOL; + +$largeVendors = []; +for ($i = 0; $i < 32; ++$i) { + $largeVendors[] = "vendor{$i}=" . str_repeat('x', 30); +} +$tracestate = headersWithTracestate(implode(',', $largeVendors))['tracestate']; +echo 'bytes=', strlen($tracestate) <= 512 ? 'within-cap' : 'over-cap', + ' complete=', substr($tracestate, -1) === 'x' ? 'yes' : 'no', PHP_EOL; + +$largeDatadog = 'dd=p:0000000000000001;t.large:' . str_repeat('x', 470); +$tracestate = headersWithTracestate($largeDatadog)['tracestate']; +$members = explode(',', $tracestate); +echo 'owned-bytes=', strlen($tracestate) <= 512 ? 'within-cap' : 'over-cap', + ' leading=', implode(',', array_map( + function (string $member): string { + return strstr($member, '=', true); + }, + array_slice($members, 0, 2) + )), + ' large=', strpos($tracestate, 't.large:') === false ? 'dropped' : 'kept', PHP_EOL; + +$oversizedUnknown = 'future:' . str_repeat('x', 230) . ';next:value'; +$tracestate = headersWithTracestate('ot=' . $oversizedUnknown)['tracestate']; +preg_match('/(?:^|,)ot=([^,]+)/', $tracestate, $matches); +echo 'ot-bytes=', strlen($matches[1]), + ' future=', strpos($matches[1], 'future:') === false ? 'dropped' : 'kept', + ' next=', strpos($matches[1], 'next:value') === false ? 'dropped' : 'kept', PHP_EOL; + +?> +--EXPECTF-- +members=32 leading=dd,ot +bytes=within-cap complete=yes +owned-bytes=within-cap leading=dd,ot large=dropped +ot-bytes=33 future=dropped next=kept diff --git a/tests/ext/distributed_tracing/otel_sampling_inherited.phpt b/tests/ext/distributed_tracing/otel_sampling_inherited.phpt new file mode 100644 index 00000000000..94302191d7e --- /dev/null +++ b/tests/ext/distributed_tracing/otel_sampling_inherited.phpt @@ -0,0 +1,82 @@ +--TEST-- +Inherited OpenTelemetry tracestate sampling fields are validated and forwarded +--ENV-- +DD_TRACE_SAMPLE_RATE=0.5 +DD_TRACE_RATE_LIMIT=10000000 +DD_TRACE_SAMPLING_RULES=[{"sample_rate":0.1,"service":"locally-decided","target_span":"any"}] +--FILE-- + '00-0000000000000000fff972474538efff-0000000000000001-0' . ($sampled ? '1' : '0'), + 'tracestate' => $tracestate, + ]); + if ($manualKeep) { + DDTrace\set_priority_sampling(DD_TRACE_PRIORITY_SAMPLING_USER_KEEP); + } + + $headers = DDTrace\generate_distributed_tracing_headers(['tracecontext']); + DDTrace\close_span(); + return $headers['tracestate']; +} + +function ot(string $tracestate): string +{ + foreach (explode(',', $tracestate) as $member) { + $member = ltrim($member, " \t"); + if (strncmp($member, 'ot=', 3) === 0) { + return substr($member, 3); + } + } + return ''; +} + +function locallyDecide(string $tracestate): string +{ + $span = DDTrace\start_span(); + DDTrace\consume_distributed_tracing_headers([ + 'traceparent' => '00-0000000000000000fff972474538efff-0000000000000001-00', + 'tracestate' => $tracestate, + ]); + $span->service = 'locally-decided'; + + $headers = DDTrace\generate_distributed_tracing_headers(['tracecontext']); + DDTrace\close_span(); + return $headers['tracestate']; +} + +echo ot(propagate('dd=s:2;t.dm:-3,ot=rv:ef284ace7a91e1;th:e6666666666668;foo:bar')), PHP_EOL; +echo ot(propagate('ot=th:e6666666666668')), PHP_EOL; +echo ot(propagate('dd=s:0,ot=rv:ef284ace7a91e1;th:e6666666666668', false)), PHP_EOL; +echo ot(propagate('dd=s:1,ot=rv:not-hex;th:not-hex,congo=xyz123')), PHP_EOL; +echo ot(propagate('ot=rv:1234567890abcd;th:not-hex')), PHP_EOL; +echo ot(propagate('dd=s:1')), PHP_EOL; +echo ot(propagate('ot=foo:bar')), PHP_EOL; +echo ot(propagate('ot=rv:65cd67504a538e;th:e6666666666668', false, true)), PHP_EOL; +echo ot(propagate('', false, true)), PHP_EOL; + +$ordered = propagate('dd=s:1,foo=bar,ot=rv:6e6d1a75832a2f,something=else'); +echo substr($ordered, strpos($ordered, ',') + 1), PHP_EOL; + +echo ot(locallyDecide('ot=rv:00000000000000;th:f0000000000000;foo:bar')), PHP_EOL; +echo ot(locallyDecide('ot=rv:00000000000000;foo:bar')), PHP_EOL; +echo ot(locallyDecide('ot=th:f0000000000000;foo:bar')), PHP_EOL; + +?> +--EXPECT-- +rv:ef284ace7a91e1;th:e6666666666668;foo:bar +th:e6666666666668 +rv:ef284ace7a91e1;th:e6666666666668 + +rv:1234567890abcd + +foo:bar +rv:65cd67504a538e + +ot=rv:6e6d1a75832a2f,foo=bar,something=else +rv:ef284ace7a91e1;th:e6666666666668;foo:bar +rv:ef284ace7a91e1;th:e6666666666668;foo:bar +rv:ef284ace7a91e1;th:e6666666666668;foo:bar diff --git a/tests/ext/distributed_tracing/otel_sampling_limiter.phpt b/tests/ext/distributed_tracing/otel_sampling_limiter.phpt new file mode 100644 index 00000000000..52d45d02a84 --- /dev/null +++ b/tests/ext/distributed_tracing/otel_sampling_limiter.phpt @@ -0,0 +1,30 @@ +--TEST-- +OpenTelemetry threshold is not emitted when the trace rate limiter makes the drop decision +--SKIPIF-- + +--ENV-- +DD_TRACE_AUTO_FLUSH_ENABLED=0 +DD_TRACE_GENERATE_ROOT_SPAN=0 +DD_TRACE_RATE_LIMIT=10 +DD_TRACE_SAMPLE_RATE=1 +--FILE-- + +--EXPECT-- +OK diff --git a/tests/ext/distributed_tracing/otel_sampling_precision.phpt b/tests/ext/distributed_tracing/otel_sampling_precision.phpt new file mode 100644 index 00000000000..e75bf78d242 --- /dev/null +++ b/tests/ext/distributed_tracing/otel_sampling_precision.phpt @@ -0,0 +1,21 @@ +--TEST-- +OpenTelemetry random value is reconciled with the 64-bit Datadog sampling decision +--ENV-- +DD_TRACE_SAMPLE_RATE=0.1 +DD_TRACE_RATE_LIMIT=10000000 +--FILE-- +traceId = str_pad('03a93ee8b1999f00', 32, '0', STR_PAD_LEFT); + +$headers = DDTrace\generate_distributed_tracing_headers(['tracecontext']); +preg_match('/(?:^|,)ot=([^,]+)/', $headers['tracestate'], $matches); +echo $matches[1], ' sampled=', substr($headers['traceparent'], -1), PHP_EOL; + +DDTrace\close_span(); + +?> +--EXPECT-- +rv:e6666666666668;th:e6666666666668 sampled=1 diff --git a/tests/ext/distributed_tracing/otel_sampling_precision_drop.phpt b/tests/ext/distributed_tracing/otel_sampling_precision_drop.phpt new file mode 100644 index 00000000000..42bfac04168 --- /dev/null +++ b/tests/ext/distributed_tracing/otel_sampling_precision_drop.phpt @@ -0,0 +1,21 @@ +--TEST-- +OpenTelemetry random value is reconciled for a 64-bit Datadog drop decision +--ENV-- +DD_TRACE_SAMPLE_RATE=0.05 +DD_TRACE_RATE_LIMIT=10000000 +--FILE-- +traceId = str_pad(dechex(5401449561355763072), 32, '0', STR_PAD_LEFT); + +$headers = DDTrace\generate_distributed_tracing_headers(['tracecontext']); +preg_match('/(?:^|,)ot=([^,]+)/', $headers['tracestate'], $matches); +echo $matches[1], ' sampled=', substr($headers['traceparent'], -1), PHP_EOL; + +DDTrace\close_span(); + +?> +--EXPECT-- +rv:f333333333332f;th:f333333333333 sampled=0 diff --git a/tests/ext/distributed_tracing/otel_sampling_probability.phpt b/tests/ext/distributed_tracing/otel_sampling_probability.phpt new file mode 100644 index 00000000000..ff8a6b90044 --- /dev/null +++ b/tests/ext/distributed_tracing/otel_sampling_probability.phpt @@ -0,0 +1,28 @@ +--TEST-- +OpenTelemetry tracestate fields are emitted for probability sampling decisions +--ENV-- +DD_TRACE_SAMPLE_RATE=0.5 +DD_TRACE_RATE_LIMIT=10000000 +--FILE-- +traceId = str_pad($traceId, 32, '0', STR_PAD_LEFT); + + $headers = DDTrace\generate_distributed_tracing_headers(['tracecontext']); + preg_match('/(?:^|,)ot=([^,]+)/', $headers['tracestate'], $matches); + echo $matches[1], ' sampled=', substr($headers['traceparent'], -1), PHP_EOL; + + DDTrace\close_span(); +} + +sample('1'); +sample('a'); + +?> +--EXPECT-- +rv:f0948a54d43b8e;th:8 sampled=1 +rv:65cd67504a538e;th:8 sampled=0 diff --git a/tests/ext/distributed_tracing/otel_sampling_propagation_behavior_ignore.phpt b/tests/ext/distributed_tracing/otel_sampling_propagation_behavior_ignore.phpt new file mode 100644 index 00000000000..03d07670f22 --- /dev/null +++ b/tests/ext/distributed_tracing/otel_sampling_propagation_behavior_ignore.phpt @@ -0,0 +1,34 @@ +--TEST-- +OpenTelemetry sampling state is regenerated when extraction ignores the context +--ENV-- +DD_TRACE_GENERATE_ROOT_SPAN=0 +DD_TRACE_PROPAGATION_BEHAVIOR_EXTRACT=ignore +DD_TRACE_PROPAGATION_STYLE_EXTRACT=tracecontext +DD_TRACE_SAMPLE_RATE=0.5 +DD_TRACE_RATE_LIMIT=10000000 +--FILE-- + "00-$inboundTraceId-0000000000000001-01", + 'tracestate' => "ot=rv:$inboundRandomValue;th:e6666666666668", +]); + +DDTrace\start_span(); +$headers = DDTrace\generate_distributed_tracing_headers(['tracecontext']); +preg_match('/(?:^|,)ot=([^,]+)/', $headers['tracestate'], $matches); +$ot = $matches[1] ?? ''; + +echo 'new trace: ', DDTrace\root_span()->traceId === $inboundTraceId ? 'no' : 'yes', PHP_EOL; +echo 'new probability state: ', preg_match('/^rv:[0-9a-f]{14};th:8$/', $ot) ? 'yes' : 'no', PHP_EOL; +echo 'inbound rv removed: ', strpos($ot, $inboundRandomValue) === false ? 'yes' : 'no', PHP_EOL; + +DDTrace\close_span(); + +?> +--EXPECT-- +new trace: yes +new probability state: yes +inbound rv removed: yes diff --git a/tests/ext/distributed_tracing/otel_sampling_propagation_behavior_restart.phpt b/tests/ext/distributed_tracing/otel_sampling_propagation_behavior_restart.phpt new file mode 100644 index 00000000000..426a5353a9f --- /dev/null +++ b/tests/ext/distributed_tracing/otel_sampling_propagation_behavior_restart.phpt @@ -0,0 +1,34 @@ +--TEST-- +OpenTelemetry sampling state is regenerated when extraction restarts the trace +--ENV-- +DD_TRACE_GENERATE_ROOT_SPAN=0 +DD_TRACE_PROPAGATION_BEHAVIOR_EXTRACT=restart +DD_TRACE_PROPAGATION_STYLE_EXTRACT=tracecontext +DD_TRACE_SAMPLE_RATE=0.5 +DD_TRACE_RATE_LIMIT=10000000 +--FILE-- + "00-$inboundTraceId-0000000000000001-01", + 'tracestate' => "ot=rv:$inboundRandomValue;th:e6666666666668", +]); + +DDTrace\start_span(); +$headers = DDTrace\generate_distributed_tracing_headers(['tracecontext']); +preg_match('/(?:^|,)ot=([^,]+)/', $headers['tracestate'], $matches); +$ot = $matches[1] ?? ''; + +echo 'new trace: ', DDTrace\root_span()->traceId === $inboundTraceId ? 'no' : 'yes', PHP_EOL; +echo 'new probability state: ', preg_match('/^rv:[0-9a-f]{14};th:8$/', $ot) ? 'yes' : 'no', PHP_EOL; +echo 'inbound rv removed: ', strpos($ot, $inboundRandomValue) === false ? 'yes' : 'no', PHP_EOL; + +DDTrace\close_span(); + +?> +--EXPECT-- +new trace: yes +new probability state: yes +inbound rv removed: yes diff --git a/tests/ext/distributed_tracing/otel_sampling_span_link_caps.phpt b/tests/ext/distributed_tracing/otel_sampling_span_link_caps.phpt new file mode 100644 index 00000000000..ce06c4c2a2e --- /dev/null +++ b/tests/ext/distributed_tracing/otel_sampling_span_link_caps.phpt @@ -0,0 +1,50 @@ +--TEST-- +SpanLink::fromHeaders reapplies tracestate limits after rebuilding Datadog state +--ENV-- +DD_TRACE_GENERATE_ROOT_SPAN=0 +DD_TRACE_PROPAGATION_STYLE_EXTRACT=datadog,tracecontext +--FILE-- + '42', + 'x-datadog-parent-id' => '1', + 'x-datadog-tags' => '_dd.p.test=value', + 'traceparent' => '00-0000000000000000000000000000002a-0000000000000001-01', + 'tracestate' => implode(',', $members), + ]); + + $tracestate = $link->traceState; + $members = explode(',', $tracestate); + $keys = array_map( + function (string $member): string { + return strstr($member, '=', true); + }, + $members + ); + + echo 'members=', count($members), + ' bytes=', strlen($tracestate) <= 512 ? 'within-cap' : 'over-cap', + ' leading=', implode(',', array_slice($keys, 0, 2)), + ' last=', end($keys), PHP_EOL; +} + +$members = ['ot=rv:ef284ace7a91e1;th:8']; +for ($i = 0; $i < 31; ++$i) { + $members[] = "vendor{$i}=value"; +} +report($members); + +$members = []; +for ($i = 0; $i < 32; ++$i) { + $members[] = "vendor{$i}=value"; +} +$members[] = 'ot=rv:ef284ace7a91e1;th:8'; +report($members); + +?> +--EXPECT-- +members=32 bytes=within-cap leading=dd,ot last=vendor29 +members=32 bytes=within-cap leading=dd,ot last=vendor29 diff --git a/tests/ext/integrations/curl/distributed_tracing_curl.phpt b/tests/ext/integrations/curl/distributed_tracing_curl.phpt index 9a561f6649a..3484ebedcf6 100644 --- a/tests/ext/integrations/curl/distributed_tracing_curl.phpt +++ b/tests/ext/integrations/curl/distributed_tracing_curl.phpt @@ -56,7 +56,7 @@ echo 'Done.' . PHP_EOL; [ddtrace] [error] [%d] The to be propagated tag '_dd.p.very=looooooooooooooooong' is too long and exceeds the maximum limit of 25 characters and is thus dropped. b3: %s-%s-1 traceparent: 00-%s-%s -tracestate: dd=p:%s;o:phpt-test +tracestate: dd=p:%s;o:phpt-test,ot=rv:785eb1ead1c555;th:0 x-b3-spanid: %s x-b3-traceid: %s x-datadog-origin: phpt-test diff --git a/tests/ext/integrations/curl/distributed_tracing_curl_invalid_tags.phpt b/tests/ext/integrations/curl/distributed_tracing_curl_invalid_tags.phpt index 761e03d937c..134d2b895b2 100644 --- a/tests/ext/integrations/curl/distributed_tracing_curl_invalid_tags.phpt +++ b/tests/ext/integrations/curl/distributed_tracing_curl_invalid_tags.phpt @@ -43,7 +43,7 @@ echo 'Done.' . PHP_EOL; --EXPECTF-- b3: %s248869c998246a2e-248869c998246a2e-1 traceparent: 00-%s248869c998246a2e-248869c998246a2e-03 -tracestate: dd=p:248869c998246a2e;o:_______~_: ;t.escaped:_~_: ;t.dm:-0 +tracestate: dd=p:248869c998246a2e;o:_______~_: ;t.escaped:_~_: ;t.dm:-0,ot=rv:30493bb1a39b28;th:0 x-datadog-origin: ∂~,=;: x-datadog-tags: _dd.p.tid=%s,_dd.p.escaped=_=;: ,_dd.p.dm=-0 bool(false) diff --git a/tests/snapshots/tests.integrations.kafka_test.test_span_links_high_level_consumer.json b/tests/snapshots/tests.integrations.kafka_test.test_span_links_high_level_consumer.json index 57d057e8900..54a5a7197cc 100644 --- a/tests/snapshots/tests.integrations.kafka_test.test_span_links_high_level_consumer.json +++ b/tests/snapshots/tests.integrations.kafka_test.test_span_links_high_level_consumer.json @@ -9,7 +9,7 @@ "type": "queue", "meta": { "_dd.p.dm": "0", - "_dd.span_links": "[{\"trace_id\":\"0000000000000000c151df7d6ee5e2d6\",\"span_id\":\"c151df7d6ee5e2d6\",\"trace_state\":\"dd=t.dm:-0\",\"attributes\":{\"_dd.p.dm\":\"-0\"}}]", + "_dd.span_links": "[{\"trace_id\":\"0000000000000000c151df7d6ee5e2d6\",\"span_id\":\"c151df7d6ee5e2d6\",\"trace_state\":\"dd=t.dm:-0,ot=rv:ea5f0f7db45435;th:0\",\"attributes\":{\"_dd.p.dm\":\"-0\"}}]", "component": "kafka", "messaging.destination": "test-highlevel", "messaging.destination_kind": "queue", diff --git a/tests/snapshots/tests.integrations.kafka_test.test_span_links_low_level_consumer.json b/tests/snapshots/tests.integrations.kafka_test.test_span_links_low_level_consumer.json index 32185d854b7..904d2796f9b 100644 --- a/tests/snapshots/tests.integrations.kafka_test.test_span_links_low_level_consumer.json +++ b/tests/snapshots/tests.integrations.kafka_test.test_span_links_low_level_consumer.json @@ -9,7 +9,7 @@ "type": "queue", "meta": { "_dd.p.dm": "0", - "_dd.span_links": "[{\"trace_id\":\"0000000000000000c151df7d6ee5e2d6\",\"span_id\":\"c151df7d6ee5e2d6\",\"trace_state\":\"dd=t.dm:-0\",\"attributes\":{\"_dd.p.dm\":\"-0\"}}]", + "_dd.span_links": "[{\"trace_id\":\"0000000000000000c151df7d6ee5e2d6\",\"span_id\":\"c151df7d6ee5e2d6\",\"trace_state\":\"dd=t.dm:-0,ot=rv:ea5f0f7db45435;th:0\",\"attributes\":{\"_dd.p.dm\":\"-0\"}}]", "component": "kafka", "messaging.destination": "test-lowlevel", "messaging.destination_kind": "queue", diff --git a/tracer/ddtrace.c b/tracer/ddtrace.c index df9b73ef12c..7f56228d9bd 100644 --- a/tracer/ddtrace.c +++ b/tracer/ddtrace.c @@ -442,6 +442,7 @@ static void dd_initialize_request(void) { DDTRACE_G(distributed_trace_id) = (datadog_trace_id){0}; DDTRACE_G(distributed_parent_trace_id) = 0; DDTRACE_G(distributed_trace_flags) = 0; + DDTRACE_G(otel_sampling) = (ddtrace_otel_sampling_state){0}; DDTRACE_G(additional_global_tags) = zend_new_array(0); DDTRACE_G(default_priority_sampling) = DDTRACE_PRIORITY_SAMPLING_UNKNOWN; DDTRACE_G(propagated_priority_sampling) = DDTRACE_PRIORITY_SAMPLING_UNSET; @@ -735,10 +736,12 @@ void ddtrace_internal_handle_fork() { DDTRACE_G(distributed_trace_id) = ddtrace_peek_trace_id(); ddtrace_root_span_data *root = DDTRACE_G(active_stack) ? DDTRACE_G(active_stack)->root_span : NULL; DDTRACE_G(distributed_trace_flags) = root ? root->trace_flags : 0; + DDTRACE_G(otel_sampling) = root ? root->otel_sampling : (ddtrace_otel_sampling_state){0}; } else { DDTRACE_G(distributed_parent_trace_id) = 0; DDTRACE_G(distributed_trace_id) = (datadog_trace_id){0}; DDTRACE_G(distributed_trace_flags) = 0; + DDTRACE_G(otel_sampling) = (ddtrace_otel_sampling_state){0}; } ddtrace_free_span_stacks(true); ddtrace_init_span_stacks(); diff --git a/tracer/ddtrace_globals.h b/tracer/ddtrace_globals.h index 062a02ca797..a0bc90fb8c5 100644 --- a/tracer/ddtrace_globals.h +++ b/tracer/ddtrace_globals.h @@ -7,6 +7,7 @@ #endif #include +#include "otel_sampling.h" typedef struct ddtrace_span_ids_t ddtrace_span_ids_t; typedef struct ddtrace_span_data ddtrace_span_data; @@ -44,6 +45,7 @@ typedef struct { zend_array root_span_tags_preset; zend_array propagated_root_span_tags; zend_string *tracestate; + ddtrace_otel_sampling_state otel_sampling; zend_array tracestate_unknown_dd_keys; ddtrace_error_data active_error; HashTable baggage; diff --git a/tracer/distributed_tracing_headers.c b/tracer/distributed_tracing_headers.c index 732d64014e9..c7f019fc79d 100644 --- a/tracer/distributed_tracing_headers.c +++ b/tracer/distributed_tracing_headers.c @@ -1,4 +1,5 @@ #include "distributed_tracing_headers.h" +#include "otel_sampling.h" #include "configuration.h" #ifdef __linux__ #include "otel_context.h" @@ -7,12 +8,15 @@ #include "tracer_tag_propagation/tracer_tag_propagation.h" #include "serializer.h" #include "trace_context.h" +#include "tracestate.h" #include "zend_string.h" #include #include ZEND_EXTERN_MODULE_GLOBALS(datadog); +#define DDTRACE_TRACESTATE_MAX_VENDOR_MEMBERS 30 + static inline bool dd_is_hex_char(char chr) { return (chr >= '0' && chr <= '9') || (chr >= 'a' && chr <= 'f'); } @@ -56,6 +60,89 @@ static inline int hex2int(char c) { (c >= 'A' && c <= 'F') ? c - 'A' + 10 : -1; } +static void ddtrace_parse_datadog_tracestate_member(ddtrace_distributed_tracing_result *result, char *member, + char *member_end, size_t *tags_size, + zend_string **span_parent_key) { + while (member < member_end && *member != '=') { + ++member; + } + + while (member < member_end) { + char *keystart = ++member; + while (member < member_end && *member != ';' && *member != ':') { + ++member; + } + size_t keylen = member - keystart; + if (member >= member_end) { + break; + } + + char *valuestart = ++member; + while (member < member_end && *member != ';') { + ++member; + } + char *valueend = member; + while (valueend > valuestart && (valueend[-1] == ' ' || valueend[-1] == '\t')) { + --valueend; + } + size_t valuelen = valueend - valuestart; + + if (keylen == 1 && keystart[0] == 'p') { + if (*span_parent_key) { + zval zv; + ZVAL_STRINGL(&zv, valuestart, valuelen); + zend_hash_update(&result->meta_tags, *span_parent_key, &zv); + zend_string_release(*span_parent_key); + *span_parent_key = NULL; + } + } else if (keylen == 1 && keystart[0] == 's') { + int extracted_priority = strtol(valuestart, NULL, 10); + if ((result->priority_sampling > 0) == (extracted_priority > 0)) { + result->priority_sampling = extracted_priority; + } else { + result->conflicting_sampling_priority = true; + } + } else if (keylen == 1 && keystart[0] == 'o') { + if (result->origin) { + zend_string_release(result->origin); + } + result->origin = zend_string_init(valuestart, valuelen, 0); + for (char *valptr = ZSTR_VAL(result->origin), *valend = valptr + valuelen; valptr < valend; ++valptr) { + if (*valptr == '~') { + *valptr = '='; + } + } + } else if (keylen > 2 && keystart[0] == 't' && keystart[1] == '.') { + *tags_size += keylen + sizeof("_dd.") + valuelen; + if (*tags_size < 512) { + zend_string *tag_name = zend_strpprintf(0, "_dd.p.%.*s", (int)keylen - 2, keystart + 2); + zval zv; + ZVAL_STRINGL(&zv, valuestart, valuelen); + for (char *valptr = Z_STRVAL(zv), *valend = valptr + valuelen; valptr < valend; ++valptr) { + if (*valptr == '~') { + *valptr = '='; + } + } + zend_hash_update(&result->meta_tags, tag_name, &zv); + zend_hash_add_empty_element(&result->propagated_tags, tag_name); + zend_string_release(tag_name); + } else { + zval error_zv; + ZVAL_STRING(&error_zv, "extract_max_size"); + zend_hash_str_update(&result->meta_tags, ZEND_STRL("_dd.propagation_error"), &error_zv); + } + } else if (zend_hash_num_elements(&result->tracestate_unknown_dd_keys) < 100) { + zval zv; + ZVAL_STRINGL(&zv, valuestart, valuelen); + zend_hash_str_update(&result->tracestate_unknown_dd_keys, keystart, keylen, &zv); + } + + if (member >= member_end || *member != ';') { + break; + } + } +} + static void ddtrace_deserialize_baggage(char *baggage_ptr, char *baggage_end, HashTable *baggage) { bool is_malformed = false; uint64_t item_count = 0; @@ -370,105 +457,51 @@ static ddtrace_distributed_tracing_result ddtrace_read_distributed_tracing_ids_t // header format: "[*,]dd=p:0000000000000111;s:1;o:rum;t.dm:-4;t.usr.id:12345[,*]" if (read_header((zai_str)ZAI_STRL("TRACESTATE"), "tracestate", &tracestate, data)) { - bool last_comma = true; result.tracestate = zend_string_alloc(ZSTR_LEN(tracestate), 0); char *persist = ZSTR_VAL(result.tracestate); - int commas = 0; size_t tags_size = 0; - for (char *ptr = ZSTR_VAL(tracestate), *end = ptr + ZSTR_LEN(tracestate); ptr < end; ++ptr) { - // dd member - if (last_comma && ptr + 2 < end && ptr[0] == 'd' && ptr[1] == 'd' && (ptr[2] == '=' || ptr[2] == '\t' || ptr[2] == ' ')) { - // If there's dd= members, ignore x-datadog-tags fully - while (ptr < end && *ptr != '=') { - ++ptr; - } - - do { - char *keystart = ++ptr; - while (ptr < end && *ptr != ';' && *ptr != ',' && *ptr != ':') { - ++ptr; - } - size_t keylen = ptr - keystart; - if (ptr >= end) { - break; - } - char *valuestart = ++ptr; - while (ptr < end && *ptr != ';' && *ptr != ',') { - ++ptr; - } - char *valueend = ptr; - while (*valueend == ' ' || *valueend == '\t') { - --valueend; - } - size_t valuelen = valueend - valuestart; - - if (keylen == 1 && keystart[0] == 'p') { - if (span_parent_key) { - zval zv; - ZVAL_STRINGL(&zv, valuestart, valuelen); - zend_hash_update(&result.meta_tags, span_parent_key, &zv); - zend_string_release(span_parent_key); - span_parent_key = NULL; - } - } else if (keylen == 1 && keystart[0] == 's') { - int extraced_priority = strtol(valuestart, NULL, 10); - if ((result.priority_sampling > 0) == (extraced_priority > 0)) { - result.priority_sampling = extraced_priority; - } else { - result.conflicting_sampling_priority = true; - } - } else if (keylen == 1 && keystart[0] == 'o') { - if (result.origin) { - zend_string_release(result.origin); - } - result.origin = zend_string_init(valuestart, valuelen, 0); - for (char *valptr = ZSTR_VAL(result.origin), *valend = valptr + valuelen; valptr < valend; ++valptr) { - if (*valptr == '~') { - *valptr = '='; - } - } - } else if (keylen > 2 && keystart[0] == 't' && keystart[1] == '.') { - tags_size += keylen + sizeof("_dd.") + valuelen; - if (tags_size < 512) { - zend_string *tag_name = zend_strpprintf(0, "_dd.p.%.*s", (int) keylen - 2, keystart + 2); - zval zv; - ZVAL_STRINGL(&zv, valuestart, valuelen); - for (char *valptr = Z_STRVAL(zv), *valend = valptr + valuelen; valptr < valend; ++valptr) { - if (*valptr == '~') { - *valptr = '='; - } - } - zend_hash_update(&result.meta_tags, tag_name, &zv); - zend_hash_add_empty_element(&result.propagated_tags, tag_name); - zend_string_release(tag_name); - } else { - zval error_zv; - ZVAL_STRING(&error_zv, "extract_max_size"); - zend_hash_str_update(&result.meta_tags, ZEND_STRL("_dd.propagation_error"), &error_zv); - } - } else { - if (zend_hash_num_elements(&result.tracestate_unknown_dd_keys) < 100) { - zval zv; - ZVAL_STRINGL(&zv, valuestart, valuelen); - zend_hash_str_update(&result.tracestate_unknown_dd_keys, keystart, keylen, &zv); - } - } - } while (*ptr == ';'); - - continue; + size_t vendor_member_count = 0; + bool found_otel = false; + char *end = ZSTR_VAL(tracestate) + ZSTR_LEN(tracestate); + for (char *member = ZSTR_VAL(tracestate); member < end;) { + char *member_end = memchr(member, ',', end - member); + if (!member_end) { + member_end = end; } - *(persist++) = *ptr; - if (*ptr == ' ' || *ptr == '\t') { - continue; + char *trimmed_member = member; + while (trimmed_member < member_end && (*trimmed_member == ' ' || *trimmed_member == '\t')) { + ++trimmed_member; } - - last_comma = *ptr == ','; - // preserve only up to 31 vendor specific values, excluding our own - if (last_comma && ++commas == 30) { - --persist; - break; + char *trimmed_end = member_end; + while (trimmed_end > trimmed_member && (trimmed_end[-1] == ' ' || trimmed_end[-1] == '\t')) { + --trimmed_end; } + size_t trimmed_len = trimmed_end - trimmed_member; + bool is_datadog_member = ddtrace_tracestate_member_is(trimmed_member, trimmed_len, "dd") || + (trimmed_len >= 3 && trimmed_member[0] == 'd' && trimmed_member[1] == 'd' && + (trimmed_member[2] == ' ' || trimmed_member[2] == '\t')); + + if (ddtrace_tracestate_member_is(trimmed_member, trimmed_len, "ot")) { + if (!found_otel) { + ddtrace_otel_sampling_parse(&result.otel_sampling, trimmed_member + 3, trimmed_len - 3); + found_otel = true; + } + } else if (is_datadog_member) { + // If there's a dd member, ignore x-datadog-tags fully. + ddtrace_parse_datadog_tracestate_member(&result, trimmed_member, trimmed_end, &tags_size, + &span_parent_key); + } else if (trimmed_len && vendor_member_count < DDTRACE_TRACESTATE_MAX_VENDOR_MEMBERS) { + if (vendor_member_count) { + *persist++ = ','; + } + size_t member_len = member_end - member; + memcpy(persist, member, member_len); + persist += member_len; + ++vendor_member_count; + } + + member = member_end == end ? end : member_end + 1; } *persist = 0; // and zero-terminate it ZSTR_LEN(result.tracestate) = persist - ZSTR_VAL(result.tracestate); @@ -550,6 +583,7 @@ ddtrace_distributed_tracing_result ddtrace_read_distributed_tracing_ids(ddtrace_ if (!result.tracestate && new_result.tracestate) { result.tracestate = new_result.tracestate; new_result.tracestate = NULL; + result.otel_sampling = new_result.otel_sampling; zend_hash_destroy(&result.tracestate_unknown_dd_keys); result.tracestate_unknown_dd_keys = new_result.tracestate_unknown_dd_keys; @@ -652,6 +686,7 @@ void ddtrace_apply_distributed_tracing_result(ddtrace_distributed_tracing_result result->trace_id = (datadog_trace_id){0}; result->parent_id = 0; result->priority_sampling = DDTRACE_PRIORITY_SAMPLING_UNKNOWN; + result->otel_sampling = (ddtrace_otel_sampling_state){0}; zval reason_str; ZVAL_STR(&reason_str, zend_string_init(ZEND_STRL("propagation_behavior_extract"), 0)); @@ -720,6 +755,7 @@ void ddtrace_apply_distributed_tracing_result(ddtrace_distributed_tracing_result ZVAL_STR(&zv, result->tracestate); datadog_assign_variable(&span->property_tracestate, &zv); } + span->otel_sampling = result->otel_sampling; ZVAL_ARR(&zv, emalloc(sizeof(HashTable))); *Z_ARR(zv) = result->tracestate_unknown_dd_keys; @@ -763,6 +799,7 @@ void ddtrace_apply_distributed_tracing_result(ddtrace_distributed_tracing_result zend_string_release(DDTRACE_G(tracestate)); } DDTRACE_G(tracestate) = result->tracestate; + DDTRACE_G(otel_sampling) = result->otel_sampling; zend_hash_destroy(&DDTRACE_G(baggage)); DDTRACE_G(baggage) = result->baggage; zend_string *key; diff --git a/tracer/distributed_tracing_headers.h b/tracer/distributed_tracing_headers.h index 263e74e914b..2ab38e712c4 100644 --- a/tracer/distributed_tracing_headers.h +++ b/tracer/distributed_tracing_headers.h @@ -2,6 +2,7 @@ #define DD_DISTRIBUTED_TRACING_HEADERS_H #include "ddtrace.h" +#include "otel_sampling.h" #include "priority_sampling/priority_sampling.h" #include @@ -10,6 +11,7 @@ typedef struct { uint64_t parent_id; zend_string *origin; zend_string *tracestate; + ddtrace_otel_sampling_state otel_sampling; HashTable baggage; HashTable tracestate_unknown_dd_keys; HashTable propagated_tags; diff --git a/tracer/functions.c b/tracer/functions.c index b0744e569b8..882bad3a337 100644 --- a/tracer/functions.c +++ b/tracer/functions.c @@ -244,7 +244,8 @@ void ddtrace_build_span_link_from_result(ddtrace_distributed_tracing_result *res zend_hash_copy(Z_ARR(link->property_attributes), &result->meta_tags, (copy_ctor_func_t)zval_add_ref); zend_string *propagated_tags = ddtrace_format_propagated_tags(&result->propagated_tags, &result->meta_tags); - zend_string *full_tracestate = ddtrace_format_tracestate(result->tracestate, 0, result->origin, result->priority_sampling, propagated_tags, &result->tracestate_unknown_dd_keys); + zend_string *full_tracestate = ddtrace_format_tracestate(result->tracestate, 0, result->origin, result->priority_sampling, propagated_tags, &result->tracestate_unknown_dd_keys, &result->otel_sampling); + full_tracestate = ddtrace_otel_sampling_limit_tracestate(full_tracestate); if (propagated_tags) { zend_string_release(propagated_tags); } @@ -282,6 +283,9 @@ ZEND_METHOD(DDTrace_SpanLink, fromHeaders) { if (result.tracestate) { zend_string_release(result.tracestate); } + if (result.context_headers) { + zend_string_release(result.context_headers); + } } /* DDTrace\SpanData */ @@ -589,8 +593,16 @@ static zval *ddtrace_root_span_data_write(zend_object *object, zend_string *memb root_span_data_changed = true; } cache_slot = NULL; + } else if (zend_string_equals_literal(prop_name, "tracestate") && Z_TYPE_P(value) == IS_STRING) { + ZVAL_STR(&zv, ddtrace_otel_sampling_extract_tracestate(Z_STR_P(value), &span->otel_sampling)); + Z_DELREF(zv); // zend_std_write_property will incref itself + value = &zv; + cache_slot = NULL; } else if (zend_string_equals_literal(prop_name, "samplingPriority")) { span->explicit_sampling_priority = zval_get_long(value) != DDTRACE_PRIORITY_SAMPLING_UNKNOWN; + if (span->explicit_sampling_priority) { + ddtrace_otel_sampling_decide_non_probability(&span->otel_sampling); + } #ifdef __linux__ sampling_priority_changed = true; #endif diff --git a/tracer/handlers_http.h b/tracer/handlers_http.h index e3d1db2c898..00381254033 100644 --- a/tracer/handlers_http.h +++ b/tracer/handlers_http.h @@ -6,13 +6,14 @@ #include "random.h" #include "tracer_tag_propagation/tracer_tag_propagation.h" #include "span.h" +#include "otel_sampling.h" #include "trace_context.h" #include #include ZEND_EXTERN_MODULE_GLOBALS(datadog); -static inline zend_string *ddtrace_format_tracestate(zend_string *tracestate, uint64_t span_id, zend_string *origin, zend_long sampling_priority, zend_string *propagated_tags, zend_array *tracestate_unknown_dd_keys) { +static inline zend_string *ddtrace_format_tracestate(zend_string *tracestate, uint64_t span_id, zend_string *origin, zend_long sampling_priority, zend_string *propagated_tags, zend_array *tracestate_unknown_dd_keys, const ddtrace_otel_sampling_state *otel_sampling) { smart_str str = {0}; if (span_id) { @@ -91,6 +92,7 @@ static inline zend_string *ddtrace_format_tracestate(zend_string *tracestate, ui } ZEND_HASH_FOREACH_END(); bool hasdd = str.s != NULL; + ddtrace_otel_sampling_append_to_tracestate(&str, otel_sampling); if (tracestate && ZSTR_LEN(tracestate) > 0) { if (str.s) { smart_str_appendc(&str, ','); @@ -405,7 +407,9 @@ static inline void ddtrace_inject_distributed_headers_config(zend_array *array, propagated_span_id = ddtrace_parse_hex_span_id(old_parent_id); } - zend_string *full_tracestate = ddtrace_format_tracestate(tracestate, propagated_span_id, origin, sampling_priority, propagated_tags, tracestate_unknown_dd_keys); + const ddtrace_otel_sampling_state *otel_sampling = root ? &root->otel_sampling : &DDTRACE_G(otel_sampling); + zend_string *full_tracestate = ddtrace_format_tracestate(tracestate, propagated_span_id, origin, sampling_priority, propagated_tags, tracestate_unknown_dd_keys, otel_sampling); + full_tracestate = ddtrace_otel_sampling_limit_tracestate(full_tracestate); if (full_tracestate) { ADD_HEADER("tracestate", "%.*s", (int)ZSTR_LEN(full_tracestate), ZSTR_VAL(full_tracestate)); zend_string_release(full_tracestate); diff --git a/tracer/otel_sampling.c b/tracer/otel_sampling.c new file mode 100644 index 00000000000..a8cc65514c6 --- /dev/null +++ b/tracer/otel_sampling.c @@ -0,0 +1,345 @@ +#include "otel_sampling.h" +#include "tracestate.h" + +#include + +#define DDTRACE_TRACESTATE_MAX_LEN 512 +#define DDTRACE_TRACESTATE_MAX_MEMBERS 32 +#define DDTRACE_OTEL_MAX_THRESHOLD (UINT64_C(1) << 56) +#define DDTRACE_OTEL_MAX_ENCODABLE_THRESHOLD (DDTRACE_OTEL_MAX_THRESHOLD - 1) + +static const uint64_t DDTRACE_OTEL_KNUTH_FACTOR = UINT64_C(1111111111111111111); + +typedef struct { + const char* datadog_member; + size_t datadog_member_len; + const char* otel_member; + size_t otel_member_len; + size_t member_count; +} ddtrace_otel_tracestate_members; + +static bool ddtrace_otel_parse_lower_hex(const char* value, size_t len, uint64_t* parsed) { + uint64_t result = 0; + for (size_t i = 0; i < len; ++i) { + result <<= 4; + if (value[i] >= '0' && value[i] <= '9') { + result |= (uint64_t)(value[i] - '0'); + } else if (value[i] >= 'a' && value[i] <= 'f') { + result |= (uint64_t)(value[i] - 'a' + 10); + } else { + return false; + } + } + *parsed = result; + return true; +} + +static bool ddtrace_otel_field_is(const char* field, size_t field_len, const char* key) { + return field_len >= 2 && field[0] == key[0] && field[1] == key[1] && (field_len == 2 || field[2] == ':'); +} + +static void ddtrace_otel_append_unknown_field(ddtrace_otel_sampling_state* state, const char* field, size_t field_len) { + size_t separator_len = state->unknown_fields_len ? 1 : 0; + if (state->unknown_fields_len + separator_len + field_len > DDTRACE_OTEL_MAX_VALUE_LEN) { + return; + } + if (separator_len) { + state->unknown_fields[state->unknown_fields_len++] = ';'; + } + memcpy(state->unknown_fields + state->unknown_fields_len, field, field_len); + state->unknown_fields_len += field_len; +} + +void ddtrace_otel_sampling_parse(ddtrace_otel_sampling_state* state, const char* value, size_t value_len) { + *state = (ddtrace_otel_sampling_state){0}; + const char* end = value + value_len; + + for (const char* field = value; field <= end;) { + const char* field_end = memchr(field, ';', end - field); + if (!field_end) { + field_end = end; + } + size_t field_len = field_end - field; + + if (ddtrace_otel_field_is(field, field_len, "rv")) { + state->random_value_len = 0; + if (field_len == 17 && ddtrace_otel_parse_lower_hex(field + 3, 14, &state->random_value)) { + state->random_value_len = 14; + } + } else if (ddtrace_otel_field_is(field, field_len, "th")) { + state->threshold_len = 0; + size_t threshold_len = field_len > 2 && field[2] == ':' ? field_len - 3 : 0; + if (threshold_len >= 1 && threshold_len <= 14 && ddtrace_otel_parse_lower_hex(field + 3, threshold_len, &state->threshold)) { + state->threshold_len = threshold_len; + } + } else if (field_len) { + ddtrace_otel_append_unknown_field(state, field, field_len); + } + + if (field_end == end) { + break; + } + field = field_end + 1; + } +} + +static void ddtrace_otel_append_field(smart_str* result, const char* field, size_t field_len) { + size_t separator_len = result->s ? 1 : 0; + if ((result->s ? ZSTR_LEN(result->s) : 0) + separator_len + field_len > DDTRACE_OTEL_MAX_VALUE_LEN) { + return; + } + if (separator_len) { + smart_str_appendc(result, ';'); + } + smart_str_appendl(result, field, field_len); +} + +static uint64_t ddtrace_otel_threshold_for(double sample_rate) { + if (sample_rate >= 1) { + return 0; + } + + double threshold = round((1 - sample_rate) * (double)DDTRACE_OTEL_MAX_THRESHOLD); + if (threshold >= (double)DDTRACE_OTEL_MAX_THRESHOLD) { + return DDTRACE_OTEL_MAX_ENCODABLE_THRESHOLD; + } + return (uint64_t)threshold; +} + +static uint64_t ddtrace_otel_derive_random_value(uint64_t trace_id) { + return ~(trace_id * DDTRACE_OTEL_KNUTH_FACTOR) >> 8; +} + +static uint64_t ddtrace_otel_reconcile_random_value(uint64_t random_value, uint64_t threshold, bool kept) { + if (kept && random_value < threshold) { + return threshold; + } + if (!kept && random_value >= threshold) { + return threshold > 0 ? threshold - 1 : 0; + } + return random_value; +} + +void ddtrace_otel_sampling_decide_probability(ddtrace_otel_sampling_state* state, uint64_t trace_id, zend_long sampling_priority, + double sample_rate) { + uint64_t threshold = ddtrace_otel_threshold_for(sample_rate); + state->random_value = ddtrace_otel_reconcile_random_value(ddtrace_otel_derive_random_value(trace_id), threshold, sampling_priority > 0); + state->random_value_len = 14; + state->threshold_len = 14; + while (state->threshold_len > 1 && (threshold & 0xf) == 0) { + threshold >>= 4; + --state->threshold_len; + } + state->threshold = threshold; +} + +void ddtrace_otel_sampling_decide_non_probability(ddtrace_otel_sampling_state* state) { + state->threshold_len = 0; +} + +static bool ddtrace_otel_is_member(const char* member, size_t member_len, const char** value, size_t* value_len) { + while (member_len && (*member == ' ' || *member == '\t')) { + ++member; + --member_len; + } + while (member_len && (member[member_len - 1] == ' ' || member[member_len - 1] == '\t')) { + --member_len; + } + + if (member_len < 3 || memcmp(member, "ot=", 3) != 0) { + return false; + } + + *value = member + 3; + *value_len = member_len - 3; + return true; +} + +zend_string* ddtrace_otel_sampling_extract_tracestate(zend_string* tracestate, ddtrace_otel_sampling_state* state) { + *state = (ddtrace_otel_sampling_state){0}; + smart_str vendors = {0}; + bool found_otel = false; + const char* raw = ZSTR_VAL(tracestate); + const char* end = raw + ZSTR_LEN(tracestate); + + for (const char* member = raw; member < end;) { + const char* member_end = memchr(member, ',', end - member); + if (!member_end) { + member_end = end; + } + size_t member_len = member_end - member; + const char* otel_value; + size_t otel_value_len; + + if (ddtrace_otel_is_member(member, member_len, &otel_value, &otel_value_len)) { + if (!found_otel) { + ddtrace_otel_sampling_parse(state, otel_value, otel_value_len); + found_otel = true; + } + } else if (member_len) { + if (vendors.s) { + smart_str_appendc(&vendors, ','); + } + smart_str_appendl(&vendors, member, member_len); + } + + member = member_end == end ? end : member_end + 1; + } + + if (vendors.s) { + smart_str_0(&vendors); + return vendors.s; + } + return zend_string_init("", 0, 0); +} + +void ddtrace_otel_sampling_append_to_tracestate(smart_str* tracestate, const ddtrace_otel_sampling_state* state) { + smart_str value = {0}; + if (state->random_value_len) { + smart_str_append_printf(&value, "rv:%0*" PRIx64, state->random_value_len, state->random_value); + } + if (state->threshold_len) { + if (value.s) { + smart_str_appendc(&value, ';'); + } + smart_str_append_printf(&value, "th:%0*" PRIx64, state->threshold_len, state->threshold); + } + + const char* unknown = state->unknown_fields; + const char* end = unknown + state->unknown_fields_len; + for (const char* field = unknown; field < end;) { + const char* field_end = memchr(field, ';', end - field); + if (!field_end) { + field_end = end; + } + ddtrace_otel_append_field(&value, field, field_end - field); + field = field_end == end ? end : field_end + 1; + } + + if (!value.s) { + return; + } + if (tracestate->s) { + smart_str_appendc(tracestate, ','); + } + smart_str_appends(tracestate, "ot="); + smart_str_append(tracestate, value.s); + smart_str_free(&value); +} + +static ddtrace_otel_tracestate_members ddtrace_otel_scan_tracestate(zend_string* tracestate) { + ddtrace_otel_tracestate_members members = {0}; + const char* raw = ZSTR_VAL(tracestate); + const char* end = raw + ZSTR_LEN(tracestate); + + for (const char* member = raw; member < end;) { + const char* member_end = memchr(member, ',', end - member); + if (!member_end) { + member_end = end; + } + size_t member_len = member_end - member; + ++members.member_count; + + if (!members.datadog_member && ddtrace_tracestate_member_is(member, member_len, "dd")) { + members.datadog_member = member; + members.datadog_member_len = member_len; + } else if (!members.otel_member && ddtrace_tracestate_member_is(member, member_len, "ot")) { + members.otel_member = member; + members.otel_member_len = member_len; + } + + member = member_end == end ? end : member_end + 1; + } + + return members; +} + +static bool ddtrace_otel_append_member(smart_str* result, const char* member, size_t member_len, size_t* member_count) { + size_t separator_len = result->s ? 1 : 0; + if (*member_count >= DDTRACE_TRACESTATE_MAX_MEMBERS || + (result->s ? ZSTR_LEN(result->s) : 0) + separator_len + member_len > DDTRACE_TRACESTATE_MAX_LEN) { + return false; + } + if (separator_len) { + smart_str_appendc(result, ','); + } + smart_str_appendl(result, member, member_len); + ++*member_count; + return true; +} + +static size_t ddtrace_otel_datadog_member_prefix_len(const ddtrace_otel_tracestate_members* members) { + size_t datadog_member_len = members->datadog_member_len; + if (!members->otel_member) { + return datadog_member_len; + } + + size_t reserved_otel_len = members->otel_member_len + 1; + if (reserved_otel_len >= DDTRACE_TRACESTATE_MAX_LEN) { + return 0; + } + + size_t max_datadog_len = DDTRACE_TRACESTATE_MAX_LEN - reserved_otel_len; + if (datadog_member_len <= max_datadog_len) { + return datadog_member_len; + } + + while (max_datadog_len > 0 && members->datadog_member[max_datadog_len] != ';') { + --max_datadog_len; + } + return max_datadog_len; +} + +// Takes ownership of a tracestate which is known to exceed a W3C limit. +static zend_string* ddtrace_otel_limit_oversized_tracestate(zend_string* tracestate) { + if (!tracestate) { + return NULL; + } + + const char* raw = ZSTR_VAL(tracestate); + const char* end = raw + ZSTR_LEN(tracestate); + ddtrace_otel_tracestate_members members = ddtrace_otel_scan_tracestate(tracestate); + + smart_str limited = {0}; + size_t member_count = 0; + if (members.datadog_member) { + size_t datadog_member_len = ddtrace_otel_datadog_member_prefix_len(&members); + if (datadog_member_len) { + ddtrace_otel_append_member(&limited, members.datadog_member, datadog_member_len, &member_count); + } + } + if (members.otel_member) { + ddtrace_otel_append_member(&limited, members.otel_member, members.otel_member_len, &member_count); + } + + for (const char* member = raw; member < end;) { + const char* member_end = memchr(member, ',', end - member); + if (!member_end) { + member_end = end; + } + size_t member_len = member_end - member; + if (member != members.datadog_member && member != members.otel_member && member_len && + !ddtrace_otel_append_member(&limited, member, member_len, &member_count)) { + break; + } + member = member_end == end ? end : member_end + 1; + } + + zend_string_release(tracestate); + if (limited.s) { + smart_str_0(&limited); + } + return limited.s; +} + +zend_string* ddtrace_otel_sampling_limit_tracestate(zend_string* tracestate) { + if (!tracestate) { + return NULL; + } + + ddtrace_otel_tracestate_members members = ddtrace_otel_scan_tracestate(tracestate); + if (members.member_count <= DDTRACE_TRACESTATE_MAX_MEMBERS && ZSTR_LEN(tracestate) <= DDTRACE_TRACESTATE_MAX_LEN) { + return tracestate; + } + return ddtrace_otel_limit_oversized_tracestate(tracestate); +} diff --git a/tracer/otel_sampling.h b/tracer/otel_sampling.h new file mode 100644 index 00000000000..795a8990422 --- /dev/null +++ b/tracer/otel_sampling.h @@ -0,0 +1,27 @@ +#ifndef DD_OTEL_SAMPLING_H +#define DD_OTEL_SAMPLING_H + +#include +#include + +#define DDTRACE_OTEL_MAX_VALUE_LEN 256 + +typedef struct { + uint64_t random_value; + uint64_t threshold; + size_t unknown_fields_len; + uint8_t random_value_len; + uint8_t threshold_len; + char unknown_fields[DDTRACE_OTEL_MAX_VALUE_LEN]; +} ddtrace_otel_sampling_state; + +void ddtrace_otel_sampling_parse(ddtrace_otel_sampling_state *state, const char *value, size_t value_len); +zend_string *ddtrace_otel_sampling_extract_tracestate(zend_string *tracestate, ddtrace_otel_sampling_state *state); +void ddtrace_otel_sampling_decide_probability(ddtrace_otel_sampling_state *state, uint64_t trace_id, zend_long sampling_priority, double sample_rate); +void ddtrace_otel_sampling_decide_non_probability(ddtrace_otel_sampling_state *state); +void ddtrace_otel_sampling_append_to_tracestate(smart_str *tracestate, const ddtrace_otel_sampling_state *state); + +// Takes ownership of tracestate and applies the W3C byte and member limits. +zend_string* ddtrace_otel_sampling_limit_tracestate(zend_string* tracestate); + +#endif // DD_OTEL_SAMPLING_H diff --git a/tracer/priority_sampling/priority_sampling.c b/tracer/priority_sampling/priority_sampling.c index 8ee4b9992a2..b10c071d060 100644 --- a/tracer/priority_sampling/priority_sampling.c +++ b/tracer/priority_sampling/priority_sampling.c @@ -389,6 +389,12 @@ static void dd_decide_on_sampling(ddtrace_root_span_data *span) { &limit_zv); } + if (mechanism == DD_MECHANISM_MANUAL || mechanism == DD_MECHANISM_ASM || limited || sample_rate <= 0) { + ddtrace_otel_sampling_decide_non_probability(&span->otel_sampling); + } else { + ddtrace_otel_sampling_decide_probability(&span->otel_sampling, span->trace_id.low, priority, sample_rate); + } + zval priority_zv; ZVAL_LONG(&priority_zv, priority); datadog_assign_variable(&span->property_sampling_priority, &priority_zv); @@ -451,6 +457,9 @@ void ddtrace_set_priority_sampling_on_span(ddtrace_root_span_data *root_span, ze dd_update_decision_maker_tag(root_span, mechanism); // Default is never explicit - e.g. distributed tracing. root_span->explicit_sampling_priority = mechanism != DD_MECHANISM_DEFAULT; + if (mechanism != DD_MECHANISM_DEFAULT) { + ddtrace_otel_sampling_decide_non_probability(&root_span->otel_sampling); + } } } diff --git a/tracer/serializer.c b/tracer/serializer.c index e1fc3e71721..7f4a97fdbf8 100644 --- a/tracer/serializer.c +++ b/tracer/serializer.c @@ -850,6 +850,7 @@ void ddtrace_set_root_span_properties(ddtrace_root_span_data *span) { if (parent_root) { ddtrace_inherit_span_properties(&span->span, &parent_root->span); ZVAL_COPY_DEREF(&span->property_origin, &parent_root->property_origin); + span->otel_sampling = parent_root->otel_sampling; } else { zval *prop_type = &span->property_type; zval *prop_name = &span->property_name; @@ -888,6 +889,7 @@ void ddtrace_set_root_span_properties(ddtrace_root_span_data *span) { if (DDTRACE_G(tracestate)) { ZVAL_STR_COPY(&span->property_tracestate, DDTRACE_G(tracestate)); } + span->otel_sampling = DDTRACE_G(otel_sampling); SEPARATE_ARRAY(&span->property_propagated_tags); zend_hash_copy(Z_ARR(span->property_propagated_tags), &DDTRACE_G(propagated_root_span_tags), zval_add_ref); @@ -901,6 +903,9 @@ void ddtrace_set_root_span_properties(ddtrace_root_span_data *span) { } if (DDTRACE_G(default_priority_sampling) != DDTRACE_PRIORITY_SAMPLING_UNKNOWN) { ddtrace_set_priority_sampling_on_span(span, DDTRACE_G(default_priority_sampling), DD_MECHANISM_MANUAL); + if (DDTRACE_G(propagated_priority_sampling) != DDTRACE_PRIORITY_SAMPLING_UNSET) { + span->otel_sampling = DDTRACE_G(otel_sampling); + } } if (DATADOG_G(asm_event_emitted)) { @@ -2212,4 +2217,3 @@ void ddtrace_serializer_startup() { ddtrace_user_req_add_listeners(&ser_user_req_listeners); } - diff --git a/tracer/span.c b/tracer/span.c index ff4653eea53..d0c3f5363dc 100644 --- a/tracer/span.c +++ b/tracer/span.c @@ -833,6 +833,7 @@ static void dd_mark_closed_spans_flushable(ddtrace_span_stack *stack) { ZVAL_LONG(&priority, PRIORITY_SAMPLING_AUTO_REJECT); datadog_assign_variable(&root_span->property_sampling_priority, &priority); root_span->explicit_sampling_priority = true; + ddtrace_otel_sampling_decide_non_probability(&root_span->otel_sampling); #ifdef __linux__ ddtrace_otel_update_trace_flags(root_span); #endif diff --git a/tracer/span.h b/tracer/span.h index 2cc2e5460a8..5229f25d255 100644 --- a/tracer/span.h +++ b/tracer/span.h @@ -8,6 +8,7 @@ #include #include "ddtrace.h" +#include "otel_sampling.h" #include #include "priority_sampling/priority_sampling.h" #include "inferred_proxy_headers.h" @@ -126,6 +127,7 @@ struct ddtrace_root_span_data { uint64_t parent_id; uint8_t trace_flags; ddtrace_rule_result sampling_rule; + ddtrace_otel_sampling_state otel_sampling; #ifdef __linux__ datadog_otel_thr_ctx_rec otel_context; // Incremented when this root is the entrypoint whose inherited identity changed. diff --git a/tracer/tracestate.h b/tracer/tracestate.h new file mode 100644 index 00000000000..a99ca335397 --- /dev/null +++ b/tracer/tracestate.h @@ -0,0 +1,15 @@ +#ifndef DDTRACE_TRACESTATE_H +#define DDTRACE_TRACESTATE_H + +#include +#include + +static inline bool ddtrace_tracestate_member_is(const char *member, size_t member_len, const char key[2]) { + while (member_len && (*member == ' ' || *member == '\t')) { + ++member; + --member_len; + } + return member_len >= 3 && member[0] == key[0] && member[1] == key[1] && member[2] == '='; +} + +#endif // DDTRACE_TRACESTATE_H