From 3c6394ed166a15f65ac7ee0a424a57a093fe3614 Mon Sep 17 00:00:00 2001 From: Milan Garnier Date: Fri, 14 Aug 2026 14:21:04 +0200 Subject: [PATCH 01/16] feat(tracing): add OpenTelemetry tracestate sampling --- include/datadog/sampling_decision.h | 2 + include/datadog/trace_segment.h | 2 + src/datadog/extracted_data.h | 2 + src/datadog/trace_sampler.cpp | 6 +- src/datadog/trace_segment.cpp | 98 +++++++++--- src/datadog/tracer.cpp | 4 +- src/datadog/w3c_propagation.cpp | 227 ++++++++++++++++++++++++++-- src/datadog/w3c_propagation.h | 10 ++ test/test_span.cpp | 143 +++++++++++++++++- test/test_tracer.cpp | 44 ++++++ 10 files changed, 496 insertions(+), 42 deletions(-) diff --git a/include/datadog/sampling_decision.h b/include/datadog/sampling_decision.h index cfccbf747..8bb87f433 100644 --- a/include/datadog/sampling_decision.h +++ b/include/datadog/sampling_decision.h @@ -39,6 +39,8 @@ struct SamplingDecision { // The per-second maximum allowed number of "keeps" configured for the limiter // consulted in this decision, if any. Optional limiter_max_per_second; + // The outcome of the probability comparison before rate limiting, if any. + Optional probability_sampled; // The provenance of this decision. Origin origin; }; diff --git a/include/datadog/trace_segment.h b/include/datadog/trace_segment.h index f7fa958ab..e4c0e24d1 100644 --- a/include/datadog/trace_segment.h +++ b/include/datadog/trace_segment.h @@ -77,6 +77,7 @@ class TraceSegment { std::vector> spans_; std::size_t num_finished_spans_; Optional sampling_decision_; + const Optional otel_w3c_tracestate_; const Optional additional_w3c_tracestate_; const Optional additional_datadog_w3c_tracestate_; @@ -99,6 +100,7 @@ class TraceSegment { Optional origin, std::size_t tags_header_max_size, std::vector> trace_tags, Optional sampling_decision, + Optional otel_w3c_tracestate, Optional additional_w3c_tracestate, Optional additional_datadog_w3c_tracestate, std::unique_ptr local_root, diff --git a/src/datadog/extracted_data.h b/src/datadog/extracted_data.h index a26274d78..37a5e7417 100644 --- a/src/datadog/extracted_data.h +++ b/src/datadog/extracted_data.h @@ -31,6 +31,8 @@ struct ExtractedData { // then `additional_w3c_tracestate` is null. // `additional_w3c_tracestate` is used for the `W3C` injection style. Optional additional_w3c_tracestate; + // The raw value of the OpenTelemetry `ot` tracestate member, if present. + Optional otel_w3c_tracestate; // If this `ExtractedData` was created on account of `PropagationStyle::W3C`, // and if the "tracestate" header contained a "dd" (Datadog) entry, then // `additional_datadog_w3c_tracestate` contains fields from within the "dd" diff --git a/src/datadog/trace_sampler.cpp b/src/datadog/trace_sampler.cpp index d455b9c42..6ed1fada9 100644 --- a/src/datadog/trace_sampler.cpp +++ b/src/datadog/trace_sampler.cpp @@ -51,7 +51,8 @@ SamplingDecision TraceSampler::decide(const SpanData& span) { decision.limiter_max_per_second = limiter_max_per_second_; decision.configured_rate = rule.rate; const std::uint64_t threshold = max_id_from_rate(rule.rate); - if (knuth_hash(span.trace_id.low) <= threshold) { + decision.probability_sampled = knuth_hash(span.trace_id.low) <= threshold; + if (*decision.probability_sampled) { if (rule.bypass_limiter) { decision.priority = int(SamplingPriority::USER_KEEP); return decision; @@ -91,7 +92,8 @@ SamplingDecision TraceSampler::decide(const SpanData& span) { } const std::uint64_t threshold = max_id_from_rate(*decision.configured_rate); - if (knuth_hash(span.trace_id.low) <= threshold) { + decision.probability_sampled = knuth_hash(span.trace_id.low) <= threshold; + if (*decision.probability_sampled) { decision.priority = int(SamplingPriority::AUTO_KEEP); } else { decision.priority = int(SamplingPriority::AUTO_DROP); diff --git a/src/datadog/trace_segment.cpp b/src/datadog/trace_segment.cpp index 555f648c0..81011412b 100644 --- a/src/datadog/trace_segment.cpp +++ b/src/datadog/trace_segment.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -14,6 +15,8 @@ #include #include #include +#include +#include #include #include #include @@ -23,6 +26,7 @@ #include "endpoint_inferral.h" #include "hex.h" #include "platform_util.h" +#include "sampling_util.h" #include "span_data.h" #include "span_sampler.h" #include "tag_propagation.h" @@ -151,6 +155,54 @@ Optional format_rate(double rate, Logger& logger) { return std::string(begin, end); } +bool is_probability_mechanism(int mechanism) { + switch (static_cast(mechanism)) { + case SamplingMechanism::DEFAULT: + case SamplingMechanism::AGENT_RATE: + case SamplingMechanism::REMOTE_RATE_AUTO: + case SamplingMechanism::RULE: + case SamplingMechanism::REMOTE_RATE_USER_DEFINED: + case SamplingMechanism::REMOTE_RATE_EMERGENCY: + case SamplingMechanism::REMOTE_RULE: + case SamplingMechanism::REMOTE_ADAPTIVE_RULE: + return true; + default: + return false; + } +} + +Optional resolve_otel_tracestate( + TraceID trace_id, const SamplingDecision& decision, + const Optional& inherited) { + const StringView raw = inherited ? StringView(*inherited) : StringView{}; + if (decision.origin != SamplingDecision::Origin::LOCAL) { + return inherited ? sanitize_otel_tracestate(raw) : nullopt; + } + + if (!decision.mechanism || !decision.configured_rate || + !decision.probability_sampled || + !is_probability_mechanism(*decision.mechanism) || + (*decision.probability_sampled && decision.priority <= 0)) { + return rewrite_otel_tracestate(raw, extract_otel_random_value(raw), + nullopt); + } + + constexpr std::uint64_t max_value = UINT64_C(1) << 56; + auto threshold = static_cast( + std::round((1.0 - decision.configured_rate->value()) * + static_cast(max_value))); + threshold = std::min(threshold, max_value - 1); + + std::uint64_t random_value = (~knuth_hash(trace_id.low)) >> 8; + if (*decision.probability_sampled && random_value < threshold) { + random_value = threshold; + } else if (!*decision.probability_sampled && random_value >= threshold) { + random_value = threshold == 0 ? 0 : threshold - 1; + } + + return rewrite_otel_tracestate(raw, random_value, threshold); +} + } // anonymous namespace TraceSegment::TraceSegment( @@ -166,6 +218,7 @@ TraceSegment::TraceSegment( std::size_t tags_header_max_size, std::vector> trace_tags, Optional sampling_decision, + Optional otel_w3c_tracestate, Optional additional_w3c_tracestate, Optional additional_datadog_w3c_tracestate, std::unique_ptr local_root, @@ -184,6 +237,7 @@ TraceSegment::TraceSegment( trace_tags_(std::move(trace_tags)), num_finished_spans_(0), sampling_decision_(std::move(sampling_decision)), + otel_w3c_tracestate_(std::move(otel_w3c_tracestate)), additional_w3c_tracestate_(std::move(additional_w3c_tracestate)), additional_datadog_w3c_tracestate_( std::move(additional_datadog_w3c_tracestate)), @@ -216,14 +270,14 @@ Optional TraceSegment::sampling_decision() const { Optional> TraceSegment::w3c_link_context( const SpanData& span) const { - int sampling_priority; + SamplingDecision sampling_decision; std::vector> trace_tags; { std::lock_guard lock(mutex_); if (!sampling_decision_) { return nullopt; } - sampling_priority = sampling_decision_->priority; + sampling_decision = *sampling_decision_; trace_tags = trace_tags_; const Optional trace_source_tag = @@ -234,10 +288,13 @@ Optional> TraceSegment::w3c_link_context( } return std::make_pair( - encode_tracestate(span.span_id, sampling_priority, origin_, trace_tags, - additional_datadog_w3c_tracestate_, - additional_w3c_tracestate_), - sampling_priority > 0 ? 1u : 0u); + encode_tracestate( + span.span_id, sampling_decision.priority, origin_, trace_tags, + additional_datadog_w3c_tracestate_, + resolve_otel_tracestate(span.trace_id, sampling_decision, + otel_w3c_tracestate_), + additional_w3c_tracestate_), + sampling_decision.priority > 0 ? 1u : 0u); } Logger& TraceSegment::logger() const { return *logger_; } @@ -443,13 +500,13 @@ bool TraceSegment::inject(DictWriter& writer, const SpanData& span, // and trace tags might change when that happens ("_dd.p.dm"). // So, we lock here, make a sampling decision if necessary, and then copy the // decision and trace tags before unlocking. - int sampling_priority; + SamplingDecision sampling_decision; std::vector> trace_tags; { std::lock_guard lock(mutex_); make_sampling_decision_if_null(); assert(sampling_decision_); - sampling_priority = sampling_decision_->priority; + sampling_decision = *sampling_decision_; trace_tags = trace_tags_; } @@ -464,7 +521,7 @@ bool TraceSegment::inject(DictWriter& writer, const SpanData& span, // - the local root span is NOT created by another product (no `_dd.p.ts`) // - sampling priority is DROP if (!tracing_enabled_) { - if (!trace_source_tag && sampling_priority <= 0) { + if (!trace_source_tag && sampling_decision.priority <= 0) { writer.erase("x-datadog-trace-id"); writer.erase("x-datadog-parent-id"); writer.erase("x-datadog-sampling-priority"); @@ -492,7 +549,7 @@ bool TraceSegment::inject(DictWriter& writer, const SpanData& span, writer.set("x-datadog-trace-id", std::to_string(span.trace_id.low)); writer.set("x-datadog-parent-id", std::to_string(span.span_id)); writer.set("x-datadog-sampling-priority", - std::to_string(sampling_priority)); + std::to_string(sampling_decision.priority)); if (origin_) { writer.set("x-datadog-origin", *origin_); } @@ -509,7 +566,8 @@ bool TraceSegment::inject(DictWriter& writer, const SpanData& span, writer.set("x-b3-traceid", hex_padded(span.trace_id.low)); } writer.set("x-b3-spanid", hex_padded(span.span_id)); - writer.set("x-b3-sampled", std::to_string(int(sampling_priority > 0))); + writer.set("x-b3-sampled", + std::to_string(int(sampling_decision.priority > 0))); if (origin_) { writer.set("x-datadog-origin", *origin_); } @@ -519,14 +577,16 @@ bool TraceSegment::inject(DictWriter& writer, const SpanData& span, {"header_style:b3multi"}); break; case PropagationStyle::W3C: - writer.set( - "traceparent", - encode_traceparent(span.trace_id, span.span_id, sampling_priority)); - writer.set( - "tracestate", - encode_tracestate(span.span_id, sampling_priority, origin_, - trace_tags, additional_datadog_w3c_tracestate_, - additional_w3c_tracestate_)); + writer.set("traceparent", + encode_traceparent(span.trace_id, span.span_id, + sampling_decision.priority)); + writer.set("tracestate", + encode_tracestate( + span.span_id, sampling_decision.priority, origin_, + trace_tags, additional_datadog_w3c_tracestate_, + resolve_otel_tracestate(span.trace_id, sampling_decision, + otel_w3c_tracestate_), + additional_w3c_tracestate_)); telemetry::counter::increment(metrics::tracer::trace_context::injected, {"header_style:tracecontext"}); break; diff --git a/src/datadog/tracer.cpp b/src/datadog/tracer.cpp index 8c0e6bd76..4872dcea3 100644 --- a/src/datadog/tracer.cpp +++ b/src/datadog/tracer.cpp @@ -247,7 +247,8 @@ Span Tracer::create_span(const SpanConfig& config) { logger_, collector_, config_manager_->trace_sampler(), span_sampler_, defaults, config_manager_, runtime_id_, injection_styles_, hostname_, nullopt /* origin */, tags_header_max_size_, std::move(trace_tags), - nullopt /* sampling_decision */, nullopt /* additional_w3c_tracestate */, + nullopt /* sampling_decision */, nullopt /* otel_w3c_tracestate */, + nullopt /* additional_w3c_tracestate */, nullopt /* additional_datadog_w3c_tracestate*/, std::move(span_data), resource_renaming_mode_, tracing_enabled_); Span span{span_data_ptr, segment, @@ -483,6 +484,7 @@ Expected Tracer::extract_span(const DictReader& reader, injection_styles_, hostname_, std::move(merged_context.origin), tags_header_max_size_, std::move(merged_context.trace_tags), std::move(sampling_decision), + std::move(merged_context.otel_w3c_tracestate), std::move(merged_context.additional_w3c_tracestate), std::move(merged_context.additional_datadog_w3c_tracestate), std::move(span_data), resource_renaming_mode_, tracing_enabled_); diff --git a/src/datadog/w3c_propagation.cpp b/src/datadog/w3c_propagation.cpp index acba657fa..da1ac2a50 100644 --- a/src/datadog/w3c_propagation.cpp +++ b/src/datadog/w3c_propagation.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include "hex.h" @@ -38,6 +39,107 @@ constexpr bool is_hexdiglc(const char c) { (c >= 'A' && c <= 'F'); } +constexpr bool is_lowercase_hexdig(const char c) { + return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f'); +} + +bool is_valid_otel_hex(StringView value, std::size_t minimum_size, + std::size_t maximum_size) { + return value.size() >= minimum_size && value.size() <= maximum_size && + std::all_of(value.begin(), value.end(), is_lowercase_hexdig); +} + +Optional parse_otel_random_value(StringView value) { + if (!is_valid_otel_hex(value, 14, 14)) { + return nullopt; + } + + const auto parsed = parse_uint64(value, 16); + if (parsed.if_error()) { + return nullopt; + } + return *parsed; +} + +Optional parse_otel_threshold(StringView value) { + if (!is_valid_otel_hex(value, 1, 14)) { + return nullopt; + } + + const auto parsed = parse_uint64(value, 16); + if (parsed.if_error()) { + return nullopt; + } + return *parsed; +} + +template +void for_each_otel_item(StringView raw, Function&& function) { + std::size_t begin = 0; + while (begin <= raw.size()) { + const auto end = raw.find(';', begin); + const auto item = raw.substr(begin, end - begin); + const auto separator = item.find(':'); + const auto key = item.substr(0, separator); + const auto value = separator == StringView::npos + ? StringView{} + : item.substr(separator + 1); + function(item, key, value); + if (end == StringView::npos) { + return; + } + begin = end + 1; + } +} + +template +void for_each_tracestate_member(StringView raw, Function&& function) { + std::size_t begin = 0; + while (begin < raw.size()) { + const auto end = raw.find(',', begin); + const auto member = trim(raw.substr(begin, end - begin)); + const auto separator = member.find('='); + const auto key = member.substr(0, separator); + const auto value = separator == StringView::npos + ? StringView{} + : member.substr(separator + 1); + function(member, key, value); + if (end == StringView::npos) { + return; + } + begin = end + 1; + } +} + +bool append_otel_item(std::string& result, StringView item) { + if (item.empty()) { + return true; + } + + const std::size_t separator_size = result.empty() ? 0 : 1; + if (result.size() + separator_size + item.size() > 256) { + return false; + } + + if (separator_size) { + result += ';'; + } + append(result, item); + return true; +} + +std::string format_otel_hex(std::uint64_t value) { + return hex_padded(value).substr(2); +} + +std::string format_otel_threshold(std::uint64_t threshold) { + auto result = format_otel_hex(threshold); + while (result.size() > 1 && result.back() == '0') { + result.pop_back(); + } + return result; +} + // Populate the specified `result` with data extracted from the "traceparent" // entry of the specified `headers`. Return `nullopt` on success. Return a value // for the `tags::internal::w3c_extraction_error` tag if an error occurs. @@ -295,22 +397,47 @@ void extract_tracestate( if (!tracestate.empty()) { result.additional_w3c_tracestate = std::string{tracestate}; } - return; - } + } else { + auto& [datadog_value, other_entries] = *maybe_parsed; + if (!other_entries.empty()) { + result.additional_w3c_tracestate = std::move(other_entries); + } - auto& [datadog_value, other_entries] = *maybe_parsed; - if (!other_entries.empty()) { - result.additional_w3c_tracestate = std::move(other_entries); + // If the "dd" vendor entry's value exceeds 512 bytes, drop it and record a + // propagation error tag. + if (datadog_value.size() > 512) { + span_tags[tags::internal::propagation_error] = "extract_max_size"; + } else { + parse_datadog_tracestate(result, datadog_value); + } } - // If the "dd" vendor entry's value exceeds 512 bytes, drop it and record a - // propagation error tag. - if (datadog_value.size() > 512) { - span_tags[tags::internal::propagation_error] = "extract_max_size"; + if (!result.additional_w3c_tracestate) { return; } - parse_datadog_tracestate(result, datadog_value); + std::string remaining; + for_each_tracestate_member( + *result.additional_w3c_tracestate, + [&](StringView item, StringView key, StringView value) { + if (key == "ot") { + if (!result.otel_w3c_tracestate) { + result.otel_w3c_tracestate = std::string(value); + } + return; + } + + if (!remaining.empty()) { + remaining += ','; + } + append(remaining, item); + }); + + if (remaining.empty()) { + result.additional_w3c_tracestate = nullopt; + } else { + result.additional_w3c_tracestate = std::move(remaining); + } } } // namespace @@ -424,19 +551,95 @@ std::string encode_datadog_tracestate( return result; } +Optional sanitize_otel_tracestate(StringView raw) { + std::string result; + for_each_otel_item(raw, + [&](StringView item, StringView key, StringView value) { + if ((key == "rv" && !parse_otel_random_value(value)) || + (key == "th" && !parse_otel_threshold(value))) { + return; + } + append_otel_item(result, item); + }); + if (result.empty()) { + return nullopt; + } + return result; +} + +Optional extract_otel_random_value(StringView raw) { + Optional result; + for_each_otel_item(raw, [&](StringView, StringView key, StringView value) { + if (!result && key == "rv") { + result = parse_otel_random_value(value); + } + }); + return result; +} + +Optional rewrite_otel_tracestate( + StringView raw, Optional random_value, + Optional threshold) { + std::string result; + if (random_value) { + const std::string item = "rv:" + format_otel_hex(*random_value); + append_otel_item(result, item); + } + if (threshold) { + const std::string item = "th:" + format_otel_threshold(*threshold); + append_otel_item(result, item); + } + + for_each_otel_item(raw, [&](StringView item, StringView key, StringView) { + if (key != "rv" && key != "th") { + append_otel_item(result, item); + } + }); + + if (result.empty()) { + return nullopt; + } + return result; +} + +void append_tracestate_entries(std::string& result, StringView entries, + std::size_t& member_count) { + std::size_t begin = 0; + while (member_count < 32 && begin < entries.size()) { + const auto end = entries.find(',', begin); + const auto entry = trim(entries.substr(begin, end - begin)); + if (!entry.empty()) { + result += ','; + append(result, entry); + ++member_count; + } + if (end == StringView::npos) { + return; + } + begin = end + 1; + } +} + std::string encode_tracestate( uint64_t span_id, int sampling_priority, const Optional& origin, const std::vector>& trace_tags, const Optional& additional_datadog_w3c_tracestate, + const Optional& otel_w3c_tracestate, const Optional& additional_w3c_tracestate) { std::string result = encode_datadog_tracestate(span_id, sampling_priority, origin, trace_tags, additional_datadog_w3c_tracestate); + std::size_t member_count = 1; + if (otel_w3c_tracestate) { + result += ",ot="; + result += *otel_w3c_tracestate; + ++member_count; + } + if (additional_w3c_tracestate) { - result += ','; - result += *additional_w3c_tracestate; + append_tracestate_entries(result, *additional_w3c_tracestate, member_count); } return result; diff --git a/src/datadog/w3c_propagation.h b/src/datadog/w3c_propagation.h index 823cce39b..7c1e66163 100644 --- a/src/datadog/w3c_propagation.h +++ b/src/datadog/w3c_propagation.h @@ -7,6 +7,7 @@ #include #include #include +#include #include #include @@ -37,12 +38,21 @@ Expected extract_w3c( std::string encode_traceparent(TraceID trace_id, std::uint64_t span_id, int sampling_priority); +Optional sanitize_otel_tracestate(StringView raw); + +Optional extract_otel_random_value(StringView raw); + +Optional rewrite_otel_tracestate( + StringView raw, Optional random_value, + Optional threshold); + // Return a value for the "tracestate" header containing the specified fields. std::string encode_tracestate( uint64_t span_id, int sampling_priority, const Optional& origin, const std::vector>& trace_tags, const Optional& additional_datadog_w3c_tracestate, + const Optional& otel_w3c_tracestate, const Optional& additional_w3c_tracestate); } // namespace tracing diff --git a/test/test_span.cpp b/test/test_span.cpp index 6608e60b8..06d61ecb8 100644 --- a/test/test_span.cpp +++ b/test/test_span.cpp @@ -4,6 +4,7 @@ #include #include +#include #include #include #include @@ -792,7 +793,8 @@ TEST_SPAN("injecting W3C tracestate header") { {"x-datadog-origin", "France"}, }, // The "s:-1" and "t.ksr:0" comes from the 0% sample rate. - "dd=s:-1;p:$parent_id;o:France;t.ksr:0"}, + "dd=s:-1;p:$parent_id;o:France;t.ksr:0,ot=rv:f0948a54d43b8e;th:" + "ffffffffffffff"}, {__LINE__, "trace tags", @@ -802,7 +804,8 @@ TEST_SPAN("injecting W3C tracestate header") { {"x-datadog-tags", "_dd.p.foo=x,_dd.p.bar=y,ignored=wrong_prefix"}, }, // The "s:-1" and "t.ksr:0" comes from the 0% sample rate. - "dd=s:-1;p:$parent_id;t.foo:x;t.bar:y;t.ksr:0"}, + "dd=s:-1;p:$parent_id;t.foo:x;t.bar:y;t.ksr:0,ot=rv:f0948a54d43b8e;" + "th:ffffffffffffff"}, {__LINE__, "extra fields", @@ -832,7 +835,7 @@ TEST_SPAN("injecting W3C tracestate header") { }, // The "s:-1" comes from the 0% sample rate. "dd=s:-1;p:$parent_id;o:France_ is a country~nation_ so is " - "______.;t.ksr:0", + "______.;t.ksr:0,ot=rv:f0948a54d43b8e;th:ffffffffffffff", }, {__LINE__, @@ -843,7 +846,8 @@ TEST_SPAN("injecting W3C tracestate header") { {"x-datadog-tags", "_dd.p.a;d台北x =foo,_dd.p.ok=bar"}, }, // The "s:-1" comes from the 0% sample rate. - "dd=s:-1;p:$parent_id;t.a_d______x_:foo;t.ok:bar;t.ksr:0"}, + "dd=s:-1;p:$parent_id;t.a_d______x_:foo;t.ok:bar;t.ksr:0,ot=rv:" + "f0948a54d43b8e;th:ffffffffffffff"}, {__LINE__, "replace invalid characters in trace tag value", @@ -854,7 +858,7 @@ TEST_SPAN("injecting W3C tracestate header") { }, // The "s:-1" comes from the 0% sample rate. "dd=s:-1;p:$parent_id;t.wacky:hello fr_d_ how are " - "_________?;t.ksr:0"}, + "_________?;t.ksr:0,ot=rv:f0948a54d43b8e;th:ffffffffffffff"}, {__LINE__, "replace equal signs with tildes in trace tag value", @@ -864,7 +868,8 @@ TEST_SPAN("injecting W3C tracestate header") { {"x-datadog-tags", "_dd.p.base64_thingy=d2Fra2EhIHdhaw=="}, }, // The "s:-1" comes from the 0% sample rate. - "dd=s:-1;p:$parent_id;t.base64_thingy:d2Fra2EhIHdhaw~~;t.ksr:0"}, + "dd=s:-1;p:$parent_id;t.base64_thingy:d2Fra2EhIHdhaw~~;t.ksr:0,ot=" + "rv:f0948a54d43b8e;th:ffffffffffffff"}, {__LINE__, "oversized origin truncates it and subsequent fields", @@ -883,7 +888,7 @@ TEST_SPAN("injecting W3C tracestate header") { {"x-datadog-tags", "_dd.p.foo=bar,_dd.p.honk=honk"}, }, // The "s:-1" comes from the 0% sample rate. - "dd=s:-1;p:$parent_id"}, + "dd=s:-1;p:$parent_id,ot=rv:f0948a54d43b8e;th:ffffffffffffff"}, {__LINE__, "oversized trace tag truncates it and subsequent fields", @@ -901,7 +906,8 @@ TEST_SPAN("injecting W3C tracestate header") { "ooooooooooooooooooong,_dd.p.lost=forever"}, }, // The "s:-1" comes from the 0% sample rate. - "dd=s:-1;p:$parent_id;t.foo:bar"}, + "dd=s:-1;p:$parent_id;t.foo:bar,ot=rv:f0948a54d43b8e;" + "th:ffffffffffffff"}, {__LINE__, "oversized extra field truncates itself and subsequent fields", @@ -955,6 +961,127 @@ TEST_SPAN("injecting W3C tracestate header") { REQUIRE(logger->error_count() == 0); } +TEST_SPAN("OpenTelemetry consistent probability sampling") { + class Generator : public IDGenerator { + const TraceID trace_id_; + + public: + explicit Generator(TraceID trace_id) : trace_id_(trace_id) {} + TraceID trace_id(const TimePoint&) const override { return trace_id_; } + std::uint64_t span_id() const override { return trace_id_.low; } + }; + + SECTION("local probability decisions emit a consistent rv and th") { + struct TestCase { + double rate; + std::uint64_t trace_id; + bool sampled; + std::string expected_ot; + }; + + const auto test_case = GENERATE(values({ + {0.01, 1, false, "rv:f0948a54d43b8e;th:fd70a3d70a3d7"}, + {0.1, 1, true, "rv:f0948a54d43b8e;th:e6666666666668"}, + {0.2, 1, true, "rv:f0948a54d43b8e;th:ccccccccccccd"}, + {0.5, 1, true, "rv:f0948a54d43b8e;th:8"}, + {0.99, 1, true, "rv:f0948a54d43b8e;th:028f5c28f5c29"}, + {0.1, UINT64_C(0x03A93EE8B1999F00), true, + "rv:e6666666666668;th:e6666666666668"}, + {0.05, UINT64_C(5401449561355763072), false, + "rv:f333333333332f;th:f333333333333"}, + })); + + CAPTURE(test_case.rate); + CAPTURE(test_case.trace_id); + CAPTURE(test_case.expected_ot); + + TracerConfig config; + config.service = "testsvc"; + config.collector = std::make_shared(); + config.logger = std::make_shared(); + config.telemetry.enabled = false; + config.injection_styles = {PropagationStyle::W3C}; + config.trace_sampler.sample_rate = test_case.rate; + config.trace_sampler.max_per_second = 100; + + const auto finalized = finalize_config(config); + REQUIRE(finalized); + Tracer tracer{*finalized, + std::make_shared(TraceID(test_case.trace_id))}; + + auto span = tracer.create_span(); + MockDictWriter writer; + span.inject(writer); + + const auto tracestate = writer.items.find("tracestate"); + REQUIRE(tracestate != writer.items.end()); + REQUIRE(tracestate->second.find("dd=") == 0); + REQUIRE(tracestate->second.find("ot=" + test_case.expected_ot) != + std::string::npos); + const auto& traceparent = writer.items.at("traceparent"); + REQUIRE(traceparent.substr(traceparent.size() - 3) == + (test_case.sampled ? "-01" : "-00")); + } + + SECTION("non-probability decisions retain inherited rv but erase th") { + TracerConfig config; + config.service = "testsvc"; + config.collector = std::make_shared(); + config.logger = std::make_shared(); + config.telemetry.enabled = false; + config.extraction_styles = {PropagationStyle::W3C}; + config.injection_styles = {PropagationStyle::W3C}; + + const auto finalized = finalize_config(config); + REQUIRE(finalized); + Tracer tracer{*finalized}; + + const std::unordered_map input_headers{ + {"traceparent", + "00-00000000000000000000000000000001-0000000000000001-00"}, + {"tracestate", "ot=rv:1234567890abcd;th:e6666666666668"}, + }; + MockDictReader reader{input_headers}; + auto span = tracer.extract_span(reader); + REQUIRE(span); + span->trace_segment().override_sampling_priority( + int(SamplingPriority::USER_KEEP)); + + MockDictWriter writer; + span->inject(writer); + REQUIRE(writer.items.at("tracestate").find("ot=rv:1234567890abcd") != + std::string::npos); + REQUIRE(writer.items.at("tracestate").find("th:") == std::string::npos); + } + + SECTION("rate-limiter demotion clears locally generated sampling values") { + TracerConfig config; + config.service = "testsvc"; + config.collector = std::make_shared(); + config.logger = std::make_shared(); + config.telemetry.enabled = false; + config.injection_styles = {PropagationStyle::W3C}; + config.trace_sampler.sample_rate = 1.0; + config.trace_sampler.max_per_second = 0.1; + + const auto finalized = finalize_config(config); + REQUIRE(finalized); + Tracer tracer{*finalized, std::make_shared(TraceID(1))}; + + { + auto span = tracer.create_span(); + MockDictWriter writer; + span.inject(writer); + REQUIRE(writer.items.at("tracestate").find("ot=") != std::string::npos); + } + + auto span = tracer.create_span(); + MockDictWriter writer; + span.inject(writer); + REQUIRE(writer.items.at("tracestate").find("ot=") == std::string::npos); + } +} + TEST_SPAN("128-bit trace ID injection") { TracerConfig config; config.service = "testsvc"; diff --git a/test/test_tracer.cpp b/test/test_tracer.cpp index 55f8d15c7..a753adf0f 100644 --- a/test/test_tracer.cpp +++ b/test/test_tracer.cpp @@ -1727,6 +1727,50 @@ TEST_TRACER("restart extraction link uses metadata from the selected context") { REQUIRE(link.context.flags == Optional(1u)); } +TEST_TRACER("OpenTelemetry tracestate sampling values") { + SECTION("malformed sampling values are removed") { + const auto normalized = + sanitize_otel_tracestate("rv:1234567890abcd;th:not-hex;future:value"); + REQUIRE(normalized); + REQUIRE(*normalized == "rv:1234567890abcd;future:value"); + + REQUIRE(!sanitize_otel_tracestate("rv:not-hex;th:also-not-hex")); + } + + SECTION("sampling values are replaced without altering other values") { + const auto rewritten = rewrite_otel_tracestate("rv:bad;future:value;th:bad", + UINT64_C(0xf0948a54d43b8e), + UINT64_C(0xe6666666666668)); + REQUIRE(rewritten); + REQUIRE(*rewritten == "rv:f0948a54d43b8e;th:e6666666666668;future:value"); + + const auto no_threshold = + rewrite_otel_tracestate("rv:1234567890abcd;th:e6666666666668", + UINT64_C(0x1234567890abcd), nullopt); + REQUIRE(no_threshold); + REQUIRE(*no_threshold == "rv:1234567890abcd"); + } + + SECTION("the OpenTelemetry member is separated from other vendors") { + const std::unordered_map headers{ + {"traceparent", + "00-00000000000000000000000000000001-0000000000000001-01"}, + {"tracestate", + "dd=s:2,ot=rv:1234567890abcd;th:e6666666666668;future:value," + "congo=t61rcWkgMzE"}, + }; + MockDictReader reader{headers}; + std::unordered_map span_tags; + MockLogger logger; + + const auto extracted = extract_w3c(reader, span_tags, logger); + REQUIRE(extracted); + REQUIRE(extracted->otel_w3c_tracestate == + "rv:1234567890abcd;th:e6666666666668;future:value"); + REQUIRE(extracted->additional_w3c_tracestate == "congo=t61rcWkgMzE"); + } +} + TEST_TRACER("baggage usage") { TracerConfig config; config.logger = std::make_shared(); From 9c814e8f3c527fc7e1237189a73df3ffa352ae08 Mon Sep 17 00:00:00 2001 From: Milan Garnier Date: Mon, 17 Aug 2026 10:45:58 +0200 Subject: [PATCH 02/16] chore(tracing): remove redundant includes --- src/datadog/trace_segment.cpp | 2 -- src/datadog/w3c_propagation.cpp | 1 - test/test_span.cpp | 1 - 3 files changed, 4 deletions(-) diff --git a/src/datadog/trace_segment.cpp b/src/datadog/trace_segment.cpp index 81011412b..98ef7b537 100644 --- a/src/datadog/trace_segment.cpp +++ b/src/datadog/trace_segment.cpp @@ -6,7 +6,6 @@ #include #include #include -#include #include #include #include @@ -16,7 +15,6 @@ #include #include #include -#include #include #include #include diff --git a/src/datadog/w3c_propagation.cpp b/src/datadog/w3c_propagation.cpp index da1ac2a50..57f3f5942 100644 --- a/src/datadog/w3c_propagation.cpp +++ b/src/datadog/w3c_propagation.cpp @@ -6,7 +6,6 @@ #include #include #include -#include #include #include "hex.h" diff --git a/test/test_span.cpp b/test/test_span.cpp index 06d61ecb8..d579a14f3 100644 --- a/test/test_span.cpp +++ b/test/test_span.cpp @@ -4,7 +4,6 @@ #include #include -#include #include #include #include From cb6e08fa5a1d2afb9de6024280dee62621b6d370 Mon Sep 17 00:00:00 2001 From: Milan Garnier Date: Mon, 17 Aug 2026 11:32:04 +0200 Subject: [PATCH 03/16] refactor(propagation): simplify tracestate parsing --- src/datadog/w3c_propagation.cpp | 208 ++++++++++---------------------- test/test_tracer.cpp | 4 +- 2 files changed, 65 insertions(+), 147 deletions(-) diff --git a/src/datadog/w3c_propagation.cpp b/src/datadog/w3c_propagation.cpp index 57f3f5942..1c40d6225 100644 --- a/src/datadog/w3c_propagation.cpp +++ b/src/datadog/w3c_propagation.cpp @@ -91,25 +91,6 @@ void for_each_otel_item(StringView raw, Function&& function) { } } -template -void for_each_tracestate_member(StringView raw, Function&& function) { - std::size_t begin = 0; - while (begin < raw.size()) { - const auto end = raw.find(',', begin); - const auto member = trim(raw.substr(begin, end - begin)); - const auto separator = member.find('='); - const auto key = member.substr(0, separator); - const auto value = separator == StringView::npos - ? StringView{} - : member.substr(separator + 1); - function(member, key, value); - if (end == StringView::npos) { - return; - } - begin = end + 1; - } -} - bool append_otel_item(std::string& result, StringView item) { if (item.empty()) { return true; @@ -225,86 +206,25 @@ Optional extract_traceparent(ExtractedData& result, return nullopt; } -// `struct PartiallyParsedTracestat` contains the separated Datadog-specific and -// non-Datadog-specific portions of tracestate. -struct PartiallyParsedTracestate { - StringView datadog_value; - std::string other_entries; -}; - -// Return the separate Datadog-specific and non-Datadog-specific portions of the -// specified `tracestate`. If `tracestate` does not have a Datadog-specific -// portion, return `nullopt`. -Optional parse_tracestate(StringView tracestate) { - const std::size_t begin = 0; - const std::size_t end = tracestate.size(); - std::size_t pair_begin = begin; - while (pair_begin < end) { - const std::size_t pair_end = tracestate.find(',', pair_begin); - // Note that since this `pair` is `strip`ped, `pair_begin` is not - // necessarily equal to `pair.begin()` (similarly for the ends). - const auto pair = - trim(tracestate.substr(pair_begin, pair_end - pair_begin)); - if (pair.empty()) { - pair_begin = (pair_end == StringView::npos) ? end : pair_end + 1; - continue; - } - - const auto kv_separator = pair.find('='); - if (kv_separator == StringView::npos) { - // This is an invalid entry because it contains a non-whitespace character - // but not a "=". - // Let's move on to the next entry. - pair_begin = (pair_end == StringView::npos) ? end : pair_end + 1; - continue; - } - - const auto key = pair.substr(0, kv_separator); - if (key != "dd") { - // On to the next. - pair_begin = (pair_end == StringView::npos) ? end : pair_end + 1; - continue; - } - - PartiallyParsedTracestate result; - result.datadog_value = pair.substr(kv_separator + 1); - // `result->other_entries` is whatever was before the "dd" entry and - // whatever is after the "dd" entry, but without an extra comma in the - // middle. - if (pair_begin != 0) { - // There's a prefix - append(result.other_entries, tracestate.substr(0, pair_begin - 1)); - if (pair_end != StringView::npos && pair_end + 1 < end) { - // and a suffix - append(result.other_entries, tracestate.substr(pair_end)); - } - } else if (pair_end != StringView::npos && pair_end + 1 < end) { - // There's just a suffix - append(result.other_entries, tracestate.substr(pair_end + 1)); - } - - return result; - } - - return nullopt; -} // Fill the specified `result` with information parsed from the specified -// `datadog_value`. `datadog_value` is the value of the "dd" entry in the -// "tracestate" header. +// `datadog_trace_state`. `datadog_trace_state` is the value of the "dd" entry +// in the W3C "tracestate" header. // -// `parse_datadog_tracestate` populates the following `ExtractedData` fields: +// `parse_datadog_trace_state` populates the following `ExtractedData` fields: // // - `origin` // - `trace_tags` // - `sampling_priority` // - `datadog_w3c_parent_id` // - `additional_datadog_w3c_tracestate` -void parse_datadog_tracestate(ExtractedData& result, StringView datadog_value) { - const std::size_t end = datadog_value.size(); +void parse_datadog_trace_state(ExtractedData& result, + StringView datadog_trace_state) { + const std::size_t end = datadog_trace_state.size(); std::size_t pair_begin = 0; while (pair_begin < end) { - const std::size_t pair_end = datadog_value.find(';', pair_begin); - const auto pair = datadog_value.substr(pair_begin, pair_end - pair_begin); + const std::size_t pair_end = datadog_trace_state.find(';', pair_begin); + const auto pair = + datadog_trace_state.substr(pair_begin, pair_end - pair_begin); pair_begin = (pair_end == StringView::npos) ? end : pair_end + 1; if (pair.empty()) { continue; @@ -373,69 +293,62 @@ void parse_datadog_tracestate(ExtractedData& result, StringView datadog_value) { } } -// Fill the specified `result` with information parsed from the "tracestate" -// element of the specified `headers`, if present. +// Fill the specified `result` with information parsed from the specified +// `ot_tracestate`. `ot_tracestate` is the value of the "ot" entry in the W3C +// "tracestate" header. // -// `extract_tracestate` populates the `additional_w3c_tracestate` field of -// `ExtractedData`, in addition to those populated by -// `parse_datadog_tracestate`. -void extract_tracestate( - ExtractedData& result, const DictReader& headers, - std::unordered_map& span_tags) { - const auto maybe_tracestate = headers.lookup("tracestate"); - if (!maybe_tracestate || maybe_tracestate->empty()) { - return; +// `parse_ot_tracestate` populates `otel_w3c_tracestate` if it has not already +// been set. +void parse_ot_tracestate(ExtractedData& result, StringView ot_tracestate) { + if (!result.otel_w3c_tracestate) { + result.otel_w3c_tracestate = std::string(ot_tracestate); } +} - const auto tracestate = trim(*maybe_tracestate); - result.tracestate_full = tracestate; - - auto maybe_parsed = parse_tracestate(tracestate); - if (!maybe_parsed) { - // No "dd" entry in `tracestate`, so there's nothing to extract. - if (!tracestate.empty()) { - result.additional_w3c_tracestate = std::string{tracestate}; - } - } else { - auto& [datadog_value, other_entries] = *maybe_parsed; - if (!other_entries.empty()) { - result.additional_w3c_tracestate = std::move(other_entries); - } - - // If the "dd" vendor entry's value exceeds 512 bytes, drop it and record a - // propagation error tag. - if (datadog_value.size() > 512) { - span_tags[tags::internal::propagation_error] = "extract_max_size"; +// Fill the specified `result` with information parsed from the specified W3C +// `tracestate`. +// +// `parse_w3c_tracestate` populates `additional_w3c_tracestate`, in addition to +// the fields populated by `parse_datadog_trace_state` and +// `parse_ot_tracestate`. +void parse_w3c_tracestate( + ExtractedData& result, StringView w3c_tracestate, + std::unordered_map& span_tags) { + std::string other_w3c_tracestate; + std::size_t begin = 0; + while (begin < w3c_tracestate.size()) { + const auto end = w3c_tracestate.find(',', begin); + const auto member = trim(w3c_tracestate.substr(begin, end - begin)); + const auto separator = member.find('='); + const auto key = member.substr(0, separator); + const auto member_value = separator == StringView::npos + ? StringView{} + : member.substr(separator + 1); + if (key == "dd") { + // If the "dd" vendor entry's value exceeds 512 bytes, drop it and + // record a propagation error tag. + if (member_value.size() > 512) { + span_tags[tags::internal::propagation_error] = "extract_max_size"; + } else { + parse_datadog_trace_state(result, member_value); + } + } else if (key == "ot") { + parse_ot_tracestate(result, member_value); } else { - parse_datadog_tracestate(result, datadog_value); + if (!other_w3c_tracestate.empty()) { + other_w3c_tracestate += ','; + } + append(other_w3c_tracestate, member); } - } - if (!result.additional_w3c_tracestate) { - return; + if (end == StringView::npos) { + break; + } + begin = end + 1; } - std::string remaining; - for_each_tracestate_member( - *result.additional_w3c_tracestate, - [&](StringView item, StringView key, StringView value) { - if (key == "ot") { - if (!result.otel_w3c_tracestate) { - result.otel_w3c_tracestate = std::string(value); - } - return; - } - - if (!remaining.empty()) { - remaining += ','; - } - append(remaining, item); - }); - - if (remaining.empty()) { - result.additional_w3c_tracestate = nullopt; - } else { - result.additional_w3c_tracestate = std::move(remaining); + if (!other_w3c_tracestate.empty()) { + result.additional_w3c_tracestate = std::move(other_w3c_tracestate); } } @@ -466,7 +379,12 @@ Expected extract_w3c( } result.datadog_w3c_parent_id = "0000000000000000"; - extract_tracestate(result, headers, span_tags); + const auto maybe_tracestate = headers.lookup("tracestate"); + if (maybe_tracestate && !maybe_tracestate->empty()) { + const auto tracestate = trim(*maybe_tracestate); + result.tracestate_full = tracestate; + parse_w3c_tracestate(result, tracestate, span_tags); + } return result; } diff --git a/test/test_tracer.cpp b/test/test_tracer.cpp index a753adf0f..b064bcab1 100644 --- a/test/test_tracer.cpp +++ b/test/test_tracer.cpp @@ -1730,11 +1730,11 @@ TEST_TRACER("restart extraction link uses metadata from the selected context") { TEST_TRACER("OpenTelemetry tracestate sampling values") { SECTION("malformed sampling values are removed") { const auto normalized = - sanitize_otel_tracestate("rv:1234567890abcd;th:not-hex;future:value"); + sanitize_otel_tracestate("rv:1234567890abcd;th:ABC;future:value"); REQUIRE(normalized); REQUIRE(*normalized == "rv:1234567890abcd;future:value"); - REQUIRE(!sanitize_otel_tracestate("rv:not-hex;th:also-not-hex")); + REQUIRE(!sanitize_otel_tracestate("rv:1234567890ABCD;th:A")); } SECTION("sampling values are replaced without altering other values") { From 5a53772315fa4e5916234b4268a28f65e22e422a Mon Sep 17 00:00:00 2001 From: Milan Garnier Date: Mon, 17 Aug 2026 13:02:08 +0200 Subject: [PATCH 04/16] fix(propagation): retain matching OpenTelemetry tracestate --- src/datadog/extracted_data.h | 6 +++--- src/datadog/extraction_util.cpp | 1 + src/datadog/w3c_propagation.cpp | 30 +++++++++++++++++++----------- test/test_tracer.cpp | 10 +++++++--- 4 files changed, 30 insertions(+), 17 deletions(-) diff --git a/src/datadog/extracted_data.h b/src/datadog/extracted_data.h index 37a5e7417..83e12a5ae 100644 --- a/src/datadog/extracted_data.h +++ b/src/datadog/extracted_data.h @@ -26,9 +26,9 @@ struct ExtractedData { // refering to the latest datadog parent ID. Optional datadog_w3c_parent_id; // If this `ExtractedData` was created on account of `PropagationStyle::W3C`, - // then `additional_w3c_tracestate` contains the parts of the "tracestate" - // header that are not the "dd" (Datadog) entry. If there are no other parts, - // then `additional_w3c_tracestate` is null. + // then `additional_w3c_tracestate` contains the entries of the "tracestate" + // header other than the "dd" (Datadog) and "ot" (OpenTelemetry) entries. + // If there are no such entries, then `additional_w3c_tracestate` is null. // `additional_w3c_tracestate` is used for the `W3C` injection style. Optional additional_w3c_tracestate; // The raw value of the OpenTelemetry `ot` tracestate member, if present. diff --git a/src/datadog/extraction_util.cpp b/src/datadog/extraction_util.cpp index b5405ece2..e9a7c27e0 100644 --- a/src/datadog/extraction_util.cpp +++ b/src/datadog/extraction_util.cpp @@ -283,6 +283,7 @@ ExtractedData merge( if (w3c != contexts.end() && w3c->second.trace_id == result.trace_id) { result.additional_w3c_tracestate = w3c->second.additional_w3c_tracestate; + result.otel_w3c_tracestate = w3c->second.otel_w3c_tracestate; result.additional_datadog_w3c_tracestate = w3c->second.additional_datadog_w3c_tracestate; result.headers_examined.insert(result.headers_examined.end(), diff --git a/src/datadog/w3c_propagation.cpp b/src/datadog/w3c_propagation.cpp index 1c40d6225..d9ec5830b 100644 --- a/src/datadog/w3c_propagation.cpp +++ b/src/datadog/w3c_propagation.cpp @@ -42,6 +42,12 @@ constexpr bool is_lowercase_hexdig(const char c) { return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f'); } +constexpr std::size_t otel_sampling_value_size = 14; +constexpr std::size_t min_otel_threshold_size = 1; +constexpr std::size_t max_w3c_tracestate_member_value_size = 256; +constexpr std::size_t max_datadog_tracestate_value_size = 512; +constexpr std::size_t max_w3c_tracestate_members = 32; + bool is_valid_otel_hex(StringView value, std::size_t minimum_size, std::size_t maximum_size) { return value.size() >= minimum_size && value.size() <= maximum_size && @@ -49,7 +55,8 @@ bool is_valid_otel_hex(StringView value, std::size_t minimum_size, } Optional parse_otel_random_value(StringView value) { - if (!is_valid_otel_hex(value, 14, 14)) { + if (!is_valid_otel_hex(value, otel_sampling_value_size, + otel_sampling_value_size)) { return nullopt; } @@ -61,7 +68,8 @@ Optional parse_otel_random_value(StringView value) { } Optional parse_otel_threshold(StringView value) { - if (!is_valid_otel_hex(value, 1, 14)) { + if (!is_valid_otel_hex(value, min_otel_threshold_size, + otel_sampling_value_size)) { return nullopt; } @@ -97,7 +105,8 @@ bool append_otel_item(std::string& result, StringView item) { } const std::size_t separator_size = result.empty() ? 0 : 1; - if (result.size() + separator_size + item.size() > 256) { + if (result.size() + separator_size + item.size() > + max_w3c_tracestate_member_value_size) { return false; } @@ -325,9 +334,9 @@ void parse_w3c_tracestate( ? StringView{} : member.substr(separator + 1); if (key == "dd") { - // If the "dd" vendor entry's value exceeds 512 bytes, drop it and - // record a propagation error tag. - if (member_value.size() > 512) { + // If the "dd" vendor entry's value exceeds the maximum size, drop it + // and record a propagation error tag. + if (member_value.size() > max_datadog_tracestate_value_size) { span_tags[tags::internal::propagation_error] = "extract_max_size"; } else { parse_datadog_trace_state(result, member_value); @@ -455,12 +464,11 @@ std::string encode_datadog_tracestate( result += *additional_datadog_w3c_tracestate; } - const std::size_t max_size = 256; - while (result.size() > max_size) { + while (result.size() > max_w3c_tracestate_member_value_size) { const auto last_semicolon_index = result.rfind(';'); // This assumption is safe, because `result` always begins with - // "dd=s:", and that's fewer than `max_size` characters for any - // ``. + // "dd=s:", and that's fewer than + // `max_w3c_tracestate_member_value_size` characters for any ``. assert(last_semicolon_index != std::string::npos); result.resize(last_semicolon_index); } @@ -522,7 +530,7 @@ Optional rewrite_otel_tracestate( void append_tracestate_entries(std::string& result, StringView entries, std::size_t& member_count) { std::size_t begin = 0; - while (member_count < 32 && begin < entries.size()) { + while (member_count < max_w3c_tracestate_members && begin < entries.size()) { const auto end = entries.find(',', begin); const auto entry = trim(entries.substr(begin, end - begin)); if (!entry.empty()) { diff --git a/test/test_tracer.cpp b/test/test_tracer.cpp index b064bcab1..2d8cd1764 100644 --- a/test/test_tracer.cpp +++ b/test/test_tracer.cpp @@ -2081,9 +2081,12 @@ TEST_TRACER("heterogeneous extraction") { {{"x-datadog-trace-id", "48"}, {"x-datadog-parent-id", "64"}, {"x-datadog-origin", "Kansas"}, {"x-datadog-sampling-priority", "2"}, {"traceparent", "00-00000000000000000000000000000030-0000000000000040-01"}, - {"tracestate", "competitor=stuff,dd=o:Nebraska;s:1;ah:choo"}}, // origin is different + {"tracestate", "competitor=stuff,dd=o:Nebraska;s:1;ah:choo," + "ot=rv:1234567890abcd;th:e6666666666668;future:value"}}, // origin is different {{"traceparent", "00-00000000000000000000000000000030-000000000000002a-01"}, - {"tracestate", "dd=s:2;p:000000000000002a;o:Kansas;ah:choo,competitor=stuff"}}}, + {"tracestate", "dd=s:2;p:000000000000002a;o:Kansas;ah:choo," + "ot=rv:1234567890abcd;th:e6666666666668;future:value," + "competitor=stuff"}}}, {__LINE__, "ignore interlopers", {PropagationStyle::DATADOG, PropagationStyle::B3, PropagationStyle::W3C}, @@ -2104,7 +2107,8 @@ TEST_TRACER("heterogeneous extraction") { {{"x-datadog-trace-id", "48"}, {"x-datadog-parent-id", "64"}, {"x-datadog-origin", "Kansas"}, {"x-datadog-sampling-priority", "2"}, {"traceparent", "00-00000000000000000000000000000031-0000000000000040-01"}, - {"tracestate", "competitor=stuff,dd=o:Nebraska;s:1;ah:choo"}}, + {"tracestate", "competitor=stuff,dd=o:Nebraska;s:1;ah:choo," + "ot=rv:1234567890abcd;th:e6666666666668;future:value"}}, {{"traceparent", "00-00000000000000000000000000000030-000000000000002a-01"}, {"tracestate", "dd=s:2;p:000000000000002a;o:Kansas"}}}, From 0e2c402b5628ba14522359ef740db45f0f0e6259 Mon Sep 17 00:00:00 2001 From: Milan Garnier Date: Tue, 18 Aug 2026 15:26:03 +0200 Subject: [PATCH 05/16] refactor: use explicit types --- src/datadog/trace_segment.cpp | 2 +- src/datadog/w3c_propagation.cpp | 43 +++++++++++++++++---------------- test/test_span.cpp | 16 ++++++------ test/test_tracer.cpp | 13 +++++----- 4 files changed, 38 insertions(+), 36 deletions(-) diff --git a/src/datadog/trace_segment.cpp b/src/datadog/trace_segment.cpp index 98ef7b537..b0ca4a335 100644 --- a/src/datadog/trace_segment.cpp +++ b/src/datadog/trace_segment.cpp @@ -186,7 +186,7 @@ Optional resolve_otel_tracestate( } constexpr std::uint64_t max_value = UINT64_C(1) << 56; - auto threshold = static_cast( + std::uint64_t threshold = static_cast( std::round((1.0 - decision.configured_rate->value()) * static_cast(max_value))); threshold = std::min(threshold, max_value - 1); diff --git a/src/datadog/w3c_propagation.cpp b/src/datadog/w3c_propagation.cpp index d9ec5830b..c4fab4273 100644 --- a/src/datadog/w3c_propagation.cpp +++ b/src/datadog/w3c_propagation.cpp @@ -60,7 +60,7 @@ Optional parse_otel_random_value(StringView value) { return nullopt; } - const auto parsed = parse_uint64(value, 16); + const Expected parsed = parse_uint64(value, 16); if (parsed.if_error()) { return nullopt; } @@ -73,7 +73,7 @@ Optional parse_otel_threshold(StringView value) { return nullopt; } - const auto parsed = parse_uint64(value, 16); + const Expected parsed = parse_uint64(value, 16); if (parsed.if_error()) { return nullopt; } @@ -84,13 +84,13 @@ template void for_each_otel_item(StringView raw, Function&& function) { std::size_t begin = 0; while (begin <= raw.size()) { - const auto end = raw.find(';', begin); - const auto item = raw.substr(begin, end - begin); - const auto separator = item.find(':'); - const auto key = item.substr(0, separator); - const auto value = separator == StringView::npos - ? StringView{} - : item.substr(separator + 1); + const std::size_t end = raw.find(';', begin); + const StringView item = raw.substr(begin, end - begin); + const std::size_t separator = item.find(':'); + const StringView key = item.substr(0, separator); + const StringView value = + separator == StringView::npos ? StringView{} + : item.substr(separator + 1); function(item, key, value); if (end == StringView::npos) { return; @@ -122,7 +122,7 @@ std::string format_otel_hex(std::uint64_t value) { } std::string format_otel_threshold(std::uint64_t threshold) { - auto result = format_otel_hex(threshold); + std::string result = format_otel_hex(threshold); while (result.size() > 1 && result.back() == '0') { result.pop_back(); } @@ -326,13 +326,14 @@ void parse_w3c_tracestate( std::string other_w3c_tracestate; std::size_t begin = 0; while (begin < w3c_tracestate.size()) { - const auto end = w3c_tracestate.find(',', begin); - const auto member = trim(w3c_tracestate.substr(begin, end - begin)); - const auto separator = member.find('='); - const auto key = member.substr(0, separator); - const auto member_value = separator == StringView::npos - ? StringView{} - : member.substr(separator + 1); + const std::size_t end = w3c_tracestate.find(',', begin); + const StringView member = + trim(w3c_tracestate.substr(begin, end - begin)); + const std::size_t separator = member.find('='); + const StringView key = member.substr(0, separator); + const StringView member_value = + separator == StringView::npos ? StringView{} + : member.substr(separator + 1); if (key == "dd") { // If the "dd" vendor entry's value exceeds the maximum size, drop it // and record a propagation error tag. @@ -388,9 +389,9 @@ Expected extract_w3c( } result.datadog_w3c_parent_id = "0000000000000000"; - const auto maybe_tracestate = headers.lookup("tracestate"); + const Optional maybe_tracestate = headers.lookup("tracestate"); if (maybe_tracestate && !maybe_tracestate->empty()) { - const auto tracestate = trim(*maybe_tracestate); + const StringView tracestate = trim(*maybe_tracestate); result.tracestate_full = tracestate; parse_w3c_tracestate(result, tracestate, span_tags); } @@ -531,8 +532,8 @@ void append_tracestate_entries(std::string& result, StringView entries, std::size_t& member_count) { std::size_t begin = 0; while (member_count < max_w3c_tracestate_members && begin < entries.size()) { - const auto end = entries.find(',', begin); - const auto entry = trim(entries.substr(begin, end - begin)); + const std::size_t end = entries.find(',', begin); + const StringView entry = trim(entries.substr(begin, end - begin)); if (!entry.empty()) { result += ','; append(result, entry); diff --git a/test/test_span.cpp b/test/test_span.cpp index d579a14f3..1304c0998 100644 --- a/test/test_span.cpp +++ b/test/test_span.cpp @@ -1003,12 +1003,12 @@ TEST_SPAN("OpenTelemetry consistent probability sampling") { config.trace_sampler.sample_rate = test_case.rate; config.trace_sampler.max_per_second = 100; - const auto finalized = finalize_config(config); + const Expected finalized = finalize_config(config); REQUIRE(finalized); Tracer tracer{*finalized, std::make_shared(TraceID(test_case.trace_id))}; - auto span = tracer.create_span(); + Span span = tracer.create_span(); MockDictWriter writer; span.inject(writer); @@ -1017,7 +1017,7 @@ TEST_SPAN("OpenTelemetry consistent probability sampling") { REQUIRE(tracestate->second.find("dd=") == 0); REQUIRE(tracestate->second.find("ot=" + test_case.expected_ot) != std::string::npos); - const auto& traceparent = writer.items.at("traceparent"); + const std::string& traceparent = writer.items.at("traceparent"); REQUIRE(traceparent.substr(traceparent.size() - 3) == (test_case.sampled ? "-01" : "-00")); } @@ -1031,7 +1031,7 @@ TEST_SPAN("OpenTelemetry consistent probability sampling") { config.extraction_styles = {PropagationStyle::W3C}; config.injection_styles = {PropagationStyle::W3C}; - const auto finalized = finalize_config(config); + const Expected finalized = finalize_config(config); REQUIRE(finalized); Tracer tracer{*finalized}; @@ -1041,7 +1041,7 @@ TEST_SPAN("OpenTelemetry consistent probability sampling") { {"tracestate", "ot=rv:1234567890abcd;th:e6666666666668"}, }; MockDictReader reader{input_headers}; - auto span = tracer.extract_span(reader); + Expected span = tracer.extract_span(reader); REQUIRE(span); span->trace_segment().override_sampling_priority( int(SamplingPriority::USER_KEEP)); @@ -1063,18 +1063,18 @@ TEST_SPAN("OpenTelemetry consistent probability sampling") { config.trace_sampler.sample_rate = 1.0; config.trace_sampler.max_per_second = 0.1; - const auto finalized = finalize_config(config); + const Expected finalized = finalize_config(config); REQUIRE(finalized); Tracer tracer{*finalized, std::make_shared(TraceID(1))}; { - auto span = tracer.create_span(); + Span span = tracer.create_span(); MockDictWriter writer; span.inject(writer); REQUIRE(writer.items.at("tracestate").find("ot=") != std::string::npos); } - auto span = tracer.create_span(); + Span span = tracer.create_span(); MockDictWriter writer; span.inject(writer); REQUIRE(writer.items.at("tracestate").find("ot=") == std::string::npos); diff --git a/test/test_tracer.cpp b/test/test_tracer.cpp index 2d8cd1764..16e3db3bf 100644 --- a/test/test_tracer.cpp +++ b/test/test_tracer.cpp @@ -1729,7 +1729,7 @@ TEST_TRACER("restart extraction link uses metadata from the selected context") { TEST_TRACER("OpenTelemetry tracestate sampling values") { SECTION("malformed sampling values are removed") { - const auto normalized = + const Optional normalized = sanitize_otel_tracestate("rv:1234567890abcd;th:ABC;future:value"); REQUIRE(normalized); REQUIRE(*normalized == "rv:1234567890abcd;future:value"); @@ -1738,13 +1738,13 @@ TEST_TRACER("OpenTelemetry tracestate sampling values") { } SECTION("sampling values are replaced without altering other values") { - const auto rewritten = rewrite_otel_tracestate("rv:bad;future:value;th:bad", - UINT64_C(0xf0948a54d43b8e), - UINT64_C(0xe6666666666668)); + const Optional rewritten = rewrite_otel_tracestate( + "rv:bad;future:value;th:bad", UINT64_C(0xf0948a54d43b8e), + UINT64_C(0xe6666666666668)); REQUIRE(rewritten); REQUIRE(*rewritten == "rv:f0948a54d43b8e;th:e6666666666668;future:value"); - const auto no_threshold = + const Optional no_threshold = rewrite_otel_tracestate("rv:1234567890abcd;th:e6666666666668", UINT64_C(0x1234567890abcd), nullopt); REQUIRE(no_threshold); @@ -1763,7 +1763,8 @@ TEST_TRACER("OpenTelemetry tracestate sampling values") { std::unordered_map span_tags; MockLogger logger; - const auto extracted = extract_w3c(reader, span_tags, logger); + const Expected extracted = + extract_w3c(reader, span_tags, logger); REQUIRE(extracted); REQUIRE(extracted->otel_w3c_tracestate == "rv:1234567890abcd;th:e6666666666668;future:value"); From f489cbff44f311d74afb7eacb930096e5c60f716 Mon Sep 17 00:00:00 2001 From: Milan Garnier Date: Tue, 18 Aug 2026 15:54:47 +0200 Subject: [PATCH 06/16] refactor(propagation): split tracestate parsing --- src/datadog/w3c_propagation.cpp | 57 ++++++++++++++++++--------------- 1 file changed, 31 insertions(+), 26 deletions(-) diff --git a/src/datadog/w3c_propagation.cpp b/src/datadog/w3c_propagation.cpp index c4fab4273..545a32972 100644 --- a/src/datadog/w3c_propagation.cpp +++ b/src/datadog/w3c_propagation.cpp @@ -88,9 +88,9 @@ void for_each_otel_item(StringView raw, Function&& function) { const StringView item = raw.substr(begin, end - begin); const std::size_t separator = item.find(':'); const StringView key = item.substr(0, separator); - const StringView value = - separator == StringView::npos ? StringView{} - : item.substr(separator + 1); + const StringView value = separator == StringView::npos + ? StringView{} + : item.substr(separator + 1); function(item, key, value); if (end == StringView::npos) { return; @@ -314,6 +314,31 @@ void parse_ot_tracestate(ExtractedData& result, StringView ot_tracestate) { } } +void parse_w3c_tracestate_member( + ExtractedData& result, StringView member, + std::unordered_map& span_tags, + std::string& other_w3c_tracestate) { + const std::size_t separator = member.find('='); + const StringView key = member.substr(0, separator); + const StringView member_value = separator == StringView::npos + ? StringView{} + : member.substr(separator + 1); + if (key == "dd") { + if (member_value.size() > max_datadog_tracestate_value_size) { + span_tags[tags::internal::propagation_error] = "extract_max_size"; + } else { + parse_datadog_trace_state(result, member_value); + } + } else if (key == "ot") { + parse_ot_tracestate(result, member_value); + } else { + if (!other_w3c_tracestate.empty()) { + other_w3c_tracestate += ','; + } + append(other_w3c_tracestate, member); + } +} + // Fill the specified `result` with information parsed from the specified W3C // `tracestate`. // @@ -327,29 +352,9 @@ void parse_w3c_tracestate( std::size_t begin = 0; while (begin < w3c_tracestate.size()) { const std::size_t end = w3c_tracestate.find(',', begin); - const StringView member = - trim(w3c_tracestate.substr(begin, end - begin)); - const std::size_t separator = member.find('='); - const StringView key = member.substr(0, separator); - const StringView member_value = - separator == StringView::npos ? StringView{} - : member.substr(separator + 1); - if (key == "dd") { - // If the "dd" vendor entry's value exceeds the maximum size, drop it - // and record a propagation error tag. - if (member_value.size() > max_datadog_tracestate_value_size) { - span_tags[tags::internal::propagation_error] = "extract_max_size"; - } else { - parse_datadog_trace_state(result, member_value); - } - } else if (key == "ot") { - parse_ot_tracestate(result, member_value); - } else { - if (!other_w3c_tracestate.empty()) { - other_w3c_tracestate += ','; - } - append(other_w3c_tracestate, member); - } + const StringView member = trim(w3c_tracestate.substr(begin, end - begin)); + parse_w3c_tracestate_member(result, member, span_tags, + other_w3c_tracestate); if (end == StringView::npos) { break; From a368c3b7ba88f0873e86ee53d40f24c9c0d42e54 Mon Sep 17 00:00:00 2001 From: Milan Garnier Date: Wed, 19 Aug 2026 16:00:50 +0200 Subject: [PATCH 07/16] refactor(sampling): move probability mechanism helper --- src/datadog/sampling_util.h | 17 +++++++++++++++++ src/datadog/trace_segment.cpp | 16 ---------------- 2 files changed, 17 insertions(+), 16 deletions(-) diff --git a/src/datadog/sampling_util.h b/src/datadog/sampling_util.h index 19f424f45..3d6500c65 100644 --- a/src/datadog/sampling_util.h +++ b/src/datadog/sampling_util.h @@ -4,6 +4,7 @@ // `TraceSampler` and `SpanSampler`. #include +#include #include #include @@ -11,6 +12,22 @@ namespace datadog { namespace tracing { +inline bool is_probability_mechanism(int mechanism) { + switch (static_cast(mechanism)) { + case SamplingMechanism::DEFAULT: + case SamplingMechanism::AGENT_RATE: + case SamplingMechanism::REMOTE_RATE_AUTO: + case SamplingMechanism::RULE: + case SamplingMechanism::REMOTE_RATE_USER_DEFINED: + case SamplingMechanism::REMOTE_RATE_EMERGENCY: + case SamplingMechanism::REMOTE_RULE: + case SamplingMechanism::REMOTE_ADAPTIVE_RULE: + return true; + default: + return false; + } +} + // Return a hash value for the specified `value`. `value` is one of the // following: // diff --git a/src/datadog/trace_segment.cpp b/src/datadog/trace_segment.cpp index b0ca4a335..42cf216ee 100644 --- a/src/datadog/trace_segment.cpp +++ b/src/datadog/trace_segment.cpp @@ -153,22 +153,6 @@ Optional format_rate(double rate, Logger& logger) { return std::string(begin, end); } -bool is_probability_mechanism(int mechanism) { - switch (static_cast(mechanism)) { - case SamplingMechanism::DEFAULT: - case SamplingMechanism::AGENT_RATE: - case SamplingMechanism::REMOTE_RATE_AUTO: - case SamplingMechanism::RULE: - case SamplingMechanism::REMOTE_RATE_USER_DEFINED: - case SamplingMechanism::REMOTE_RATE_EMERGENCY: - case SamplingMechanism::REMOTE_RULE: - case SamplingMechanism::REMOTE_ADAPTIVE_RULE: - return true; - default: - return false; - } -} - Optional resolve_otel_tracestate( TraceID trace_id, const SamplingDecision& decision, const Optional& inherited) { From 62fdc87a85ce10f0bc5768daa533fb4200bbb2b9 Mon Sep 17 00:00:00 2001 From: Milan Garnier Date: Wed, 19 Aug 2026 17:25:14 +0200 Subject: [PATCH 08/16] refactor(sampling): clarify probability sampling result --- include/datadog/sampling_decision.h | 5 +++-- src/datadog/trace_sampler.cpp | 11 ++++++----- src/datadog/trace_segment.cpp | 8 ++++---- 3 files changed, 13 insertions(+), 11 deletions(-) diff --git a/include/datadog/sampling_decision.h b/include/datadog/sampling_decision.h index 8bb87f433..fa79d548e 100644 --- a/include/datadog/sampling_decision.h +++ b/include/datadog/sampling_decision.h @@ -39,8 +39,9 @@ struct SamplingDecision { // The per-second maximum allowed number of "keeps" configured for the limiter // consulted in this decision, if any. Optional limiter_max_per_second; - // The outcome of the probability comparison before rate limiting, if any. - Optional probability_sampled; + // Whether the sample rate alone, before the rate limiter, would keep this + // trace. + Optional was_probability_sampled; // The provenance of this decision. Origin origin; }; diff --git a/src/datadog/trace_sampler.cpp b/src/datadog/trace_sampler.cpp index 6ed1fada9..9784cde58 100644 --- a/src/datadog/trace_sampler.cpp +++ b/src/datadog/trace_sampler.cpp @@ -50,9 +50,10 @@ SamplingDecision TraceSampler::decide(const SpanData& span) { decision.mechanism = int(rule.mechanism); decision.limiter_max_per_second = limiter_max_per_second_; decision.configured_rate = rule.rate; - const std::uint64_t threshold = max_id_from_rate(rule.rate); - decision.probability_sampled = knuth_hash(span.trace_id.low) <= threshold; - if (*decision.probability_sampled) { + const std::uint64_t threshold = max_id_from_rate(*decision.configured_rate); + decision.was_probability_sampled = + knuth_hash(span.trace_id.low) <= threshold; + if (*decision.was_probability_sampled) { if (rule.bypass_limiter) { decision.priority = int(SamplingPriority::USER_KEEP); return decision; @@ -92,8 +93,8 @@ SamplingDecision TraceSampler::decide(const SpanData& span) { } const std::uint64_t threshold = max_id_from_rate(*decision.configured_rate); - decision.probability_sampled = knuth_hash(span.trace_id.low) <= threshold; - if (*decision.probability_sampled) { + decision.was_probability_sampled = knuth_hash(span.trace_id.low) <= threshold; + if (*decision.was_probability_sampled) { decision.priority = int(SamplingPriority::AUTO_KEEP); } else { decision.priority = int(SamplingPriority::AUTO_DROP); diff --git a/src/datadog/trace_segment.cpp b/src/datadog/trace_segment.cpp index 42cf216ee..354891ffa 100644 --- a/src/datadog/trace_segment.cpp +++ b/src/datadog/trace_segment.cpp @@ -162,9 +162,9 @@ Optional resolve_otel_tracestate( } if (!decision.mechanism || !decision.configured_rate || - !decision.probability_sampled || + !decision.was_probability_sampled || !is_probability_mechanism(*decision.mechanism) || - (*decision.probability_sampled && decision.priority <= 0)) { + (*decision.was_probability_sampled && decision.priority <= 0)) { return rewrite_otel_tracestate(raw, extract_otel_random_value(raw), nullopt); } @@ -176,9 +176,9 @@ Optional resolve_otel_tracestate( threshold = std::min(threshold, max_value - 1); std::uint64_t random_value = (~knuth_hash(trace_id.low)) >> 8; - if (*decision.probability_sampled && random_value < threshold) { + if (*decision.was_probability_sampled && random_value < threshold) { random_value = threshold; - } else if (!*decision.probability_sampled && random_value >= threshold) { + } else if (!*decision.was_probability_sampled && random_value >= threshold) { random_value = threshold == 0 ? 0 : threshold - 1; } From 21e3a87eefc1bc4ea4ebe6f4f201f6db85f04428 Mon Sep 17 00:00:00 2001 From: Milan Garnier Date: Wed, 19 Aug 2026 17:40:01 +0200 Subject: [PATCH 09/16] refactor(propagation): rename hex digit predicate --- src/datadog/w3c_propagation.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/datadog/w3c_propagation.cpp b/src/datadog/w3c_propagation.cpp index 545a32972..cdd4a1f21 100644 --- a/src/datadog/w3c_propagation.cpp +++ b/src/datadog/w3c_propagation.cpp @@ -33,7 +33,7 @@ auto verboten(int lowest_ascii, int highest_ascii, }; } -constexpr bool is_hexdiglc(const char c) { +constexpr bool is_hexdig(const char c) { return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'); } @@ -155,7 +155,7 @@ Optional extract_traceparent(ExtractedData& result, beg = i + 1; internal_state = state::trace_id; - } else if (!is_hexdiglc(traceparent[i])) { + } else if (!is_hexdig(traceparent[i])) { return "invalid_version"; } } break; From 2deb5ae4e60e93a36f834a622c7dd849148439b8 Mon Sep 17 00:00:00 2001 From: Milan Garnier Date: Thu, 20 Aug 2026 12:04:19 +0200 Subject: [PATCH 10/16] refactor(propagation): consolidate tracestate helpers --- src/datadog/w3c_propagation.cpp | 72 +++++++++++++++------------------ 1 file changed, 33 insertions(+), 39 deletions(-) diff --git a/src/datadog/w3c_propagation.cpp b/src/datadog/w3c_propagation.cpp index cdd4a1f21..714e011cc 100644 --- a/src/datadog/w3c_propagation.cpp +++ b/src/datadog/w3c_propagation.cpp @@ -48,15 +48,11 @@ constexpr std::size_t max_w3c_tracestate_member_value_size = 256; constexpr std::size_t max_datadog_tracestate_value_size = 512; constexpr std::size_t max_w3c_tracestate_members = 32; -bool is_valid_otel_hex(StringView value, std::size_t minimum_size, - std::size_t maximum_size) { - return value.size() >= minimum_size && value.size() <= maximum_size && - std::all_of(value.begin(), value.end(), is_lowercase_hexdig); -} - -Optional parse_otel_random_value(StringView value) { - if (!is_valid_otel_hex(value, otel_sampling_value_size, - otel_sampling_value_size)) { +Optional parse_otel_value(StringView value, + std::size_t minimum_size, + std::size_t maximum_size) { + if (value.size() < minimum_size || value.size() > maximum_size || + !std::all_of(value.begin(), value.end(), is_lowercase_hexdig)) { return nullopt; } @@ -67,23 +63,20 @@ Optional parse_otel_random_value(StringView value) { return *parsed; } -Optional parse_otel_threshold(StringView value) { - if (!is_valid_otel_hex(value, min_otel_threshold_size, - otel_sampling_value_size)) { - return nullopt; - } +Optional parse_otel_random_value(StringView value) { + return parse_otel_value(value, otel_sampling_value_size, + otel_sampling_value_size); +} - const Expected parsed = parse_uint64(value, 16); - if (parsed.if_error()) { - return nullopt; - } - return *parsed; +Optional parse_otel_threshold(StringView value) { + return parse_otel_value(value, min_otel_threshold_size, + otel_sampling_value_size); } template void for_each_otel_item(StringView raw, Function&& function) { std::size_t begin = 0; - while (begin <= raw.size()) { + while (begin < raw.size()) { const std::size_t end = raw.find(';', begin); const StringView item = raw.substr(begin, end - begin); const std::size_t separator = item.find(':'); @@ -99,6 +92,20 @@ void for_each_otel_item(StringView raw, Function&& function) { } } +template +void for_each_w3c_tracestate_member(StringView tracestate, + Function&& function) { + std::size_t begin = 0; + while (begin < tracestate.size()) { + const std::size_t end = tracestate.find(',', begin); + const StringView member = trim(tracestate.substr(begin, end - begin)); + if (!function(member) || end == StringView::npos) { + return; + } + begin = end + 1; + } +} + bool append_otel_item(std::string& result, StringView item) { if (item.empty()) { return true; @@ -349,18 +356,11 @@ void parse_w3c_tracestate( ExtractedData& result, StringView w3c_tracestate, std::unordered_map& span_tags) { std::string other_w3c_tracestate; - std::size_t begin = 0; - while (begin < w3c_tracestate.size()) { - const std::size_t end = w3c_tracestate.find(',', begin); - const StringView member = trim(w3c_tracestate.substr(begin, end - begin)); + for_each_w3c_tracestate_member(w3c_tracestate, [&](StringView member) { parse_w3c_tracestate_member(result, member, span_tags, other_w3c_tracestate); - - if (end == StringView::npos) { - break; - } - begin = end + 1; - } + return true; + }); if (!other_w3c_tracestate.empty()) { result.additional_w3c_tracestate = std::move(other_w3c_tracestate); @@ -535,20 +535,14 @@ Optional rewrite_otel_tracestate( void append_tracestate_entries(std::string& result, StringView entries, std::size_t& member_count) { - std::size_t begin = 0; - while (member_count < max_w3c_tracestate_members && begin < entries.size()) { - const std::size_t end = entries.find(',', begin); - const StringView entry = trim(entries.substr(begin, end - begin)); + for_each_w3c_tracestate_member(entries, [&](StringView entry) { if (!entry.empty()) { result += ','; append(result, entry); ++member_count; } - if (end == StringView::npos) { - return; - } - begin = end + 1; - } + return member_count < max_w3c_tracestate_members; + }); } std::string encode_tracestate( From 4b9032bd42f0d99e05c6171caed85d617bd52836 Mon Sep 17 00:00:00 2001 From: Milan Garnier Date: Thu, 20 Aug 2026 12:22:44 +0200 Subject: [PATCH 11/16] refactor(propagation): remove unused item append result --- src/datadog/w3c_propagation.cpp | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/datadog/w3c_propagation.cpp b/src/datadog/w3c_propagation.cpp index 714e011cc..cbe1b9a0d 100644 --- a/src/datadog/w3c_propagation.cpp +++ b/src/datadog/w3c_propagation.cpp @@ -106,22 +106,21 @@ void for_each_w3c_tracestate_member(StringView tracestate, } } -bool append_otel_item(std::string& result, StringView item) { +void append_otel_item(std::string& result, StringView item) { if (item.empty()) { - return true; + return; } const std::size_t separator_size = result.empty() ? 0 : 1; if (result.size() + separator_size + item.size() > max_w3c_tracestate_member_value_size) { - return false; + return; } if (separator_size) { result += ';'; } append(result, item); - return true; } std::string format_otel_hex(std::uint64_t value) { From 772c4c1cd88dfb479e5befceabb0eec100fa77e9 Mon Sep 17 00:00:00 2001 From: Milan Garnier Date: Thu, 20 Aug 2026 12:23:49 +0200 Subject: [PATCH 12/16] refactor(propagation): name OpenTelemetry value limits --- src/datadog/w3c_propagation.cpp | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/datadog/w3c_propagation.cpp b/src/datadog/w3c_propagation.cpp index cbe1b9a0d..12095e5c7 100644 --- a/src/datadog/w3c_propagation.cpp +++ b/src/datadog/w3c_propagation.cpp @@ -42,8 +42,9 @@ constexpr bool is_lowercase_hexdig(const char c) { return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f'); } -constexpr std::size_t otel_sampling_value_size = 14; +constexpr std::size_t otel_random_value_size = 14; constexpr std::size_t min_otel_threshold_size = 1; +constexpr std::size_t max_otel_threshold_size = otel_random_value_size; constexpr std::size_t max_w3c_tracestate_member_value_size = 256; constexpr std::size_t max_datadog_tracestate_value_size = 512; constexpr std::size_t max_w3c_tracestate_members = 32; @@ -64,13 +65,13 @@ Optional parse_otel_value(StringView value, } Optional parse_otel_random_value(StringView value) { - return parse_otel_value(value, otel_sampling_value_size, - otel_sampling_value_size); + return parse_otel_value(value, otel_random_value_size, + otel_random_value_size); } Optional parse_otel_threshold(StringView value) { return parse_otel_value(value, min_otel_threshold_size, - otel_sampling_value_size); + max_otel_threshold_size); } template From 0e64820c307c3750c6dffd4bb595950ad7a8851b Mon Sep 17 00:00:00 2001 From: Milan Garnier Date: Thu, 20 Aug 2026 12:25:58 +0200 Subject: [PATCH 13/16] fix(propagation): reject oversized OpenTelemetry tracestate --- src/datadog/w3c_propagation.cpp | 6 +++++- test/test_tracer.cpp | 27 +++++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/src/datadog/w3c_propagation.cpp b/src/datadog/w3c_propagation.cpp index 12095e5c7..0522b20af 100644 --- a/src/datadog/w3c_propagation.cpp +++ b/src/datadog/w3c_propagation.cpp @@ -337,7 +337,11 @@ void parse_w3c_tracestate_member( parse_datadog_trace_state(result, member_value); } } else if (key == "ot") { - parse_ot_tracestate(result, member_value); + if (member_value.size() > max_w3c_tracestate_member_value_size) { + span_tags[tags::internal::propagation_error] = "extract_max_size"; + } else { + parse_ot_tracestate(result, member_value); + } } else { if (!other_w3c_tracestate.empty()) { other_w3c_tracestate += ','; diff --git a/test/test_tracer.cpp b/test/test_tracer.cpp index 16e3db3bf..ccce8b88b 100644 --- a/test/test_tracer.cpp +++ b/test/test_tracer.cpp @@ -1291,6 +1291,33 @@ TEST_TRACER("span extraction") { "extract_max_size"); } + SECTION( + "'extract_max_size' propagation error if tracestate \"ot\" vendor " + "value is oversized on extract") { + constexpr std::size_t max_w3c_tracestate_member_value_size = 256; + const std::string ot_value(max_w3c_tracestate_member_value_size + 1, 'a'); + std::unordered_map span_tags; + MockLogger logger; + CAPTURE(logger.entries); + CAPTURE(span_tags); + + std::unordered_map headers{ + {"traceparent", + "00-00000000000000000000000000000001-0000000000000001-00"}, + {"tracestate", "dd=s:1,ot=" + ot_value + ",vendorx=keepme"}}; + MockDictReader reader{headers}; + + const auto extracted = extract_w3c(reader, span_tags, logger); + REQUIRE(extracted); + REQUIRE(extracted->otel_w3c_tracestate == nullopt); + REQUIRE(extracted->additional_w3c_tracestate == "vendorx=keepme"); + + REQUIRE(logger.entries.empty()); + REQUIRE(span_tags.count(tags::internal::propagation_error) == 1); + REQUIRE(span_tags.at(tags::internal::propagation_error) == + "extract_max_size"); + } + SECTION("W3C Phase 3 support - Preferring tracecontext") { // Tests behavior from system-test // test_headers_tracecontext.py::test_tracestate_w3c_p_extract_datadog_w3c From ccccc525889c96cbfdd6fccbfb900fa40ef6b92a Mon Sep 17 00:00:00 2001 From: Milan Garnier Date: Thu, 20 Aug 2026 12:27:18 +0200 Subject: [PATCH 14/16] test(propagation): cover duplicate OpenTelemetry state --- src/datadog/w3c_propagation.cpp | 3 +-- test/test_tracer.cpp | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/datadog/w3c_propagation.cpp b/src/datadog/w3c_propagation.cpp index 0522b20af..670963c4e 100644 --- a/src/datadog/w3c_propagation.cpp +++ b/src/datadog/w3c_propagation.cpp @@ -313,8 +313,7 @@ void parse_datadog_trace_state(ExtractedData& result, // `ot_tracestate`. `ot_tracestate` is the value of the "ot" entry in the W3C // "tracestate" header. // -// `parse_ot_tracestate` populates `otel_w3c_tracestate` if it has not already -// been set. +// `parse_ot_tracestate` preserves the first OpenTelemetry state in a header. void parse_ot_tracestate(ExtractedData& result, StringView ot_tracestate) { if (!result.otel_w3c_tracestate) { result.otel_w3c_tracestate = std::string(ot_tracestate); diff --git a/test/test_tracer.cpp b/test/test_tracer.cpp index ccce8b88b..4cd26aef6 100644 --- a/test/test_tracer.cpp +++ b/test/test_tracer.cpp @@ -1797,6 +1797,22 @@ TEST_TRACER("OpenTelemetry tracestate sampling values") { "rv:1234567890abcd;th:e6666666666668;future:value"); REQUIRE(extracted->additional_w3c_tracestate == "congo=t61rcWkgMzE"); } + + SECTION("the first OpenTelemetry member is retained") { + const std::unordered_map headers{ + {"traceparent", + "00-00000000000000000000000000000001-0000000000000001-01"}, + {"tracestate", "ot=first,ot=second"}, + }; + MockDictReader reader{headers}; + std::unordered_map span_tags; + MockLogger logger; + + const Expected extracted = + extract_w3c(reader, span_tags, logger); + REQUIRE(extracted); + REQUIRE(extracted->otel_w3c_tracestate == "first"); + } } TEST_TRACER("baggage usage") { From 3f4e4fca1c972a5f1f289ec0433d73ada9bbe804 Mon Sep 17 00:00:00 2001 From: Milan Garnier Date: Thu, 20 Aug 2026 12:28:50 +0200 Subject: [PATCH 15/16] refactor(tracing): use W3C link context alias --- src/datadog/trace_segment.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/datadog/trace_segment.cpp b/src/datadog/trace_segment.cpp index 354891ffa..e1e85c9e2 100644 --- a/src/datadog/trace_segment.cpp +++ b/src/datadog/trace_segment.cpp @@ -250,7 +250,7 @@ Optional TraceSegment::sampling_decision() const { return sampling_decision_; } -Optional> TraceSegment::w3c_link_context( +Optional TraceSegment::w3c_link_context( const SpanData& span) const { SamplingDecision sampling_decision; std::vector> trace_tags; From 6eb3ba3bd6309fe00f3374c189566d92e5593205 Mon Sep 17 00:00:00 2001 From: Milan Garnier Date: Thu, 20 Aug 2026 12:30:04 +0200 Subject: [PATCH 16/16] refactor(propagation): defer inherited tracestate view --- src/datadog/trace_segment.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/datadog/trace_segment.cpp b/src/datadog/trace_segment.cpp index e1e85c9e2..5c044fd51 100644 --- a/src/datadog/trace_segment.cpp +++ b/src/datadog/trace_segment.cpp @@ -156,11 +156,12 @@ Optional format_rate(double rate, Logger& logger) { Optional resolve_otel_tracestate( TraceID trace_id, const SamplingDecision& decision, const Optional& inherited) { - const StringView raw = inherited ? StringView(*inherited) : StringView{}; if (decision.origin != SamplingDecision::Origin::LOCAL) { - return inherited ? sanitize_otel_tracestate(raw) : nullopt; + return inherited ? sanitize_otel_tracestate(StringView(*inherited)) + : nullopt; } + const StringView raw = inherited ? StringView(*inherited) : StringView{}; if (!decision.mechanism || !decision.configured_rate || !decision.was_probability_sampled || !is_probability_mechanism(*decision.mechanism) ||