From 3c6394ed166a15f65ac7ee0a424a57a093fe3614 Mon Sep 17 00:00:00 2001 From: Milan Garnier Date: Fri, 14 Aug 2026 14:21:04 +0200 Subject: [PATCH] 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();