From f7329a3d7e2fcfe18567da300169560adc9e3aa3 Mon Sep 17 00:00:00 2001 From: Victor Pellan Date: Wed, 9 Sep 2026 16:20:21 +0200 Subject: [PATCH 01/13] feat(otel): otel sampling support in distributed tracing --- config.m4 | 1 + config.w32 | 1 + ...tributed_trace_span_link_from_headers.phpt | 2 +- .../otel_sampling_caps.phpt | 52 ++ .../otel_sampling_inherited.phpt | 54 ++ .../otel_sampling_limiter.phpt | 30 + .../otel_sampling_precision.phpt | 21 + .../otel_sampling_precision_drop.phpt | 21 + .../otel_sampling_probability.phpt | 28 + ..._sampling_propagation_behavior_ignore.phpt | 34 ++ ...sampling_propagation_behavior_restart.phpt | 34 ++ tracer/distributed_tracing_headers.c | 10 +- tracer/functions.c | 3 + tracer/handlers_http.h | 9 +- tracer/otel_sampling.c | 528 ++++++++++++++++++ tracer/otel_sampling.h | 11 + tracer/priority_sampling/priority_sampling.c | 8 + tracer/priority_sampling/priority_sampling.h | 6 + tracer/serializer.c | 5 + tracer/span.c | 1 + tracer/span.h | 2 + 21 files changed, 857 insertions(+), 4 deletions(-) create mode 100644 tests/ext/distributed_tracing/otel_sampling_caps.phpt create mode 100644 tests/ext/distributed_tracing/otel_sampling_inherited.phpt create mode 100644 tests/ext/distributed_tracing/otel_sampling_limiter.phpt create mode 100644 tests/ext/distributed_tracing/otel_sampling_precision.phpt create mode 100644 tests/ext/distributed_tracing/otel_sampling_precision_drop.phpt create mode 100644 tests/ext/distributed_tracing/otel_sampling_probability.phpt create mode 100644 tests/ext/distributed_tracing/otel_sampling_propagation_behavior_ignore.phpt create mode 100644 tests/ext/distributed_tracing/otel_sampling_propagation_behavior_restart.phpt create mode 100644 tracer/otel_sampling.c create mode 100644 tracer/otel_sampling.h 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/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/otel_sampling_caps.phpt b/tests/ext/distributed_tracing/otel_sampling_caps.phpt new file mode 100644 index 00000000000..c921d48d326 --- /dev/null +++ b/tests/ext/distributed_tracing/otel_sampling_caps.phpt @@ -0,0 +1,52 @@ +--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; + +$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 +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..8aaa5d6af77 --- /dev/null +++ b/tests/ext/distributed_tracing/otel_sampling_inherited.phpt @@ -0,0 +1,54 @@ +--TEST-- +Inherited OpenTelemetry tracestate sampling fields are validated and forwarded +--ENV-- +DD_TRACE_SAMPLE_RATE=0.5 +DD_TRACE_RATE_LIMIT=10000000 +--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 +{ + return preg_match('/(?:^|,)ot=([^,]+)/', $tracestate, $matches) ? $matches[1] : ''; +} + +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; + +?> +--EXPECT-- +rv:ef284ace7a91e1;th:e6666666666668;foo:bar +th:e6666666666668 +rv:ef284ace7a91e1;th:e6666666666668 + +rv:1234567890abcd + +foo:bar +rv:65cd67504a538e + +foo=bar,ot=rv:6e6d1a75832a2f,something=else 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/tracer/distributed_tracing_headers.c b/tracer/distributed_tracing_headers.c index 732d64014e9..a4cee7fd170 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" @@ -464,14 +465,19 @@ static ddtrace_distributed_tracing_result ddtrace_read_distributed_tracing_ids_t } last_comma = *ptr == ','; - // preserve only up to 31 vendor specific values, excluding our own - if (last_comma && ++commas == 30) { + // W3C Trace Context permits at most 32 list-members. Owned members + // are removed or normalized after this pass, then protected when + // the outbound header is rebuilt. + if (last_comma && ++commas == 32) { --persist; break; } } *persist = 0; // and zero-terminate it ZSTR_LEN(result.tracestate) = persist - ZSTR_VAL(result.tracestate); + zend_string *normalized_tracestate = ddtrace_otel_sampling_update_tracestate( + result.tracestate, trace_id.low, result.priority_sampling, DDTRACE_OTEL_SAMPLING_DECISION_INHERITED, 0); + result.tracestate = normalized_tracestate ?: zend_string_init("", 0, 0); zend_string_release(tracestate); } diff --git a/tracer/functions.c b/tracer/functions.c index b0744e569b8..33353697469 100644 --- a/tracer/functions.c +++ b/tracer/functions.c @@ -591,6 +591,9 @@ static zval *ddtrace_root_span_data_write(zend_object *object, zend_string *memb cache_slot = NULL; } else if (zend_string_equals_literal(prop_name, "samplingPriority")) { span->explicit_sampling_priority = zval_get_long(value) != DDTRACE_PRIORITY_SAMPLING_UNKNOWN; + span->otel_sampling_decision = span->explicit_sampling_priority + ? DDTRACE_OTEL_SAMPLING_DECISION_NON_PROBABILITY + : DDTRACE_OTEL_SAMPLING_DECISION_INHERITED; #ifdef __linux__ sampling_priority_changed = true; #endif diff --git a/tracer/handlers_http.h b/tracer/handlers_http.h index e3d1db2c898..c3b4645b4c9 100644 --- a/tracer/handlers_http.h +++ b/tracer/handlers_http.h @@ -6,6 +6,7 @@ #include "random.h" #include "tracer_tag_propagation/tracer_tag_propagation.h" #include "span.h" +#include "otel_sampling.h" #include "trace_context.h" #include #include @@ -405,7 +406,13 @@ 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); + zend_string *base_tracestate = ddtrace_format_tracestate(tracestate, propagated_span_id, origin, sampling_priority, propagated_tags, tracestate_unknown_dd_keys); + zend_string *full_tracestate = ddtrace_otel_sampling_update_tracestate( + base_tracestate, + trace_id.low, + sampling_priority, + root ? root->otel_sampling_decision : DDTRACE_OTEL_SAMPLING_DECISION_INHERITED, + root ? root->otel_sampling_rate : 0); 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..817aba3c064 --- /dev/null +++ b/tracer/otel_sampling.c @@ -0,0 +1,528 @@ +#include "otel_sampling.h" + +#include +#include + +#define DDTRACE_OTEL_MAX_VALUE_LEN 256 +#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* random_value; + size_t random_value_len; + const char* threshold; + size_t threshold_len; +} ddtrace_otel_fields; + +typedef struct { + const char* datadog_member; + size_t datadog_member_len; + const char* otel_member; + size_t otel_member_len; + const char* otel_value; + size_t otel_value_len; + size_t member_count; +} ddtrace_otel_tracestate_members; + +typedef struct { + size_t offset; + bool comma_before; + bool comma_after; +} ddtrace_otel_insertion; + +static bool ddtrace_otel_is_lower_hex(const char* value, size_t len) { + for (size_t i = 0; i < len; ++i) { + if (!((value[i] >= '0' && value[i] <= '9') || + (value[i] >= 'a' && value[i] <= 'f'))) { + return false; + } + } + 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 ddtrace_otel_fields ddtrace_otel_parse_fields(const char* value, + size_t value_len) { + ddtrace_otel_fields fields = {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")) { + fields.random_value = field_len > 2 && field[2] == ':' ? field + 3 : NULL; + fields.random_value_len = fields.random_value ? field_len - 3 : 0; + } else if (ddtrace_otel_field_is(field, field_len, "th")) { + fields.threshold = field_len > 2 && field[2] == ':' ? field + 3 : NULL; + fields.threshold_len = fields.threshold ? field_len - 3 : 0; + } + + if (field_end == end) { + break; + } + field = field_end + 1; + } + + if (fields.random_value_len != 14 || + !ddtrace_otel_is_lower_hex(fields.random_value, + fields.random_value_len)) { + fields.random_value = NULL; + fields.random_value_len = 0; + } + if (fields.threshold_len < 1 || fields.threshold_len > 14 || + !ddtrace_otel_is_lower_hex(fields.threshold, fields.threshold_len)) { + fields.threshold = NULL; + fields.threshold_len = 0; + } + + return fields; +} + +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 <= 0) { + return 0; + } + if (threshold >= (double)DDTRACE_OTEL_MAX_THRESHOLD) { + return DDTRACE_OTEL_MAX_ENCODABLE_THRESHOLD; + } + return (uint64_t)threshold; +} + +static void ddtrace_otel_encode_56_bit_hex(uint64_t value, char output[14]) { + static const char hex_digits[] = "0123456789abcdef"; + for (size_t i = 14; i > 0; --i) { + output[i - 1] = hex_digits[value & 0xf]; + value >>= 4; + } +} + +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; +} + +static void ddtrace_otel_generate_fields(ddtrace_otel_fields* fields, + char random_value[14], + char threshold[14], uint64_t trace_id, + zend_long sampling_priority, + double sample_rate) { + uint64_t threshold_value = ddtrace_otel_threshold_for(sample_rate); + uint64_t random_value_int = ddtrace_otel_reconcile_random_value( + ddtrace_otel_derive_random_value(trace_id), threshold_value, + sampling_priority > 0); + + ddtrace_otel_encode_56_bit_hex(random_value_int, random_value); + ddtrace_otel_encode_56_bit_hex(threshold_value, threshold); + + size_t threshold_len = 14; + while (threshold_len > 1 && threshold[threshold_len - 1] == '0') { + --threshold_len; + } + fields->random_value = random_value; + fields->random_value_len = 14; + fields->threshold = threshold; + fields->threshold_len = threshold_len; +} + +static zend_string* ddtrace_otel_rebuild_value( + const char* otel_value, size_t otel_value_len, uint64_t trace_id, + zend_long sampling_priority, enum ddtrace_otel_sampling_decision decision, + double sample_rate) { + ddtrace_otel_fields fields = + ddtrace_otel_parse_fields(otel_value, otel_value_len); + char generated_random_value[14]; + char generated_threshold[14]; + + if (decision == DDTRACE_OTEL_SAMPLING_DECISION_NON_PROBABILITY) { + fields.threshold = NULL; + fields.threshold_len = 0; + } else if (decision == DDTRACE_OTEL_SAMPLING_DECISION_PROBABILITY && + !fields.random_value && !fields.threshold && sample_rate > 0) { + ddtrace_otel_generate_fields(&fields, generated_random_value, + generated_threshold, trace_id, + sampling_priority, sample_rate); + } + + smart_str result = {0}; + if (fields.random_value) { + smart_str_appends(&result, "rv:"); + smart_str_appendl(&result, fields.random_value, fields.random_value_len); + } + if (fields.threshold) { + if (result.s) { + smart_str_appendc(&result, ';'); + } + smart_str_appends(&result, "th:"); + smart_str_appendl(&result, fields.threshold, fields.threshold_len); + } + + const char* end = otel_value + otel_value_len; + for (const char* field = otel_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 (field_len && !ddtrace_otel_field_is(field, field_len, "rv") && + !ddtrace_otel_field_is(field, field_len, "th")) { + ddtrace_otel_append_field(&result, field, field_len); + } + + if (field_end == end) { + break; + } + field = field_end + 1; + } + + if (result.s) { + smart_str_0(&result); + } + return result.s; +} + +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; +} + +static bool ddtrace_tracestate_member_is(const char* member, size_t member_len, + const char* key) { + while (member_len && (*member == ' ' || *member == '\t')) { + ++member; + --member_len; + } + return member_len >= 3 && member[0] == key[0] && member[1] == key[1] && + member[2] == '='; +} + +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_otel_is_member(member, member_len, &members.otel_value, + &members.otel_value_len)) { + 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 void ddtrace_otel_append_value_member(smart_str* result, + const char key[2], + zend_string* value) { + if (result->s) { + smart_str_appendc(result, ','); + } + smart_str_appendl(result, key, 2); + smart_str_appendc(result, '='); + smart_str_append(result, value); +} + +// 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) { + ddtrace_otel_append_member(&limited, members.datadog_member, + members.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; +} + +static char* ddtrace_otel_write_generated_member( + char* output, const ddtrace_otel_fields* fields) { + memcpy(output, "ot=rv:", 6); + output += 6; + memcpy(output, fields->random_value, fields->random_value_len); + output += fields->random_value_len; + memcpy(output, ";th:", 4); + output += 4; + memcpy(output, fields->threshold, fields->threshold_len); + return output + fields->threshold_len; +} + +static ddtrace_otel_insertion ddtrace_otel_generated_member_insertion( + zend_string* tracestate) { + ddtrace_otel_insertion insertion = {0}; + size_t raw_len = tracestate ? ZSTR_LEN(tracestate) : 0; + if (!raw_len) { + return insertion; + } + + const char* first_comma = memchr(ZSTR_VAL(tracestate), ',', raw_len); + size_t first_member_len = + first_comma ? (size_t)(first_comma - ZSTR_VAL(tracestate)) : raw_len; + if (!ddtrace_tracestate_member_is(ZSTR_VAL(tracestate), first_member_len, + "dd")) { + insertion.comma_after = true; + } else if (first_comma) { + insertion.offset = first_member_len + 1; + insertion.comma_after = true; + } else { + insertion.offset = raw_len; + insertion.comma_before = true; + } + + return insertion; +} + +// Takes ownership of tracestate and inserts the generated member in-place. +static zend_string* ddtrace_otel_insert_generated_member( + zend_string* tracestate, uint64_t trace_id, zend_long sampling_priority, + double sample_rate, size_t member_count) { + ddtrace_otel_fields fields = {0}; + char random_value[14]; + char threshold[14]; + ddtrace_otel_generate_fields(&fields, random_value, threshold, trace_id, + sampling_priority, sample_rate); + + size_t raw_len = tracestate ? ZSTR_LEN(tracestate) : 0; + ddtrace_otel_insertion insertion = + ddtrace_otel_generated_member_insertion(tracestate); + size_t otel_member_len = + 3 + 3 + fields.random_value_len + 1 + 3 + fields.threshold_len; + size_t insertion_len = + otel_member_len + insertion.comma_before + insertion.comma_after; + size_t result_len = raw_len + insertion_len; + zend_string* result = tracestate + ? zend_string_extend(tracestate, result_len, 0) + : zend_string_alloc(result_len, 0); + char* raw = ZSTR_VAL(result); + memmove(raw + insertion.offset + insertion_len, raw + insertion.offset, + raw_len - insertion.offset); + + char* output = raw + insertion.offset; + if (insertion.comma_before) { + *output++ = ','; + } + output = ddtrace_otel_write_generated_member(output, &fields); + if (insertion.comma_after) { + *output++ = ','; + } + ZEND_ASSERT(output == raw + insertion.offset + insertion_len); + ZSTR_VAL(result)[result_len] = '\0'; + + if (member_count + 1 <= DDTRACE_TRACESTATE_MAX_MEMBERS && + ZSTR_LEN(result) <= DDTRACE_TRACESTATE_MAX_LEN) { + return result; + } + return ddtrace_otel_limit_oversized_tracestate(result); +} + +// Takes ownership of tracestate and rewrites the first ot member. Any duplicate +// ot members are dropped while the remaining member order is preserved. +static zend_string* ddtrace_otel_rewrite_existing_member( + zend_string* tracestate, const ddtrace_otel_tracestate_members* members, + uint64_t trace_id, zend_long sampling_priority, + enum ddtrace_otel_sampling_decision decision, double sample_rate) { + zend_string* updated_value = ddtrace_otel_rebuild_value( + members->otel_value, members->otel_value_len, trace_id, sampling_priority, + decision, sample_rate); + smart_str result = {0}; + size_t result_member_count = 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) { + if (updated_value) { + ddtrace_otel_append_value_member(&result, "ot", updated_value); + ++result_member_count; + } + found_otel = true; + } + } else if (member_len) { + if (result.s) { + smart_str_appendc(&result, ','); + } + smart_str_appendl(&result, member, member_len); + ++result_member_count; + } + + member = member_end == end ? end : member_end + 1; + } + + if (updated_value) { + zend_string_release(updated_value); + } + zend_string_release(tracestate); + if (result.s) { + smart_str_0(&result); + } + if (result_member_count <= DDTRACE_TRACESTATE_MAX_MEMBERS && + (!result.s || ZSTR_LEN(result.s) <= DDTRACE_TRACESTATE_MAX_LEN)) { + return result.s; + } + return ddtrace_otel_limit_oversized_tracestate(result.s); +} + +zend_string* ddtrace_otel_sampling_update_tracestate( + zend_string* tracestate, uint64_t trace_id, zend_long sampling_priority, + enum ddtrace_otel_sampling_decision decision, double sample_rate) { + bool should_generate = + decision == DDTRACE_OTEL_SAMPLING_DECISION_PROBABILITY && sample_rate > 0; + if (!tracestate || ZSTR_LEN(tracestate) == 0) { + return should_generate + ? ddtrace_otel_insert_generated_member( + tracestate, trace_id, sampling_priority, sample_rate, 0) + : tracestate; + } + + ddtrace_otel_tracestate_members members = + ddtrace_otel_scan_tracestate(tracestate); + if (!members.otel_member) { + if (should_generate) { + return ddtrace_otel_insert_generated_member( + tracestate, trace_id, sampling_priority, sample_rate, + members.member_count); + } + if (members.member_count <= DDTRACE_TRACESTATE_MAX_MEMBERS && + ZSTR_LEN(tracestate) <= DDTRACE_TRACESTATE_MAX_LEN) { + return tracestate; + } + return ddtrace_otel_limit_oversized_tracestate(tracestate); + } + + return ddtrace_otel_rewrite_existing_member( + tracestate, &members, trace_id, sampling_priority, decision, sample_rate); +} diff --git a/tracer/otel_sampling.h b/tracer/otel_sampling.h new file mode 100644 index 00000000000..ba82e51ff02 --- /dev/null +++ b/tracer/otel_sampling.h @@ -0,0 +1,11 @@ +#ifndef DD_OTEL_SAMPLING_H +#define DD_OTEL_SAMPLING_H + +#include "ddtrace.h" +#include "priority_sampling/priority_sampling.h" + +// Takes ownership of tracestate and returns the normalized replacement. +zend_string* ddtrace_otel_sampling_update_tracestate(zend_string* tracestate, uint64_t trace_id, zend_long sampling_priority, + enum ddtrace_otel_sampling_decision decision, double sample_rate); + +#endif // DD_OTEL_SAMPLING_H diff --git a/tracer/priority_sampling/priority_sampling.c b/tracer/priority_sampling/priority_sampling.c index 8ee4b9992a2..fdd4388c146 100644 --- a/tracer/priority_sampling/priority_sampling.c +++ b/tracer/priority_sampling/priority_sampling.c @@ -389,6 +389,11 @@ static void dd_decide_on_sampling(ddtrace_root_span_data *span) { &limit_zv); } + span->otel_sampling_rate = sample_rate; + span->otel_sampling_decision = mechanism == DD_MECHANISM_MANUAL || mechanism == DD_MECHANISM_ASM || limited || sample_rate <= 0 + ? DDTRACE_OTEL_SAMPLING_DECISION_NON_PROBABILITY + : DDTRACE_OTEL_SAMPLING_DECISION_PROBABILITY; + zval priority_zv; ZVAL_LONG(&priority_zv, priority); datadog_assign_variable(&span->property_sampling_priority, &priority_zv); @@ -451,6 +456,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; + root_span->otel_sampling_decision = mechanism == DD_MECHANISM_DEFAULT + ? DDTRACE_OTEL_SAMPLING_DECISION_INHERITED + : DDTRACE_OTEL_SAMPLING_DECISION_NON_PROBABILITY; } } diff --git a/tracer/priority_sampling/priority_sampling.h b/tracer/priority_sampling/priority_sampling.h index 747a5f97273..9f58f38dcd3 100644 --- a/tracer/priority_sampling/priority_sampling.h +++ b/tracer/priority_sampling/priority_sampling.h @@ -23,6 +23,12 @@ enum dd_sampling_mechanism { DD_MECHANISM_REMOTE_DYNAMIC_RULE = 12, }; +enum ddtrace_otel_sampling_decision { + DDTRACE_OTEL_SAMPLING_DECISION_INHERITED, + DDTRACE_OTEL_SAMPLING_DECISION_PROBABILITY, + DDTRACE_OTEL_SAMPLING_DECISION_NON_PROBABILITY, +}; + void ddtrace_set_priority_sampling_on_root(zend_long priority, enum dd_sampling_mechanism mechanism); void ddtrace_set_priority_sampling_on_span(ddtrace_root_span_data *root_span, zend_long priority, enum dd_sampling_mechanism mechanism); DATADOG_PUBLIC void ddtrace_set_priority_sampling_on_span_zobj(zend_object *root_span, zend_long priority, enum dd_sampling_mechanism mechanism); diff --git a/tracer/serializer.c b/tracer/serializer.c index e1fc3e71721..7723bbf909b 100644 --- a/tracer/serializer.c +++ b/tracer/serializer.c @@ -850,6 +850,8 @@ 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_rate = parent_root->otel_sampling_rate; + span->otel_sampling_decision = parent_root->otel_sampling_decision; } else { zval *prop_type = &span->property_type; zval *prop_name = &span->property_name; @@ -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_decision = DDTRACE_OTEL_SAMPLING_DECISION_INHERITED; + } } if (DATADOG_G(asm_event_emitted)) { diff --git a/tracer/span.c b/tracer/span.c index ff4653eea53..e4235e3c8be 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; + root_span->otel_sampling_decision = DDTRACE_OTEL_SAMPLING_DECISION_NON_PROBABILITY; #ifdef __linux__ ddtrace_otel_update_trace_flags(root_span); #endif diff --git a/tracer/span.h b/tracer/span.h index 2cc2e5460a8..637c36f8983 100644 --- a/tracer/span.h +++ b/tracer/span.h @@ -126,6 +126,8 @@ struct ddtrace_root_span_data { uint64_t parent_id; uint8_t trace_flags; ddtrace_rule_result sampling_rule; + double otel_sampling_rate; + enum ddtrace_otel_sampling_decision otel_sampling_decision; #ifdef __linux__ datadog_otel_thr_ctx_rec otel_context; // Incremented when this root is the entrypoint whose inherited identity changed. From 316e7728b066f1caf2d21608c593dcf629704f43 Mon Sep 17 00:00:00 2001 From: Victor Pellan Date: Thu, 10 Sep 2026 13:40:44 +0200 Subject: [PATCH 02/13] Fix failing integration tests --- .../OpenTelemetry/Integration/API/TracerTest.php | 16 ++++++++-------- .../curl/distributed_tracing_curl.phpt | 2 +- .../distributed_tracing_curl_invalid_tags.phpt | 2 +- ...test.test_span_links_high_level_consumer.json | 2 +- ..._test.test_span_links_low_level_consumer.json | 2 +- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/tests/OpenTelemetry/Integration/API/TracerTest.php b/tests/OpenTelemetry/Integration/API/TracerTest.php index 025bc09539d..c6ab4e654c6 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,rojo=00f067aa0ba902b7,ot=rv:[0-9a-f]{14};th:0$/', (string)$child->getContext()->getTraceState()); $child->end(); $span->end(); 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", From 7d5421d727b0c60b8116bd84d44da0554584f2af Mon Sep 17 00:00:00 2001 From: Victor Pellan Date: Thu, 10 Sep 2026 13:53:42 +0200 Subject: [PATCH 03/13] fix(otel): rebuild sampling fields after local decisions --- .../otel_sampling_inherited.phpt | 22 +++++++++++++++++++ tracer/otel_sampling.c | 2 +- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/tests/ext/distributed_tracing/otel_sampling_inherited.phpt b/tests/ext/distributed_tracing/otel_sampling_inherited.phpt index 8aaa5d6af77..0550c5031a9 100644 --- a/tests/ext/distributed_tracing/otel_sampling_inherited.phpt +++ b/tests/ext/distributed_tracing/otel_sampling_inherited.phpt @@ -3,6 +3,7 @@ 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-- '; } +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; @@ -40,6 +55,10 @@ 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 @@ -52,3 +71,6 @@ foo:bar rv:65cd67504a538e foo=bar,ot=rv:6e6d1a75832a2f,something=else +rv:ef284ace7a91e1;th:e6666666666668;foo:bar +rv:ef284ace7a91e1;th:e6666666666668;foo:bar +rv:ef284ace7a91e1;th:e6666666666668;foo:bar diff --git a/tracer/otel_sampling.c b/tracer/otel_sampling.c index 817aba3c064..fce4f991be1 100644 --- a/tracer/otel_sampling.c +++ b/tracer/otel_sampling.c @@ -180,7 +180,7 @@ static zend_string* ddtrace_otel_rebuild_value( fields.threshold = NULL; fields.threshold_len = 0; } else if (decision == DDTRACE_OTEL_SAMPLING_DECISION_PROBABILITY && - !fields.random_value && !fields.threshold && sample_rate > 0) { + sample_rate > 0) { ddtrace_otel_generate_fields(&fields, generated_random_value, generated_threshold, trace_id, sampling_priority, sample_rate); From 6841cfb7e71bdd06900df59a5ca4ce75152b4ae0 Mon Sep 17 00:00:00 2001 From: Victor Pellan Date: Thu, 10 Sep 2026 13:57:03 +0200 Subject: [PATCH 04/13] fix(otel): preserve owned tracestate members under limits --- .../otel_sampling_caps.phpt | 13 ++++++++ tracer/otel_sampling.c | 32 +++++++++++++++++-- 2 files changed, 43 insertions(+), 2 deletions(-) diff --git a/tests/ext/distributed_tracing/otel_sampling_caps.phpt b/tests/ext/distributed_tracing/otel_sampling_caps.phpt index c921d48d326..1067c64dd3c 100644 --- a/tests/ext/distributed_tracing/otel_sampling_caps.phpt +++ b/tests/ext/distributed_tracing/otel_sampling_caps.phpt @@ -38,6 +38,18 @@ $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); @@ -49,4 +61,5 @@ echo 'ot-bytes=', strlen($matches[1]), --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/tracer/otel_sampling.c b/tracer/otel_sampling.c index fce4f991be1..a51acd8316d 100644 --- a/tracer/otel_sampling.c +++ b/tracer/otel_sampling.c @@ -302,6 +302,30 @@ static bool ddtrace_otel_append_member(smart_str* result, const char* member, 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; +} + static void ddtrace_otel_append_value_member(smart_str* result, const char key[2], zend_string* value) { @@ -328,8 +352,12 @@ static zend_string* ddtrace_otel_limit_oversized_tracestate( smart_str limited = {0}; size_t member_count = 0; if (members.datadog_member) { - ddtrace_otel_append_member(&limited, members.datadog_member, - members.datadog_member_len, &member_count); + 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, From cbed965e8b5632b1b4f6f88f9d55e107d8399404 Mon Sep 17 00:00:00 2001 From: Victor Pellan Date: Thu, 10 Sep 2026 14:01:32 +0200 Subject: [PATCH 05/13] fix(otel): enforce tracestate limits on span links --- ...ce_span_link_from_headers_long_origin.phpt | 2 +- .../otel_sampling_span_link_caps.phpt | 38 +++++++++++++++++++ tracer/functions.c | 4 ++ tracer/otel_sampling.c | 21 +++++++--- tracer/otel_sampling.h | 3 ++ 5 files changed, 62 insertions(+), 6 deletions(-) create mode 100644 tests/ext/distributed_tracing/otel_sampling_span_link_caps.phpt 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_span_link_caps.phpt b/tests/ext/distributed_tracing/otel_sampling_span_link_caps.phpt new file mode 100644 index 00000000000..42e2e879d26 --- /dev/null +++ b/tests/ext/distributed_tracing/otel_sampling_span_link_caps.phpt @@ -0,0 +1,38 @@ +--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; + +?> +--EXPECT-- +members=32 bytes=within-cap leading=dd,ot last=vendor29 diff --git a/tracer/functions.c b/tracer/functions.c index 33353697469..ea5a982aa3a 100644 --- a/tracer/functions.c +++ b/tracer/functions.c @@ -245,6 +245,7 @@ void ddtrace_build_span_link_from_result(ddtrace_distributed_tracing_result *res 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); + 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 */ diff --git a/tracer/otel_sampling.c b/tracer/otel_sampling.c index a51acd8316d..e39abcd4f2d 100644 --- a/tracer/otel_sampling.c +++ b/tracer/otel_sampling.c @@ -386,6 +386,21 @@ static zend_string* ddtrace_otel_limit_oversized_tracestate( 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); +} + static char* ddtrace_otel_write_generated_member( char* output, const ddtrace_otel_fields* fields) { memcpy(output, "ot=rv:", 6); @@ -544,11 +559,7 @@ zend_string* ddtrace_otel_sampling_update_tracestate( tracestate, trace_id, sampling_priority, sample_rate, members.member_count); } - if (members.member_count <= DDTRACE_TRACESTATE_MAX_MEMBERS && - ZSTR_LEN(tracestate) <= DDTRACE_TRACESTATE_MAX_LEN) { - return tracestate; - } - return ddtrace_otel_limit_oversized_tracestate(tracestate); + return ddtrace_otel_sampling_limit_tracestate(tracestate); } return ddtrace_otel_rewrite_existing_member( diff --git a/tracer/otel_sampling.h b/tracer/otel_sampling.h index ba82e51ff02..69ad5be0054 100644 --- a/tracer/otel_sampling.h +++ b/tracer/otel_sampling.h @@ -4,6 +4,9 @@ #include "ddtrace.h" #include "priority_sampling/priority_sampling.h" +// Takes ownership of tracestate and applies the W3C byte and member limits. +zend_string* ddtrace_otel_sampling_limit_tracestate(zend_string* tracestate); + // Takes ownership of tracestate and returns the normalized replacement. zend_string* ddtrace_otel_sampling_update_tracestate(zend_string* tracestate, uint64_t trace_id, zend_long sampling_priority, enum ddtrace_otel_sampling_decision decision, double sample_rate); From e97eb59ea7c1547deed20e70e0cb8d22798f0120 Mon Sep 17 00:00:00 2001 From: Victor Pellan Date: Thu, 10 Sep 2026 17:52:57 +0200 Subject: [PATCH 06/13] fix(otel): use portable ternary for tracestate --- tracer/distributed_tracing_headers.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tracer/distributed_tracing_headers.c b/tracer/distributed_tracing_headers.c index a4cee7fd170..d7cd0df6887 100644 --- a/tracer/distributed_tracing_headers.c +++ b/tracer/distributed_tracing_headers.c @@ -477,7 +477,7 @@ static ddtrace_distributed_tracing_result ddtrace_read_distributed_tracing_ids_t ZSTR_LEN(result.tracestate) = persist - ZSTR_VAL(result.tracestate); zend_string *normalized_tracestate = ddtrace_otel_sampling_update_tracestate( result.tracestate, trace_id.low, result.priority_sampling, DDTRACE_OTEL_SAMPLING_DECISION_INHERITED, 0); - result.tracestate = normalized_tracestate ?: zend_string_init("", 0, 0); + result.tracestate = normalized_tracestate ? normalized_tracestate : zend_string_init("", 0, 0); zend_string_release(tracestate); } From 81dc5fc7451c851d5a062ceab39765799d653a66 Mon Sep 17 00:00:00 2001 From: Victor Pellan Date: Thu, 10 Sep 2026 17:53:19 +0200 Subject: [PATCH 07/13] fix(tests): avoid PCRE JIT in inherited sampling test --- .../ext/distributed_tracing/otel_sampling_inherited.phpt | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tests/ext/distributed_tracing/otel_sampling_inherited.phpt b/tests/ext/distributed_tracing/otel_sampling_inherited.phpt index 0550c5031a9..49cc85da79b 100644 --- a/tests/ext/distributed_tracing/otel_sampling_inherited.phpt +++ b/tests/ext/distributed_tracing/otel_sampling_inherited.phpt @@ -25,7 +25,13 @@ function propagate(string $tracestate, bool $sampled = true, bool $manualKeep = function ot(string $tracestate): string { - return preg_match('/(?:^|,)ot=([^,]+)/', $tracestate, $matches) ? $matches[1] : ''; + 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 From a8633194e96c2ab92e33dae536631ad0c2a77547 Mon Sep 17 00:00:00 2001 From: Victor Pellan Date: Fri, 11 Sep 2026 12:39:49 +0200 Subject: [PATCH 08/13] refactor(otel): remove redundant threshold check --- tracer/otel_sampling.c | 3 --- 1 file changed, 3 deletions(-) diff --git a/tracer/otel_sampling.c b/tracer/otel_sampling.c index e39abcd4f2d..5a76e851aad 100644 --- a/tracer/otel_sampling.c +++ b/tracer/otel_sampling.c @@ -111,9 +111,6 @@ static uint64_t ddtrace_otel_threshold_for(double sample_rate) { double threshold = round((1 - sample_rate) * (double)DDTRACE_OTEL_MAX_THRESHOLD); - if (threshold <= 0) { - return 0; - } if (threshold >= (double)DDTRACE_OTEL_MAX_THRESHOLD) { return DDTRACE_OTEL_MAX_ENCODABLE_THRESHOLD; } From 118072d3ba8ec6599d84ae51773dd332437678b3 Mon Sep 17 00:00:00 2001 From: Victor Pellan Date: Fri, 11 Sep 2026 12:42:58 +0200 Subject: [PATCH 09/13] refactor(otel): format sampling fields with smart strings --- tracer/otel_sampling.c | 82 ++++++++++++++++-------------------------- 1 file changed, 30 insertions(+), 52 deletions(-) diff --git a/tracer/otel_sampling.c b/tracer/otel_sampling.c index 5a76e851aad..e01cfbd0bc2 100644 --- a/tracer/otel_sampling.c +++ b/tracer/otel_sampling.c @@ -117,14 +117,6 @@ static uint64_t ddtrace_otel_threshold_for(double sample_rate) { return (uint64_t)threshold; } -static void ddtrace_otel_encode_56_bit_hex(uint64_t value, char output[14]) { - static const char hex_digits[] = "0123456789abcdef"; - for (size_t i = 14; i > 0; --i) { - output[i - 1] = hex_digits[value & 0xf]; - value >>= 4; - } -} - static uint64_t ddtrace_otel_derive_random_value(uint64_t trace_id) { return ~(trace_id * DDTRACE_OTEL_KNUTH_FACTOR) >> 8; } @@ -141,27 +133,24 @@ static uint64_t ddtrace_otel_reconcile_random_value(uint64_t random_value, return random_value; } -static void ddtrace_otel_generate_fields(ddtrace_otel_fields* fields, - char random_value[14], - char threshold[14], uint64_t trace_id, - zend_long sampling_priority, - double sample_rate) { +static zend_string* ddtrace_otel_generate_value(uint64_t trace_id, + zend_long sampling_priority, + double sample_rate) { uint64_t threshold_value = ddtrace_otel_threshold_for(sample_rate); - uint64_t random_value_int = ddtrace_otel_reconcile_random_value( + uint64_t random_value = ddtrace_otel_reconcile_random_value( ddtrace_otel_derive_random_value(trace_id), threshold_value, sampling_priority > 0); - ddtrace_otel_encode_56_bit_hex(random_value_int, random_value); - ddtrace_otel_encode_56_bit_hex(threshold_value, threshold); - - size_t threshold_len = 14; - while (threshold_len > 1 && threshold[threshold_len - 1] == '0') { - --threshold_len; - } - fields->random_value = random_value; - fields->random_value_len = 14; - fields->threshold = threshold; - fields->threshold_len = threshold_len; + smart_str result = {0}; + smart_str_append_printf(&result, "rv:%014" PRIx64 ";th:%014" PRIx64, + random_value, threshold_value); + size_t minimum_len = sizeof("rv:00000000000000;th:0") - 1; + while (ZSTR_LEN(result.s) > minimum_len && + ZSTR_VAL(result.s)[ZSTR_LEN(result.s) - 1] == '0') { + --ZSTR_LEN(result.s); + } + smart_str_0(&result); + return result.s; } static zend_string* ddtrace_otel_rebuild_value( @@ -170,25 +159,26 @@ static zend_string* ddtrace_otel_rebuild_value( double sample_rate) { ddtrace_otel_fields fields = ddtrace_otel_parse_fields(otel_value, otel_value_len); - char generated_random_value[14]; - char generated_threshold[14]; + zend_string* generated_value = NULL; if (decision == DDTRACE_OTEL_SAMPLING_DECISION_NON_PROBABILITY) { fields.threshold = NULL; fields.threshold_len = 0; } else if (decision == DDTRACE_OTEL_SAMPLING_DECISION_PROBABILITY && sample_rate > 0) { - ddtrace_otel_generate_fields(&fields, generated_random_value, - generated_threshold, trace_id, - sampling_priority, sample_rate); + generated_value = ddtrace_otel_generate_value( + trace_id, sampling_priority, sample_rate); } smart_str result = {0}; - if (fields.random_value) { + if (generated_value) { + smart_str_append(&result, generated_value); + zend_string_release(generated_value); + } else if (fields.random_value) { smart_str_appends(&result, "rv:"); smart_str_appendl(&result, fields.random_value, fields.random_value_len); } - if (fields.threshold) { + if (!generated_value && fields.threshold) { if (result.s) { smart_str_appendc(&result, ';'); } @@ -398,18 +388,6 @@ zend_string* ddtrace_otel_sampling_limit_tracestate( return ddtrace_otel_limit_oversized_tracestate(tracestate); } -static char* ddtrace_otel_write_generated_member( - char* output, const ddtrace_otel_fields* fields) { - memcpy(output, "ot=rv:", 6); - output += 6; - memcpy(output, fields->random_value, fields->random_value_len); - output += fields->random_value_len; - memcpy(output, ";th:", 4); - output += 4; - memcpy(output, fields->threshold, fields->threshold_len); - return output + fields->threshold_len; -} - static ddtrace_otel_insertion ddtrace_otel_generated_member_insertion( zend_string* tracestate) { ddtrace_otel_insertion insertion = {0}; @@ -439,17 +417,13 @@ static ddtrace_otel_insertion ddtrace_otel_generated_member_insertion( static zend_string* ddtrace_otel_insert_generated_member( zend_string* tracestate, uint64_t trace_id, zend_long sampling_priority, double sample_rate, size_t member_count) { - ddtrace_otel_fields fields = {0}; - char random_value[14]; - char threshold[14]; - ddtrace_otel_generate_fields(&fields, random_value, threshold, trace_id, - sampling_priority, sample_rate); + zend_string* generated_value = ddtrace_otel_generate_value( + trace_id, sampling_priority, sample_rate); size_t raw_len = tracestate ? ZSTR_LEN(tracestate) : 0; ddtrace_otel_insertion insertion = ddtrace_otel_generated_member_insertion(tracestate); - size_t otel_member_len = - 3 + 3 + fields.random_value_len + 1 + 3 + fields.threshold_len; + size_t otel_member_len = 3 + ZSTR_LEN(generated_value); size_t insertion_len = otel_member_len + insertion.comma_before + insertion.comma_after; size_t result_len = raw_len + insertion_len; @@ -464,7 +438,11 @@ static zend_string* ddtrace_otel_insert_generated_member( if (insertion.comma_before) { *output++ = ','; } - output = ddtrace_otel_write_generated_member(output, &fields); + memcpy(output, "ot=", 3); + output += 3; + memcpy(output, ZSTR_VAL(generated_value), ZSTR_LEN(generated_value)); + output += ZSTR_LEN(generated_value); + zend_string_release(generated_value); if (insertion.comma_after) { *output++ = ','; } From d2a3b1e4a48443d06694016cc07f1a61eda5998b Mon Sep 17 00:00:00 2001 From: Victor Pellan Date: Fri, 11 Sep 2026 13:33:59 +0200 Subject: [PATCH 10/13] refactor(otel): store parsed sampling state separately --- .../Integration/API/TracerTest.php | 2 +- .../otel_sampling_inherited.phpt | 2 +- tracer/ddtrace.c | 3 + tracer/ddtrace_globals.h | 2 + tracer/distributed_tracing_headers.c | 28 +- tracer/distributed_tracing_headers.h | 2 + tracer/functions.c | 13 +- tracer/handlers_http.h | 13 +- tracer/otel_sampling.c | 415 ++++++------------ tracer/otel_sampling.h | 26 +- tracer/priority_sampling/priority_sampling.c | 15 +- tracer/priority_sampling/priority_sampling.h | 6 - tracer/serializer.c | 7 +- tracer/span.c | 2 +- tracer/span.h | 4 +- 15 files changed, 219 insertions(+), 321 deletions(-) diff --git a/tests/OpenTelemetry/Integration/API/TracerTest.php b/tests/OpenTelemetry/Integration/API/TracerTest.php index c6ab4e654c6..fcb598201bf 100644 --- a/tests/OpenTelemetry/Integration/API/TracerTest.php +++ b/tests/OpenTelemetry/Integration/API/TracerTest.php @@ -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,ot=rv:[0-9a-f]{14};th:0$/', (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/otel_sampling_inherited.phpt b/tests/ext/distributed_tracing/otel_sampling_inherited.phpt index 49cc85da79b..94302191d7e 100644 --- a/tests/ext/distributed_tracing/otel_sampling_inherited.phpt +++ b/tests/ext/distributed_tracing/otel_sampling_inherited.phpt @@ -76,7 +76,7 @@ rv:1234567890abcd foo:bar rv:65cd67504a538e -foo=bar,ot=rv:6e6d1a75832a2f,something=else +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/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 d7cd0df6887..240f0a758ef 100644 --- a/tracer/distributed_tracing_headers.c +++ b/tracer/distributed_tracing_headers.c @@ -376,7 +376,28 @@ static ddtrace_distributed_tracing_result ddtrace_read_distributed_tracing_ids_t char *persist = ZSTR_VAL(result.tracestate); int commas = 0; size_t tags_size = 0; + bool found_otel = false; for (char *ptr = ZSTR_VAL(tracestate), *end = ptr + ZSTR_LEN(tracestate); ptr < end; ++ptr) { + // ot member + if (last_comma && ptr + 2 < end && ptr[0] == 'o' && ptr[1] == 't' && ptr[2] == '=') { + while (persist > ZSTR_VAL(result.tracestate) && (persist[-1] == ' ' || persist[-1] == '\t')) { + --persist; + } + char *value_start = ptr + 3; + while (ptr < end && *ptr != ',') { + ++ptr; + } + char *value_end = ptr; + while (value_end > value_start && (value_end[-1] == ' ' || value_end[-1] == '\t')) { + --value_end; + } + if (!found_otel) { + ddtrace_otel_sampling_parse(&result.otel_sampling, value_start, value_end - value_start); + found_otel = true; + } + continue; + } + // 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 @@ -475,9 +496,6 @@ static ddtrace_distributed_tracing_result ddtrace_read_distributed_tracing_ids_t } *persist = 0; // and zero-terminate it ZSTR_LEN(result.tracestate) = persist - ZSTR_VAL(result.tracestate); - zend_string *normalized_tracestate = ddtrace_otel_sampling_update_tracestate( - result.tracestate, trace_id.low, result.priority_sampling, DDTRACE_OTEL_SAMPLING_DECISION_INHERITED, 0); - result.tracestate = normalized_tracestate ? normalized_tracestate : zend_string_init("", 0, 0); zend_string_release(tracestate); } @@ -556,6 +574,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; @@ -658,6 +677,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)); @@ -726,6 +746,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; @@ -769,6 +790,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 ea5a982aa3a..882bad3a337 100644 --- a/tracer/functions.c +++ b/tracer/functions.c @@ -244,7 +244,7 @@ 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); @@ -593,11 +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; - span->otel_sampling_decision = span->explicit_sampling_priority - ? DDTRACE_OTEL_SAMPLING_DECISION_NON_PROBABILITY - : DDTRACE_OTEL_SAMPLING_DECISION_INHERITED; + 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 c3b4645b4c9..00381254033 100644 --- a/tracer/handlers_http.h +++ b/tracer/handlers_http.h @@ -13,7 +13,7 @@ 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) { @@ -92,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, ','); @@ -406,13 +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 *base_tracestate = ddtrace_format_tracestate(tracestate, propagated_span_id, origin, sampling_priority, propagated_tags, tracestate_unknown_dd_keys); - zend_string *full_tracestate = ddtrace_otel_sampling_update_tracestate( - base_tracestate, - trace_id.low, - sampling_priority, - root ? root->otel_sampling_decision : DDTRACE_OTEL_SAMPLING_DECISION_INHERITED, - root ? root->otel_sampling_rate : 0); + 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 index e01cfbd0bc2..46a6976e078 100644 --- a/tracer/otel_sampling.c +++ b/tracer/otel_sampling.c @@ -1,9 +1,7 @@ #include "otel_sampling.h" -#include #include -#define DDTRACE_OTEL_MAX_VALUE_LEN 256 #define DDTRACE_TRACESTATE_MAX_LEN 512 #define DDTRACE_TRACESTATE_MAX_MEMBERS 32 #define DDTRACE_OTEL_MAX_THRESHOLD (UINT64_C(1) << 56) @@ -11,36 +9,28 @@ static const uint64_t DDTRACE_OTEL_KNUTH_FACTOR = UINT64_C(1111111111111111111); -typedef struct { - const char* random_value; - size_t random_value_len; - const char* threshold; - size_t threshold_len; -} ddtrace_otel_fields; - typedef struct { const char* datadog_member; size_t datadog_member_len; const char* otel_member; size_t otel_member_len; - const char* otel_value; - size_t otel_value_len; size_t member_count; } ddtrace_otel_tracestate_members; -typedef struct { - size_t offset; - bool comma_before; - bool comma_after; -} ddtrace_otel_insertion; - -static bool ddtrace_otel_is_lower_hex(const char* value, size_t len) { +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) { - if (!((value[i] >= '0' && value[i] <= '9') || - (value[i] >= 'a' && value[i] <= 'f'))) { + 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; } @@ -50,9 +40,23 @@ static bool ddtrace_otel_field_is(const char* field, size_t field_len, (field_len == 2 || field[2] == ':'); } -static ddtrace_otel_fields ddtrace_otel_parse_fields(const char* value, - size_t value_len) { - ddtrace_otel_fields fields = {0}; +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;) { @@ -63,11 +67,24 @@ static ddtrace_otel_fields ddtrace_otel_parse_fields(const char* value, size_t field_len = field_end - field; if (ddtrace_otel_field_is(field, field_len, "rv")) { - fields.random_value = field_len > 2 && field[2] == ':' ? field + 3 : NULL; - fields.random_value_len = fields.random_value ? field_len - 3 : 0; + 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")) { - fields.threshold = field_len > 2 && field[2] == ':' ? field + 3 : NULL; - fields.threshold_len = fields.threshold ? field_len - 3 : 0; + 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) { @@ -76,19 +93,6 @@ static ddtrace_otel_fields ddtrace_otel_parse_fields(const char* value, field = field_end + 1; } - if (fields.random_value_len != 14 || - !ddtrace_otel_is_lower_hex(fields.random_value, - fields.random_value_len)) { - fields.random_value = NULL; - fields.random_value_len = 0; - } - if (fields.threshold_len < 1 || fields.threshold_len > 14 || - !ddtrace_otel_is_lower_hex(fields.threshold, fields.threshold_len)) { - fields.threshold = NULL; - fields.threshold_len = 0; - } - - return fields; } static void ddtrace_otel_append_field(smart_str* result, const char* field, @@ -133,82 +137,25 @@ static uint64_t ddtrace_otel_reconcile_random_value(uint64_t random_value, return random_value; } -static zend_string* ddtrace_otel_generate_value(uint64_t trace_id, - zend_long sampling_priority, - double sample_rate) { - uint64_t threshold_value = ddtrace_otel_threshold_for(sample_rate); - uint64_t random_value = ddtrace_otel_reconcile_random_value( - ddtrace_otel_derive_random_value(trace_id), threshold_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); - - smart_str result = {0}; - smart_str_append_printf(&result, "rv:%014" PRIx64 ";th:%014" PRIx64, - random_value, threshold_value); - size_t minimum_len = sizeof("rv:00000000000000;th:0") - 1; - while (ZSTR_LEN(result.s) > minimum_len && - ZSTR_VAL(result.s)[ZSTR_LEN(result.s) - 1] == '0') { - --ZSTR_LEN(result.s); + state->random_value_len = 14; + state->threshold_len = 14; + while (state->threshold_len > 1 && (threshold & 0xf) == 0) { + threshold >>= 4; + --state->threshold_len; } - smart_str_0(&result); - return result.s; + state->threshold = threshold; } -static zend_string* ddtrace_otel_rebuild_value( - const char* otel_value, size_t otel_value_len, uint64_t trace_id, - zend_long sampling_priority, enum ddtrace_otel_sampling_decision decision, - double sample_rate) { - ddtrace_otel_fields fields = - ddtrace_otel_parse_fields(otel_value, otel_value_len); - zend_string* generated_value = NULL; - - if (decision == DDTRACE_OTEL_SAMPLING_DECISION_NON_PROBABILITY) { - fields.threshold = NULL; - fields.threshold_len = 0; - } else if (decision == DDTRACE_OTEL_SAMPLING_DECISION_PROBABILITY && - sample_rate > 0) { - generated_value = ddtrace_otel_generate_value( - trace_id, sampling_priority, sample_rate); - } - - smart_str result = {0}; - if (generated_value) { - smart_str_append(&result, generated_value); - zend_string_release(generated_value); - } else if (fields.random_value) { - smart_str_appends(&result, "rv:"); - smart_str_appendl(&result, fields.random_value, fields.random_value_len); - } - if (!generated_value && fields.threshold) { - if (result.s) { - smart_str_appendc(&result, ';'); - } - smart_str_appends(&result, "th:"); - smart_str_appendl(&result, fields.threshold, fields.threshold_len); - } - - const char* end = otel_value + otel_value_len; - for (const char* field = otel_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 (field_len && !ddtrace_otel_field_is(field, field_len, "rv") && - !ddtrace_otel_field_is(field, field_len, "th")) { - ddtrace_otel_append_field(&result, field, field_len); - } - - if (field_end == end) { - break; - } - field = field_end + 1; - } - - if (result.s) { - smart_str_0(&result); - } - return result.s; +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, @@ -231,6 +178,83 @@ static bool ddtrace_otel_is_member(const char* member, size_t member_len, 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 bool ddtrace_tracestate_member_is(const char* member, size_t member_len, const char* key) { while (member_len && (*member == ' ' || *member == '\t')) { @@ -260,8 +284,7 @@ static ddtrace_otel_tracestate_members ddtrace_otel_scan_tracestate( members.datadog_member = member; members.datadog_member_len = member_len; } else if (!members.otel_member && - ddtrace_otel_is_member(member, member_len, &members.otel_value, - &members.otel_value_len)) { + ddtrace_tracestate_member_is(member, member_len, "ot")) { members.otel_member = member; members.otel_member_len = member_len; } @@ -313,17 +336,6 @@ static size_t ddtrace_otel_datadog_member_prefix_len( return max_datadog_len; } -static void ddtrace_otel_append_value_member(smart_str* result, - const char key[2], - zend_string* value) { - if (result->s) { - smart_str_appendc(result, ','); - } - smart_str_appendl(result, key, 2); - smart_str_appendc(result, '='); - smart_str_append(result, value); -} - // Takes ownership of a tracestate which is known to exceed a W3C limit. static zend_string* ddtrace_otel_limit_oversized_tracestate( zend_string* tracestate) { @@ -387,156 +399,3 @@ zend_string* ddtrace_otel_sampling_limit_tracestate( } return ddtrace_otel_limit_oversized_tracestate(tracestate); } - -static ddtrace_otel_insertion ddtrace_otel_generated_member_insertion( - zend_string* tracestate) { - ddtrace_otel_insertion insertion = {0}; - size_t raw_len = tracestate ? ZSTR_LEN(tracestate) : 0; - if (!raw_len) { - return insertion; - } - - const char* first_comma = memchr(ZSTR_VAL(tracestate), ',', raw_len); - size_t first_member_len = - first_comma ? (size_t)(first_comma - ZSTR_VAL(tracestate)) : raw_len; - if (!ddtrace_tracestate_member_is(ZSTR_VAL(tracestate), first_member_len, - "dd")) { - insertion.comma_after = true; - } else if (first_comma) { - insertion.offset = first_member_len + 1; - insertion.comma_after = true; - } else { - insertion.offset = raw_len; - insertion.comma_before = true; - } - - return insertion; -} - -// Takes ownership of tracestate and inserts the generated member in-place. -static zend_string* ddtrace_otel_insert_generated_member( - zend_string* tracestate, uint64_t trace_id, zend_long sampling_priority, - double sample_rate, size_t member_count) { - zend_string* generated_value = ddtrace_otel_generate_value( - trace_id, sampling_priority, sample_rate); - - size_t raw_len = tracestate ? ZSTR_LEN(tracestate) : 0; - ddtrace_otel_insertion insertion = - ddtrace_otel_generated_member_insertion(tracestate); - size_t otel_member_len = 3 + ZSTR_LEN(generated_value); - size_t insertion_len = - otel_member_len + insertion.comma_before + insertion.comma_after; - size_t result_len = raw_len + insertion_len; - zend_string* result = tracestate - ? zend_string_extend(tracestate, result_len, 0) - : zend_string_alloc(result_len, 0); - char* raw = ZSTR_VAL(result); - memmove(raw + insertion.offset + insertion_len, raw + insertion.offset, - raw_len - insertion.offset); - - char* output = raw + insertion.offset; - if (insertion.comma_before) { - *output++ = ','; - } - memcpy(output, "ot=", 3); - output += 3; - memcpy(output, ZSTR_VAL(generated_value), ZSTR_LEN(generated_value)); - output += ZSTR_LEN(generated_value); - zend_string_release(generated_value); - if (insertion.comma_after) { - *output++ = ','; - } - ZEND_ASSERT(output == raw + insertion.offset + insertion_len); - ZSTR_VAL(result)[result_len] = '\0'; - - if (member_count + 1 <= DDTRACE_TRACESTATE_MAX_MEMBERS && - ZSTR_LEN(result) <= DDTRACE_TRACESTATE_MAX_LEN) { - return result; - } - return ddtrace_otel_limit_oversized_tracestate(result); -} - -// Takes ownership of tracestate and rewrites the first ot member. Any duplicate -// ot members are dropped while the remaining member order is preserved. -static zend_string* ddtrace_otel_rewrite_existing_member( - zend_string* tracestate, const ddtrace_otel_tracestate_members* members, - uint64_t trace_id, zend_long sampling_priority, - enum ddtrace_otel_sampling_decision decision, double sample_rate) { - zend_string* updated_value = ddtrace_otel_rebuild_value( - members->otel_value, members->otel_value_len, trace_id, sampling_priority, - decision, sample_rate); - smart_str result = {0}; - size_t result_member_count = 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) { - if (updated_value) { - ddtrace_otel_append_value_member(&result, "ot", updated_value); - ++result_member_count; - } - found_otel = true; - } - } else if (member_len) { - if (result.s) { - smart_str_appendc(&result, ','); - } - smart_str_appendl(&result, member, member_len); - ++result_member_count; - } - - member = member_end == end ? end : member_end + 1; - } - - if (updated_value) { - zend_string_release(updated_value); - } - zend_string_release(tracestate); - if (result.s) { - smart_str_0(&result); - } - if (result_member_count <= DDTRACE_TRACESTATE_MAX_MEMBERS && - (!result.s || ZSTR_LEN(result.s) <= DDTRACE_TRACESTATE_MAX_LEN)) { - return result.s; - } - return ddtrace_otel_limit_oversized_tracestate(result.s); -} - -zend_string* ddtrace_otel_sampling_update_tracestate( - zend_string* tracestate, uint64_t trace_id, zend_long sampling_priority, - enum ddtrace_otel_sampling_decision decision, double sample_rate) { - bool should_generate = - decision == DDTRACE_OTEL_SAMPLING_DECISION_PROBABILITY && sample_rate > 0; - if (!tracestate || ZSTR_LEN(tracestate) == 0) { - return should_generate - ? ddtrace_otel_insert_generated_member( - tracestate, trace_id, sampling_priority, sample_rate, 0) - : tracestate; - } - - ddtrace_otel_tracestate_members members = - ddtrace_otel_scan_tracestate(tracestate); - if (!members.otel_member) { - if (should_generate) { - return ddtrace_otel_insert_generated_member( - tracestate, trace_id, sampling_priority, sample_rate, - members.member_count); - } - return ddtrace_otel_sampling_limit_tracestate(tracestate); - } - - return ddtrace_otel_rewrite_existing_member( - tracestate, &members, trace_id, sampling_priority, decision, sample_rate); -} diff --git a/tracer/otel_sampling.h b/tracer/otel_sampling.h index 69ad5be0054..93ec62baf40 100644 --- a/tracer/otel_sampling.h +++ b/tracer/otel_sampling.h @@ -1,14 +1,28 @@ #ifndef DD_OTEL_SAMPLING_H #define DD_OTEL_SAMPLING_H -#include "ddtrace.h" -#include "priority_sampling/priority_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); -// Takes ownership of tracestate and returns the normalized replacement. -zend_string* ddtrace_otel_sampling_update_tracestate(zend_string* tracestate, uint64_t trace_id, zend_long sampling_priority, - enum ddtrace_otel_sampling_decision decision, double sample_rate); - #endif // DD_OTEL_SAMPLING_H diff --git a/tracer/priority_sampling/priority_sampling.c b/tracer/priority_sampling/priority_sampling.c index fdd4388c146..b10c071d060 100644 --- a/tracer/priority_sampling/priority_sampling.c +++ b/tracer/priority_sampling/priority_sampling.c @@ -389,10 +389,11 @@ static void dd_decide_on_sampling(ddtrace_root_span_data *span) { &limit_zv); } - span->otel_sampling_rate = sample_rate; - span->otel_sampling_decision = mechanism == DD_MECHANISM_MANUAL || mechanism == DD_MECHANISM_ASM || limited || sample_rate <= 0 - ? DDTRACE_OTEL_SAMPLING_DECISION_NON_PROBABILITY - : DDTRACE_OTEL_SAMPLING_DECISION_PROBABILITY; + 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); @@ -456,9 +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; - root_span->otel_sampling_decision = mechanism == DD_MECHANISM_DEFAULT - ? DDTRACE_OTEL_SAMPLING_DECISION_INHERITED - : DDTRACE_OTEL_SAMPLING_DECISION_NON_PROBABILITY; + if (mechanism != DD_MECHANISM_DEFAULT) { + ddtrace_otel_sampling_decide_non_probability(&root_span->otel_sampling); + } } } diff --git a/tracer/priority_sampling/priority_sampling.h b/tracer/priority_sampling/priority_sampling.h index 9f58f38dcd3..747a5f97273 100644 --- a/tracer/priority_sampling/priority_sampling.h +++ b/tracer/priority_sampling/priority_sampling.h @@ -23,12 +23,6 @@ enum dd_sampling_mechanism { DD_MECHANISM_REMOTE_DYNAMIC_RULE = 12, }; -enum ddtrace_otel_sampling_decision { - DDTRACE_OTEL_SAMPLING_DECISION_INHERITED, - DDTRACE_OTEL_SAMPLING_DECISION_PROBABILITY, - DDTRACE_OTEL_SAMPLING_DECISION_NON_PROBABILITY, -}; - void ddtrace_set_priority_sampling_on_root(zend_long priority, enum dd_sampling_mechanism mechanism); void ddtrace_set_priority_sampling_on_span(ddtrace_root_span_data *root_span, zend_long priority, enum dd_sampling_mechanism mechanism); DATADOG_PUBLIC void ddtrace_set_priority_sampling_on_span_zobj(zend_object *root_span, zend_long priority, enum dd_sampling_mechanism mechanism); diff --git a/tracer/serializer.c b/tracer/serializer.c index 7723bbf909b..7f4a97fdbf8 100644 --- a/tracer/serializer.c +++ b/tracer/serializer.c @@ -850,8 +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_rate = parent_root->otel_sampling_rate; - span->otel_sampling_decision = parent_root->otel_sampling_decision; + span->otel_sampling = parent_root->otel_sampling; } else { zval *prop_type = &span->property_type; zval *prop_name = &span->property_name; @@ -890,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); @@ -904,7 +904,7 @@ 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_decision = DDTRACE_OTEL_SAMPLING_DECISION_INHERITED; + span->otel_sampling = DDTRACE_G(otel_sampling); } } @@ -2217,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 e4235e3c8be..d0c3f5363dc 100644 --- a/tracer/span.c +++ b/tracer/span.c @@ -833,7 +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; - root_span->otel_sampling_decision = DDTRACE_OTEL_SAMPLING_DECISION_NON_PROBABILITY; + 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 637c36f8983..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,8 +127,7 @@ struct ddtrace_root_span_data { uint64_t parent_id; uint8_t trace_flags; ddtrace_rule_result sampling_rule; - double otel_sampling_rate; - enum ddtrace_otel_sampling_decision otel_sampling_decision; + 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. From 66a158e2518cd44502acc1ff7dd74b5acd77ab41 Mon Sep 17 00:00:00 2001 From: Victor Pellan Date: Fri, 11 Sep 2026 14:17:41 +0200 Subject: [PATCH 11/13] fix(otel): reserve tracestate slots for owned members --- .../otel_sampling_span_link_caps.phpt | 54 ++-- tracer/distributed_tracing_headers.c | 230 +++++++++--------- 2 files changed, 151 insertions(+), 133 deletions(-) diff --git a/tests/ext/distributed_tracing/otel_sampling_span_link_caps.phpt b/tests/ext/distributed_tracing/otel_sampling_span_link_caps.phpt index 42e2e879d26..ce06c4c2a2e 100644 --- a/tests/ext/distributed_tracing/otel_sampling_span_link_caps.phpt +++ b/tests/ext/distributed_tracing/otel_sampling_span_link_caps.phpt @@ -6,33 +6,45 @@ 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); -$link = DDTrace\SpanLink::fromHeaders([ - 'x-datadog-trace-id' => '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 = []; +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/tracer/distributed_tracing_headers.c b/tracer/distributed_tracing_headers.c index 240f0a758ef..9d24a8daacf 100644 --- a/tracer/distributed_tracing_headers.c +++ b/tracer/distributed_tracing_headers.c @@ -14,6 +14,8 @@ 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'); } @@ -57,6 +59,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; @@ -371,128 +456,49 @@ 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; + size_t vendor_member_count = 0; bool found_otel = false; - for (char *ptr = ZSTR_VAL(tracestate), *end = ptr + ZSTR_LEN(tracestate); ptr < end; ++ptr) { - // ot member - if (last_comma && ptr + 2 < end && ptr[0] == 'o' && ptr[1] == 't' && ptr[2] == '=') { - while (persist > ZSTR_VAL(result.tracestate) && (persist[-1] == ' ' || persist[-1] == '\t')) { - --persist; - } - char *value_start = ptr + 3; - while (ptr < end && *ptr != ',') { - ++ptr; - } - char *value_end = ptr; - while (value_end > value_start && (value_end[-1] == ' ' || value_end[-1] == '\t')) { - --value_end; - } - if (!found_otel) { - ddtrace_otel_sampling_parse(&result.otel_sampling, value_start, value_end - value_start); - found_otel = true; - } - continue; + 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; } - // 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; + char *trimmed_member = member; + while (trimmed_member < member_end && (*trimmed_member == ' ' || *trimmed_member == '\t')) { + ++trimmed_member; } - *(persist++) = *ptr; - - if (*ptr == ' ' || *ptr == '\t') { - continue; + 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; - last_comma = *ptr == ','; - // W3C Trace Context permits at most 32 list-members. Owned members - // are removed or normalized after this pass, then protected when - // the outbound header is rebuilt. - if (last_comma && ++commas == 32) { - --persist; - break; + if (trimmed_len >= 3 && memcmp(trimmed_member, "ot=", 3) == 0) { + if (!found_otel) { + ddtrace_otel_sampling_parse(&result.otel_sampling, trimmed_member + 3, trimmed_len - 3); + found_otel = true; + } + } else if (trimmed_len >= 3 && trimmed_member[0] == 'd' && trimmed_member[1] == 'd' && + (trimmed_member[2] == '=' || trimmed_member[2] == ' ' || trimmed_member[2] == '\t')) { + // 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); From 1e258b56f8fb54a52b42dab5185b70d105f6a9e4 Mon Sep 17 00:00:00 2001 From: Victor Pellan Date: Fri, 11 Sep 2026 14:19:46 +0200 Subject: [PATCH 12/13] style(otel): improve sampling code readability --- tracer/otel_sampling.c | 135 +++++++++++++---------------------------- tracer/otel_sampling.h | 3 +- 2 files changed, 44 insertions(+), 94 deletions(-) diff --git a/tracer/otel_sampling.c b/tracer/otel_sampling.c index 46a6976e078..28996fa18ab 100644 --- a/tracer/otel_sampling.c +++ b/tracer/otel_sampling.c @@ -17,8 +17,7 @@ typedef struct { size_t member_count; } ddtrace_otel_tracestate_members; -static bool ddtrace_otel_parse_lower_hex(const char* value, size_t len, - uint64_t* parsed) { +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; @@ -34,17 +33,13 @@ static bool ddtrace_otel_parse_lower_hex(const char* value, size_t len, 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 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) { +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) { + if (state->unknown_fields_len + separator_len + field_len > DDTRACE_OTEL_MAX_VALUE_LEN) { return; } if (separator_len) { @@ -54,8 +49,7 @@ static void ddtrace_otel_append_unknown_field( state->unknown_fields_len += field_len; } -void ddtrace_otel_sampling_parse(ddtrace_otel_sampling_state* state, - const char* value, size_t value_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; @@ -68,19 +62,13 @@ void ddtrace_otel_sampling_parse(ddtrace_otel_sampling_state* state, 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)) { + 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)) { + 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) { @@ -92,14 +80,11 @@ void ddtrace_otel_sampling_parse(ddtrace_otel_sampling_state* state, } field = field_end + 1; } - } -static void ddtrace_otel_append_field(smart_str* result, const char* field, - size_t field_len) { +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) { + if ((result->s ? ZSTR_LEN(result->s) : 0) + separator_len + field_len > DDTRACE_OTEL_MAX_VALUE_LEN) { return; } if (separator_len) { @@ -113,8 +98,7 @@ static uint64_t ddtrace_otel_threshold_for(double sample_rate) { return 0; } - double threshold = - round((1 - sample_rate) * (double)DDTRACE_OTEL_MAX_THRESHOLD); + double threshold = round((1 - sample_rate) * (double)DDTRACE_OTEL_MAX_THRESHOLD); if (threshold >= (double)DDTRACE_OTEL_MAX_THRESHOLD) { return DDTRACE_OTEL_MAX_ENCODABLE_THRESHOLD; } @@ -125,9 +109,7 @@ 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) { +static uint64_t ddtrace_otel_reconcile_random_value(uint64_t random_value, uint64_t threshold, bool kept) { if (kept && random_value < threshold) { return threshold; } @@ -137,13 +119,10 @@ static uint64_t ddtrace_otel_reconcile_random_value(uint64_t random_value, 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) { +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 = 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) { @@ -153,19 +132,16 @@ void ddtrace_otel_sampling_decide_probability( state->threshold = threshold; } -void ddtrace_otel_sampling_decide_non_probability( - ddtrace_otel_sampling_state* state) { +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) { +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')) { + while (member_len && (member[member_len - 1] == ' ' || member[member_len - 1] == '\t')) { --member_len; } @@ -178,8 +154,7 @@ static bool ddtrace_otel_is_member(const char* member, size_t member_len, return true; } -zend_string* ddtrace_otel_sampling_extract_tracestate( - zend_string* tracestate, ddtrace_otel_sampling_state* state) { +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; @@ -195,8 +170,7 @@ zend_string* ddtrace_otel_sampling_extract_tracestate( const char* otel_value; size_t otel_value_len; - if (ddtrace_otel_is_member(member, member_len, &otel_value, - &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; @@ -218,19 +192,16 @@ zend_string* ddtrace_otel_sampling_extract_tracestate( return zend_string_init("", 0, 0); } -void ddtrace_otel_sampling_append_to_tracestate( - smart_str* tracestate, const ddtrace_otel_sampling_state* state) { +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); + 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); + smart_str_append_printf(&value, "th:%0*" PRIx64, state->threshold_len, state->threshold); } const char* unknown = state->unknown_fields; @@ -255,18 +226,15 @@ void ddtrace_otel_sampling_append_to_tracestate( smart_str_free(&value); } -static bool ddtrace_tracestate_member_is(const char* member, size_t member_len, - const char* key) { +static bool ddtrace_tracestate_member_is(const char* member, size_t member_len, const char* key) { while (member_len && (*member == ' ' || *member == '\t')) { ++member; --member_len; } - return member_len >= 3 && member[0] == key[0] && member[1] == key[1] && - member[2] == '='; + return member_len >= 3 && member[0] == key[0] && member[1] == key[1] && member[2] == '='; } -static ddtrace_otel_tracestate_members ddtrace_otel_scan_tracestate( - zend_string* tracestate) { +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); @@ -279,12 +247,10 @@ static ddtrace_otel_tracestate_members ddtrace_otel_scan_tracestate( size_t member_len = member_end - member; ++members.member_count; - if (!members.datadog_member && - ddtrace_tracestate_member_is(member, member_len, "dd")) { + 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")) { + } else if (!members.otel_member && ddtrace_tracestate_member_is(member, member_len, "ot")) { members.otel_member = member; members.otel_member_len = member_len; } @@ -295,13 +261,10 @@ static ddtrace_otel_tracestate_members ddtrace_otel_scan_tracestate( return members; } -static bool ddtrace_otel_append_member(smart_str* result, const char* member, - size_t member_len, - size_t* member_count) { +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) { + (result->s ? ZSTR_LEN(result->s) : 0) + separator_len + member_len > DDTRACE_TRACESTATE_MAX_LEN) { return false; } if (separator_len) { @@ -312,8 +275,7 @@ static bool ddtrace_otel_append_member(smart_str* result, const char* member, return true; } -static size_t ddtrace_otel_datadog_member_prefix_len( - const ddtrace_otel_tracestate_members* members) { +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; @@ -329,38 +291,32 @@ static size_t ddtrace_otel_datadog_member_prefix_len( return datadog_member_len; } - while (max_datadog_len > 0 && - members->datadog_member[max_datadog_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) { +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); + 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); + 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); + 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); + ddtrace_otel_append_member(&limited, members.otel_member, members.otel_member_len, &member_count); } for (const char* member = raw; member < end;) { @@ -369,10 +325,8 @@ static zend_string* ddtrace_otel_limit_oversized_tracestate( 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)) { + 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; @@ -385,16 +339,13 @@ static zend_string* ddtrace_otel_limit_oversized_tracestate( return limited.s; } -zend_string* ddtrace_otel_sampling_limit_tracestate( - zend_string* tracestate) { +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) { + 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 index 93ec62baf40..795a8990422 100644 --- a/tracer/otel_sampling.h +++ b/tracer/otel_sampling.h @@ -17,8 +17,7 @@ typedef struct { 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_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); From 3f393646306e38f21c41e6875a3e2d945f0c9c56 Mon Sep 17 00:00:00 2001 From: Victor Pellan Date: Fri, 11 Sep 2026 15:05:36 +0200 Subject: [PATCH 13/13] refactor(tracing): share tracestate member matching --- tracer/distributed_tracing_headers.c | 9 ++++++--- tracer/otel_sampling.c | 9 +-------- tracer/tracestate.h | 15 +++++++++++++++ 3 files changed, 22 insertions(+), 11 deletions(-) create mode 100644 tracer/tracestate.h diff --git a/tracer/distributed_tracing_headers.c b/tracer/distributed_tracing_headers.c index 9d24a8daacf..c7f019fc79d 100644 --- a/tracer/distributed_tracing_headers.c +++ b/tracer/distributed_tracing_headers.c @@ -8,6 +8,7 @@ #include "tracer_tag_propagation/tracer_tag_propagation.h" #include "serializer.h" #include "trace_context.h" +#include "tracestate.h" #include "zend_string.h" #include #include @@ -477,14 +478,16 @@ static ddtrace_distributed_tracing_result ddtrace_read_distributed_tracing_ids_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 (trimmed_len >= 3 && memcmp(trimmed_member, "ot=", 3) == 0) { + 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 (trimmed_len >= 3 && trimmed_member[0] == 'd' && trimmed_member[1] == 'd' && - (trimmed_member[2] == '=' || trimmed_member[2] == ' ' || trimmed_member[2] == '\t')) { + } 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); diff --git a/tracer/otel_sampling.c b/tracer/otel_sampling.c index 28996fa18ab..a8cc65514c6 100644 --- a/tracer/otel_sampling.c +++ b/tracer/otel_sampling.c @@ -1,4 +1,5 @@ #include "otel_sampling.h" +#include "tracestate.h" #include @@ -226,14 +227,6 @@ void ddtrace_otel_sampling_append_to_tracestate(smart_str* tracestate, const ddt smart_str_free(&value); } -static bool ddtrace_tracestate_member_is(const char* member, size_t member_len, const char* key) { - while (member_len && (*member == ' ' || *member == '\t')) { - ++member; - --member_len; - } - return member_len >= 3 && member[0] == key[0] && member[1] == key[1] && member[2] == '='; -} - static ddtrace_otel_tracestate_members ddtrace_otel_scan_tracestate(zend_string* tracestate) { ddtrace_otel_tracestate_members members = {0}; const char* raw = ZSTR_VAL(tracestate); 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