diff --git a/tracer/src/Datadog.Trace/Propagators/OtelTraceStateHelpers.cs b/tracer/src/Datadog.Trace/Propagators/OtelTraceStateHelpers.cs
new file mode 100644
index 000000000000..d8da68381d93
--- /dev/null
+++ b/tracer/src/Datadog.Trace/Propagators/OtelTraceStateHelpers.cs
@@ -0,0 +1,173 @@
+//
+// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License.
+// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc.
+//
+
+#nullable enable
+
+using System;
+using System.Collections.Generic;
+using Datadog.Trace.Util;
+
+namespace Datadog.Trace.Propagators
+{
+ ///
+ /// String-surgery helpers over the raw content of the W3C tracestate "ot=" list-member
+ /// (OpenTelemetry consistent-probability-sampling sub-keys "rv"/"th"). The value is never
+ /// decoded into a typed struct: these two helpers are the only code that inspects or
+ /// rewrites the "rv"/"th" sub-keys; every other sub-key (recognized or not) round-trips
+ /// byte-for-byte through in its original order.
+ ///
+ internal static class OtelTraceStateHelpers
+ {
+ private const int MaxRvHexDigits = 14;
+
+ ///
+ /// Finds the "rv" item in the raw "ot=" value (items separated by ';', key/value by ':')
+ /// and returns its value parsed as 1-14 lowercase hex digits, or null if absent or malformed.
+ /// Never throws.
+ ///
+ internal static ulong? ExtractRv(string? raw)
+ {
+ if (StringUtil.IsNullOrEmpty(raw))
+ {
+ return null;
+ }
+
+ foreach (var item in raw.Split(';'))
+ {
+ var colonIndex = item.IndexOf(':');
+
+ if (colonIndex <= 0 || colonIndex == item.Length - 1)
+ {
+ continue;
+ }
+
+ var key = item.Substring(0, colonIndex);
+
+ if (key != "rv")
+ {
+ continue;
+ }
+
+ var value = item.Substring(colonIndex + 1);
+ return TryParseLowercaseHex(value, MaxRvHexDigits, out var rv) ? rv : null;
+ }
+
+ return null;
+ }
+
+ ///
+ /// Drops any existing "rv"/"th" items from (whether well-formed
+ /// or not), then emits "rv:<14-hex-digits>" (if is non-null)
+ /// followed by "th:<hex, trailing zero nibbles trimmed>" (if
+ /// is non-null), followed by every other item from in its original
+ /// order. Returns null when nothing is left to emit.
+ ///
+ internal static string? SetRvTh(string? raw, ulong? rv, ulong? th)
+ {
+ List? otherItems = null;
+
+ if (!StringUtil.IsNullOrEmpty(raw))
+ {
+ foreach (var item in raw!.Split(';'))
+ {
+ var colonIndex = item.IndexOf(':');
+ var key = colonIndex > 0 ? item.Substring(0, colonIndex) : item;
+
+ if (key is "rv" or "th")
+ {
+ continue;
+ }
+
+ (otherItems ??= new List()).Add(item);
+ }
+ }
+
+ if (rv is null && th is null && otherItems is null)
+ {
+ return null;
+ }
+
+ var sb = StringBuilderCache.Acquire();
+
+ try
+ {
+ if (rv is { } rvValue)
+ {
+ sb.Append("rv:").Append(rvValue.ToString("x14"));
+ }
+
+ if (th is { } thValue)
+ {
+ if (sb.Length > 0)
+ {
+ sb.Append(';');
+ }
+
+ sb.Append("th:").Append(FormatThresholdHex(thValue));
+ }
+
+ if (otherItems is not null)
+ {
+ foreach (var item in otherItems)
+ {
+ if (sb.Length > 0)
+ {
+ sb.Append(';');
+ }
+
+ sb.Append(item);
+ }
+ }
+
+ return sb.Length == 0 ? null : StringBuilderCache.GetStringAndRelease(sb);
+ }
+ finally
+ {
+ StringBuilderCache.Release(sb);
+ }
+ }
+
+ private static string FormatThresholdHex(ulong th)
+ {
+ // Format as hex (up to 14 hex digits for a 56-bit value), then trim trailing zero nibbles.
+ // A fully-zero threshold trims to the empty string; represent it as a single "0".
+ var hex = th.ToString("x");
+ var trimmed = hex.TrimEnd('0');
+ return trimmed.Length == 0 ? "0" : trimmed;
+ }
+
+ private static bool TryParseLowercaseHex(string value, int maxDigits, out ulong result)
+ {
+ result = 0;
+
+ if (value.Length == 0 || value.Length > maxDigits)
+ {
+ return false;
+ }
+
+ foreach (var c in value)
+ {
+ int digit;
+
+ if (c is >= '0' and <= '9')
+ {
+ digit = c - '0';
+ }
+ else if (c is >= 'a' and <= 'f')
+ {
+ digit = c - 'a' + 10;
+ }
+ else
+ {
+ return false;
+ }
+
+ result = (result << 4) | (uint)digit;
+ }
+
+ return true;
+ }
+ }
+}
diff --git a/tracer/src/Datadog.Trace/Propagators/W3CTraceContextPropagator.cs b/tracer/src/Datadog.Trace/Propagators/W3CTraceContextPropagator.cs
index f9e6eabedda1..d8f04411d13a 100644
--- a/tracer/src/Datadog.Trace/Propagators/W3CTraceContextPropagator.cs
+++ b/tracer/src/Datadog.Trace/Propagators/W3CTraceContextPropagator.cs
@@ -199,6 +199,21 @@ internal static string CreateTraceStateHeader(SpanContext context)
sb.Length--;
}
+ // OTel consistent-probability-sampling sub-keys ("ot=rv:...;th:..."), placed
+ // immediately after "dd=" so both survive right-side truncation of a crowded
+ // tracestate (W3C permits dropping members past 32).
+ var otelTraceState = context.OtelTraceState;
+
+ if (!string.IsNullOrWhiteSpace(otelTraceState))
+ {
+ if (sb.Length > 0)
+ {
+ sb.Append(TraceStateHeaderValuesSeparator);
+ }
+
+ sb.Append("ot=").Append(otelTraceState);
+ }
+
var additionalState = context.AdditionalW3CTraceState;
if (!string.IsNullOrWhiteSpace(additionalState))
@@ -311,17 +326,17 @@ internal static W3CTraceState ParseTraceState(string? header)
// header format: "[*,]dd=s:1;o:rum;t.dm:-4;t.usr.id:12345[,*]"
if (string.IsNullOrWhiteSpace(header))
{
- return new W3CTraceState(samplingPriority: null, origin: null, lastParent: ZeroLastParent, propagatedTags: null, additionalValues: null);
+ return new W3CTraceState(samplingPriority: null, origin: null, lastParent: ZeroLastParent, propagatedTags: null, additionalValues: null, otTraceState: null);
}
- SplitTraceStateValues(header!, out var ddValues, out var additionalValues);
+ SplitTraceStateValues(header!, out var ddValues, out var otTraceState, out var additionalValues);
- if (ddValues is null or { Length: < 6 })
+ if (ddValues is null or { Length: < 3 })
{
// "dd" section not found or it is too short
- // shortest valid length is 6 as in "dd=a:b"
+ // shortest valid length is 3 as in "a:b" ("dd=" prefix already stripped)
// note for this case the p will be viewed as 0 if added as a span tag
- return new W3CTraceState(samplingPriority: null, origin: null, lastParent: ZeroLastParent, propagatedTags: null, additionalValues);
+ return new W3CTraceState(samplingPriority: null, origin: null, lastParent: ZeroLastParent, propagatedTags: null, additionalValues, otTraceState);
}
int? samplingPriority = null;
@@ -331,8 +346,7 @@ internal static W3CTraceState ParseTraceState(string? header)
try
{
- // skip "dd="
- var startIndex = 3;
+ var startIndex = 0;
// name1:value1;
// ^ endIndex
@@ -449,7 +463,7 @@ internal static W3CTraceState ParseTraceState(string? header)
lastParent ??= ZeroLastParent;
- return new W3CTraceState(samplingPriority, origin, lastParent, propagatedTags, additionalValues);
+ return new W3CTraceState(samplingPriority, origin, lastParent, propagatedTags, additionalValues, otTraceState);
}
finally
{
@@ -457,91 +471,87 @@ internal static W3CTraceState ParseTraceState(string? header)
}
}
- internal static void SplitTraceStateValues(string header, out string? ddValues, out string? additionalValues)
+ internal static void SplitTraceStateValues(string header, out string? ddValues, out string? otValues, out string? additionalValues)
{
- // header format: "[*,]dd=s:1;o:rum;t.dm:-4;t.usr.id:12345[,*]"
-
+ // header format: "[*,]dd=s:1;o:rum;t.dm:-4;t.usr.id:12345[,ot=rv:...;th:...][,*]"
if (string.IsNullOrWhiteSpace(header))
{
ddValues = null;
+ otValues = null;
additionalValues = null;
return;
}
header = header.Trim();
- int ddStartIndex;
- if (header.StartsWith("dd=", StringComparison.Ordinal))
+ ExtractMember(header, "dd=", out ddValues, out var afterDd);
+ ExtractMember(afterDd, "ot=", out otValues, out var afterOt);
+ additionalValues = string.IsNullOrEmpty(afterOt) ? null : afterOt;
+ }
+
+ // Finds the list-member with the given "=" prefix anywhere in a comma-separated
+ // tracestate value, returns its value (without the prefix) via "value", and returns
+ // every other member (still comma-separated, original order preserved) via "remainder".
+ private static void ExtractMember(string? header, string prefix, out string? value, out string? remainder)
+ {
+ if (string.IsNullOrEmpty(header))
{
- ddStartIndex = 0;
+ value = null;
+ remainder = header;
+ return;
+ }
+
+ int startIndex;
+
+ if (header!.StartsWith(prefix, StringComparison.Ordinal))
+ {
+ startIndex = 0;
}
else
{
- // if "dd=" is not at start of header, make sure we find the one preceded by comma
- // in case there is something like "key1=valuedd=whatisthis,dd=..."
- // ^ take this one
- // ^ ignore this one
- ddStartIndex = header.IndexOf(",dd=", StringComparison.Ordinal);
+ startIndex = header.IndexOf("," + prefix, StringComparison.Ordinal);
- if (ddStartIndex >= 0)
+ if (startIndex >= 0)
{
- // if ",dd=" was found, skip the ','
- ddStartIndex++;
+ startIndex++; // skip the ','
}
}
- if (ddStartIndex < 0)
+ if (startIndex < 0)
{
- // "dd=" was not found in header, the entire header is "additional values"
- // example tracestate: "foo=bar"
- // ^^^^^^^
- ddValues = null;
- additionalValues = header;
+ value = null;
+ remainder = header;
return;
}
- // search for end of "dd="
- var ddEndIndex = header.IndexOf(TraceStateHeaderValuesSeparator, ddStartIndex + 3);
+ var endIndex = header.IndexOf(TraceStateHeaderValuesSeparator, startIndex + prefix.Length);
- if (ddEndIndex < 0)
+ if (endIndex < 0)
{
- // "dd=" reaches the end of header
- ddEndIndex = header.Length;
+ endIndex = header.Length;
}
- ddValues = header.Substring(ddStartIndex, ddEndIndex - ddStartIndex);
+ value = header.Substring(startIndex + prefix.Length, endIndex - startIndex - prefix.Length);
- if (ddStartIndex == 0 && ddEndIndex == header.Length)
+ if (startIndex == 0 && endIndex == header.Length)
{
- // "dd" was the only key, no additional values
- // example tracestate: "dd=s:1;o:rum"
- additionalValues = null;
+ remainder = null;
}
- else if (ddStartIndex == 0)
+ else if (startIndex == 0)
{
- // "dd" first, additional values later
- // example tracestate: "dd=s:1;o:rum,foo=bar"
- // ^^^^^^^
- additionalValues = header.Substring(ddEndIndex + 1, header.Length - ddEndIndex - 1);
+ remainder = header.Substring(endIndex + 1, header.Length - endIndex - 1);
}
- else if (ddEndIndex == header.Length)
+ else if (endIndex == header.Length)
{
- // additional values first, "dd" later
- // example tracestate: "foo=bar,dd=s:1;o:rum"
- // ^^^^^^^
- additionalValues = header.Substring(0, ddStartIndex - 1);
+ remainder = header.Substring(0, startIndex - 1);
}
else
{
- // additional values on both sides, "dd" in the middle
- // example tracestate: "foo1=bar1,dd=s:1;o:rum,foo2=bar2" => "foo1=bar1,foo2=bar2"
- // ^^^^^^^^^ ^^^^^^^^^
- var otherValuesLeft = header.Substring(0, ddStartIndex - 1);
- var otherValuesRight = header.Substring(ddEndIndex + 1, header.Length - ddEndIndex - 1);
-
- var sb = StringBuilderCache.Acquire(otherValuesLeft.Length + otherValuesRight.Length + 1);
- sb.Append(otherValuesLeft).Append(TraceStateHeaderValuesSeparator).Append(otherValuesRight);
- additionalValues = StringBuilderCache.GetStringAndRelease(sb);
+ var left = header.Substring(0, startIndex - 1);
+ var right = header.Substring(endIndex + 1, header.Length - endIndex - 1);
+ var sb = StringBuilderCache.Acquire(left.Length + right.Length + 1);
+ sb.Append(left).Append(TraceStateHeaderValuesSeparator).Append(right);
+ remainder = StringBuilderCache.GetStringAndRelease(sb);
}
}
@@ -631,6 +641,7 @@ public bool TryExtract(
spanContext.PropagatedTags = traceTags;
spanContext.AdditionalW3CTraceState = traceState.AdditionalValues;
+ spanContext.OtelTraceState = traceState.OtTraceState;
spanContext.LastParentId = traceState.LastParent;
context = new PropagationContext(spanContext, baggage: null);
diff --git a/tracer/src/Datadog.Trace/Propagators/W3CTraceState.cs b/tracer/src/Datadog.Trace/Propagators/W3CTraceState.cs
index 1f7d4436f57e..a4fffdcd508f 100644
--- a/tracer/src/Datadog.Trace/Propagators/W3CTraceState.cs
+++ b/tracer/src/Datadog.Trace/Propagators/W3CTraceState.cs
@@ -18,15 +18,22 @@ internal readonly struct W3CTraceState
// format is "_dd.p.key1:value1;_dd.p.key2:value2"
public readonly string? PropagatedTags;
- // the string left in "tracestate" after removing "dd=*"
+ // the string left in "tracestate" after removing "dd=*" and "ot=*"
public readonly string? AdditionalValues;
- public W3CTraceState(int? samplingPriority, string? origin, string? lastParent, string? propagatedTags, string? additionalValues)
+ ///
+ /// Raw content of the inbound "ot=" tracestate list-member (no "ot=" prefix),
+ /// captured verbatim with no sub-key parsing. Null if no "ot=" member was present.
+ ///
+ public readonly string? OtTraceState;
+
+ public W3CTraceState(int? samplingPriority, string? origin, string? lastParent, string? propagatedTags, string? additionalValues, string? otTraceState = null)
{
SamplingPriority = samplingPriority;
Origin = origin;
LastParent = lastParent;
PropagatedTags = propagatedTags;
AdditionalValues = additionalValues;
+ OtTraceState = otTraceState;
}
}
diff --git a/tracer/src/Datadog.Trace/Sampling/SamplingDecision.cs b/tracer/src/Datadog.Trace/Sampling/SamplingDecision.cs
index 0f55b455ba43..277600710753 100644
--- a/tracer/src/Datadog.Trace/Sampling/SamplingDecision.cs
+++ b/tracer/src/Datadog.Trace/Sampling/SamplingDecision.cs
@@ -18,7 +18,8 @@ internal readonly struct SamplingDecision
priority: SamplingPriorityValues.Default,
mechanism: SamplingMechanism.Default,
rate: null,
- limiterRate: null);
+ limiterRate: null,
+ sample: null);
public readonly int Priority;
@@ -28,12 +29,21 @@ internal readonly struct SamplingDecision
public readonly float? LimiterRate;
- public SamplingDecision(int priority, string? mechanism, float? rate, float? limiterRate)
+ ///
+ /// The raw probability keep/drop outcome (before any rate-limiter demotion), or null
+ /// when no probability mechanism made this decision (e.g. ).
+ /// Used only to derive the OTel "ot.rv"/"ot.th" tracestate values in
+ /// — never affects .
+ ///
+ public readonly bool? Sample;
+
+ public SamplingDecision(int priority, string? mechanism, float? rate, float? limiterRate, bool? sample = null)
{
Priority = priority;
Mechanism = mechanism;
Rate = rate;
LimiterRate = limiterRate;
+ Sample = sample;
}
public void Deconstruct(out int priority, out string? mechanism, out float? rate, out float? limiterRate)
diff --git a/tracer/src/Datadog.Trace/Sampling/TraceSampler.cs b/tracer/src/Datadog.Trace/Sampling/TraceSampler.cs
index 269a3c719232..cf74da6a5a81 100644
--- a/tracer/src/Datadog.Trace/Sampling/TraceSampler.cs
+++ b/tracer/src/Datadog.Trace/Sampling/TraceSampler.cs
@@ -97,7 +97,7 @@ private SamplingDecision MakeSamplingDecision(Span span, float rate, string mech
}
}
- return new SamplingDecision(priority, mechanism, rate, limiterRate);
+ return new SamplingDecision(priority, mechanism, rate, limiterRate, sample);
}
public sealed class Builder(IRateLimiter limiter)
diff --git a/tracer/src/Datadog.Trace/SpanContext.cs b/tracer/src/Datadog.Trace/SpanContext.cs
index 0eb636bd7800..cb42dba0274b 100644
--- a/tracer/src/Datadog.Trace/SpanContext.cs
+++ b/tracer/src/Datadog.Trace/SpanContext.cs
@@ -58,6 +58,9 @@ public sealed partial class SpanContext : ISpanContext, IReadOnlyDictionary
/// Initializes a new instance of the class
@@ -287,6 +290,27 @@ internal string AdditionalW3CTraceState
}
}
+ ///
+ /// Gets or sets the raw content of the inbound "ot=" W3C tracestate member
+ /// (OpenTelemetry consistent-probability-sampling sub-keys). Null if none was
+ /// present on extraction and nothing has derived one locally.
+ ///
+#nullable enable
+ internal string? OtelTraceState
+ {
+ get => TraceContext?.OtelTraceState ?? _otelTraceState;
+ set
+ {
+ _otelTraceState = value;
+
+ if (TraceContext is not null)
+ {
+ TraceContext.OtelTraceState = value;
+ }
+ }
+ }
+#nullable restore
+
///
/// Gets or sets the last span ID of the most recently seen Datadog span that will be propagated downstream
/// to allow for the re-parenting of spans in cases where spans in distributed traces have missing spans.
diff --git a/tracer/src/Datadog.Trace/TraceContext.cs b/tracer/src/Datadog.Trace/TraceContext.cs
index 71ee785e2cc9..e3e936b5526d 100644
--- a/tracer/src/Datadog.Trace/TraceContext.cs
+++ b/tracer/src/Datadog.Trace/TraceContext.cs
@@ -20,6 +20,7 @@
using Datadog.Trace.FeatureFlags;
using Datadog.Trace.Iast;
using Datadog.Trace.Logging;
+using Datadog.Trace.Propagators;
using Datadog.Trace.Sampling;
using Datadog.Trace.SourceGenerators;
using Datadog.Trace.Tagging;
@@ -112,6 +113,15 @@ public Span? RootSpan
///
internal string? AdditionalW3CTraceState { get; set; }
+ ///
+ /// Gets or sets the raw content of the inbound/rewritten W3C tracestate "ot=" member
+ /// (OpenTelemetry consistent-probability-sampling sub-keys), with no "ot=" prefix.
+ /// Null means there is nothing to emit. Never decoded into typed fields — see
+ /// for the only code that inspects
+ /// or rewrites its "rv"/"th" sub-keys.
+ ///
+ internal string? OtelTraceState { get; set; }
+
/// Gets the IAST context
internal IastRequestContext? IastRequestContext => _iastRequestContext;
@@ -318,7 +328,8 @@ public int GetOrMakeSamplingDecision(Span? span)
samplingDecision.Priority,
samplingDecision.Mechanism,
samplingDecision.Rate,
- samplingDecision.LimiterRate);
+ samplingDecision.LimiterRate,
+ sample: samplingDecision.Sample);
return samplingDecision.Priority;
}
@@ -328,13 +339,16 @@ public void SetSamplingPriority(
string? mechanism = null,
float? rate = null,
float? limiterRate = null,
- bool notifyDistributedTracer = true)
+ bool notifyDistributedTracer = true,
+ bool? sample = null)
{
if (priority is not { } p)
{
return;
}
+ var isLocalRoot = SamplingPriority is null;
+
// priority (keep/drop) can change (manually, ASM, etc)
SamplingPriority = priority;
@@ -357,15 +371,54 @@ public void SetSamplingPriority(
Tags.RemoveTag(Trace.Tags.Propagated.DecisionMaker);
}
- // set Knuth sampling rate as a propagated tag for agent and rule-based sampling.
- // use TryAddTag to preserve the original rate, consistent with AppliedSamplingRate ??= rate above.
- if (rate is { } samplingRate && mechanism is Sampling.SamplingMechanism.AgentRate
+ // set Knuth sampling rate as a propagated tag for agent and rule-based sampling,
+ // and (for OTel interop) derive/erase the "ot=" tracestate rv/th sub-keys.
+ if (rate is { } samplingRate && samplingRate is >= 0f and <= 1f
+ && mechanism is Sampling.SamplingMechanism.AgentRate
or Sampling.SamplingMechanism.LocalTraceSamplingRule
or Sampling.SamplingMechanism.RemoteAdaptiveSamplingRule
- or Sampling.SamplingMechanism.RemoteUserSamplingRule)
+ or Sampling.SamplingMechanism.RemoteUserSamplingRule
+ or Sampling.SamplingMechanism.Default)
{
// format with up to 6 decimal digits, no trailing zeros (per RFC)
Tags.TryAddTag(Trace.Tags.Propagated.KnuthSamplingRate, samplingRate.ToString("0.######", CultureInfo.InvariantCulture));
+
+ if (isLocalRoot && sample is { } didSample && RootSpan is { } rootSpan)
+ {
+ var h = SamplingHelpers.ComputeKnuthHash(rootSpan.TraceId128.Lower);
+ var rv = (~h) >> 8;
+ // round-trip the rate through decimal first: samplingRate is a float, and widening it to
+ // double directly keeps its 32-bit mantissa noise in the low bits of the 56-bit threshold.
+ var th = (ulong)Math.Round((1.0 - (double)(decimal)samplingRate) * (1UL << 56), MidpointRounding.AwayFromZero);
+
+ // clamp th into the valid 56-bit domain: rate=0.0f rounds up to 2^56, one bit out of range.
+ th = Math.Min(th, (1UL << 56) - 1);
+
+ // 64<>56-bit imprecision clamp (design doc Decision 2 / RFC §7):
+ // force agreement between the (rv, th) pair and DD's actual keep/drop decision.
+ if (didSample && rv < th)
+ {
+ rv = th;
+ }
+ else if (!didSample && rv >= th)
+ {
+ rv = th > 0 ? th - 1 : 0;
+ }
+
+ // overwrites any inherited rv/th outright; keeps unrecognized sub-keys.
+ OtelTraceState = OtelTraceStateHelpers.SetRvTh(OtelTraceState, rv, th);
+
+ // rate-limiter demotion: probability decision said keep (didSample), but the
+ // final priority is a reject (the limiter is what changed the outcome) -> erase th.
+ if (didSample && SamplingPriorityValues.IsDrop(p))
+ {
+ OtelTraceState = OtelTraceStateHelpers.SetRvTh(OtelTraceState, OtelTraceStateHelpers.ExtractRv(OtelTraceState), th: null);
+ }
+ }
+ }
+ else if (mechanism is Sampling.SamplingMechanism.Manual or Sampling.SamplingMechanism.Asm)
+ {
+ OtelTraceState = OtelTraceStateHelpers.SetRvTh(OtelTraceState, OtelTraceStateHelpers.ExtractRv(OtelTraceState), th: null);
}
if (notifyDistributedTracer)
diff --git a/tracer/src/Datadog.Trace/Tracer.cs b/tracer/src/Datadog.Trace/Tracer.cs
index abb35acec096..da8450bc655a 100644
--- a/tracer/src/Datadog.Trace/Tracer.cs
+++ b/tracer/src/Datadog.Trace/Tracer.cs
@@ -333,6 +333,7 @@ internal SpanContext CreateSpanContext(ISpanContext parent = null, string servic
traceContext.SetSamplingPriority(samplingPriority);
traceContext.Origin = parentSpanContext.Origin;
traceContext.AdditionalW3CTraceState = parentSpanContext.AdditionalW3CTraceState;
+ traceContext.OtelTraceState = parentSpanContext.OtelTraceState;
}
// if the parent is a remote context, set the last parent id that came from the distributed header
diff --git a/tracer/src/Datadog.Trace/Util/SamplingHelpers.cs b/tracer/src/Datadog.Trace/Util/SamplingHelpers.cs
index fc2910ac4a0f..210c6aa8261c 100644
--- a/tracer/src/Datadog.Trace/Util/SamplingHelpers.cs
+++ b/tracer/src/Datadog.Trace/Util/SamplingHelpers.cs
@@ -41,9 +41,11 @@ internal static bool SampleByRate(ulong id, double rate)
return false;
}
- return (id * KnuthFactor) <= (rate * ulong.MaxValue);
+ return ComputeKnuthHash(id) <= (rate * ulong.MaxValue);
}
+ internal static ulong ComputeKnuthHash(ulong id) => id * KnuthFactor;
+
internal static bool IsKeptBySamplingPriority(in SpanCollection trace)
{
if (TraceContext.GetTraceContext(in trace)?.SamplingPriority is { } samplingPriority)
diff --git a/tracer/test/Datadog.Trace.Tests/Propagators/MultiSpanContextPropagatorTests.cs b/tracer/test/Datadog.Trace.Tests/Propagators/MultiSpanContextPropagatorTests.cs
index e93bb8cac067..7ae7a2403cd9 100644
--- a/tracer/test/Datadog.Trace.Tests/Propagators/MultiSpanContextPropagatorTests.cs
+++ b/tracer/test/Datadog.Trace.Tests/Propagators/MultiSpanContextPropagatorTests.cs
@@ -424,6 +424,23 @@ public void Extract_Behavior_Ignore()
result.Links.Should().BeNullOrEmpty();
}
+ [Fact]
+ public void Extract_Behavior_Ignore_DoesNotCarryOverOtelTraceState()
+ {
+ var headers = new Mock();
+
+ headers.Setup(h => h.GetValues("traceparent"))
+ .Returns(new[] { "00-000000000000000000000000075bcd15-000000003ade68b1-01" });
+ headers.Setup(h => h.GetValues("tracestate"))
+ .Returns(new[] { "dd=s:1,ot=rv:ef284ace7a91e1;th:e6666666666668" });
+
+ var names = new[] { ContextPropagationHeaderStyle.W3CTraceContext };
+ var ignorePropagator = SpanContextPropagatorFactory.GetSpanContextPropagator(names, names, propagationExtractFirst: true, ExtractBehavior.Ignore);
+ var result = ignorePropagator.Extract(headers.Object);
+
+ result.SpanContext.Should().BeNull();
+ }
+
[Fact]
public void Extract_Behavior_Restart()
{
@@ -467,6 +484,24 @@ public void Extract_Behavior_Restart()
opts => opts.ExcludingMissingMembers());
}
+ [Fact]
+ public void Extract_Behavior_Restart_DoesNotCarryOverOtelTraceState()
+ {
+ var headers = new Mock();
+
+ headers.Setup(h => h.GetValues("traceparent"))
+ .Returns(new[] { "00-000000000000000000000000075bcd15-000000003ade68b1-01" });
+ headers.Setup(h => h.GetValues("tracestate"))
+ .Returns(new[] { "dd=s:1,ot=rv:ef284ace7a91e1;th:e6666666666668" });
+
+ var names = new[] { ContextPropagationHeaderStyle.W3CTraceContext };
+ var restartPropagator = SpanContextPropagatorFactory.GetSpanContextPropagator(names, names, propagationExtractFirst: true, ExtractBehavior.Restart);
+ var result = restartPropagator.Extract(headers.Object);
+
+ result.SpanContext.Should().BeNull();
+ result.Links.Should().ContainSingle().Which.Context.OtelTraceState.Should().Be("rv:ef284ace7a91e1;th:e6666666666668");
+ }
+
[Fact]
public void Extract_B3SingleHeader_IHeadersCollection()
{
diff --git a/tracer/test/Datadog.Trace.Tests/Propagators/OtelTraceStateHelpersTests.cs b/tracer/test/Datadog.Trace.Tests/Propagators/OtelTraceStateHelpersTests.cs
new file mode 100644
index 000000000000..4705183c53b8
--- /dev/null
+++ b/tracer/test/Datadog.Trace.Tests/Propagators/OtelTraceStateHelpersTests.cs
@@ -0,0 +1,61 @@
+//
+// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License.
+// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc.
+//
+
+#nullable enable
+
+using Datadog.Trace.Propagators;
+using FluentAssertions;
+using Xunit;
+
+namespace Datadog.Trace.Tests.Propagators
+{
+ public class OtelTraceStateHelpersTests
+ {
+ [Theory]
+ [InlineData(null, null)]
+ [InlineData("", null)]
+ [InlineData("th:e6666666666668", null)]
+ [InlineData("rv:ef284ace7a91e1", 0xef284ace7a91e1UL)]
+ [InlineData("rv:ef284ace7a91e1;th:e6666666666668", 0xef284ace7a91e1UL)]
+ [InlineData("th:e6666666666668;rv:ef284ace7a91e1", 0xef284ace7a91e1UL)]
+ [InlineData("foo:bar;rv:1;baz:qux", 0x1UL)]
+ [InlineData("rv:zzzzzz", null)] // not hex -> malformed -> null
+ [InlineData("rv:123456789abcdef1", null)] // 15 hex digits -> too long -> malformed -> null
+ [InlineData("rv:", null)] // empty value -> malformed -> null
+ public void ExtractRv_ReturnsValueOrNull(string? raw, ulong? expected)
+ {
+ OtelTraceStateHelpers.ExtractRv(raw).Should().Be(expected);
+ }
+
+ [Theory]
+ // no rv, no th, no other items -> null
+ [InlineData(null, null, null, null)]
+ [InlineData("", null, null, null)]
+ // rv only
+ [InlineData(null, 0xef284ace7a91e1UL, null, "rv:ef284ace7a91e1")]
+ // th only, no trailing zeros to trim
+ [InlineData(null, null, 0xe6666666666668UL, "th:e6666666666668")]
+ // both rv and th, rv first
+ [InlineData(null, 0xef284ace7a91e1UL, 0xe6666666666668UL, "rv:ef284ace7a91e1;th:e6666666666668")]
+ // th with trailing zero nibbles trimmed
+ [InlineData(null, null, 0x100UL, "th:1")]
+ // existing rv/th replaced, unrelated sub-key preserved in original order
+ [InlineData("foo:bar;rv:1;th:2", 0xef284ace7a91e1UL, 0xe6666666666668UL, "rv:ef284ace7a91e1;th:e6666666666668;foo:bar")]
+ // malformed existing rv/th still stripped even though we're not re-deriving them
+ [InlineData("rv:zzzz;th:2;foo:bar", 0xef284ace7a91e1UL, null, "rv:ef284ace7a91e1;foo:bar")]
+ // rv/th both null, unrelated sub-key preserved
+ [InlineData("foo:bar", null, null, "foo:bar")]
+ public void SetRvTh_RewritesRvAndThInPlace(string? raw, ulong? rv, ulong? th, string? expected)
+ {
+ OtelTraceStateHelpers.SetRvTh(raw, rv, th).Should().Be(expected);
+ }
+
+ [Fact]
+ public void SetRvTh_ThZero_EmitsSingleZeroDigit()
+ {
+ OtelTraceStateHelpers.SetRvTh(null, rv: null, th: 0UL).Should().Be("th:0");
+ }
+ }
+}
diff --git a/tracer/test/Datadog.Trace.Tests/Propagators/W3CTraceContextPropagatorTests.cs b/tracer/test/Datadog.Trace.Tests/Propagators/W3CTraceContextPropagatorTests.cs
index 3e668b8a8146..d3c4dd49f2d4 100644
--- a/tracer/test/Datadog.Trace.Tests/Propagators/W3CTraceContextPropagatorTests.cs
+++ b/tracer/test/Datadog.Trace.Tests/Propagators/W3CTraceContextPropagatorTests.cs
@@ -9,6 +9,7 @@
using Datadog.Trace.ExtensionMethods;
using Datadog.Trace.Headers;
using Datadog.Trace.Propagators;
+using Datadog.Trace.Sampling;
using Datadog.Trace.Tagging;
using Datadog.Trace.Tests.Util;
using FluentAssertions;
@@ -231,6 +232,32 @@ public void CreateTraceStateHeader_With128Bit_TraceId()
tracestate.Should().Be("dd=s:2;p:0000000000000002");
}
+ [Fact]
+ public void CreateTraceStateHeader_EmitsOtRightAfterDd_WhenOtelTraceStateIsSet()
+ {
+ var traceContext = new TraceContext(new StubDatadogTracer());
+ var spanContext = new SpanContext(parent: SpanContext.None, traceContext, serviceName: null, traceId: (TraceId)1, spanId: 2)
+ {
+ OtelTraceState = "rv:ef284ace7a91e1;th:e6666666666668",
+ AdditionalW3CTraceState = "congo=t61rcWkgMzE"
+ };
+
+ var tracestate = W3CTraceContextPropagator.CreateTraceStateHeader(spanContext);
+
+ tracestate.Should().Be("dd=s:1;p:0000000000000002,ot=rv:ef284ace7a91e1;th:e6666666666668,congo=t61rcWkgMzE");
+ }
+
+ [Fact]
+ public void CreateTraceStateHeader_OmitsOtMember_WhenOtelTraceStateIsNull()
+ {
+ var traceContext = new TraceContext(new StubDatadogTracer());
+ var spanContext = new SpanContext(parent: SpanContext.None, traceContext, serviceName: null, traceId: (TraceId)1, spanId: 2);
+
+ var tracestate = W3CTraceContextPropagator.CreateTraceStateHeader(spanContext);
+
+ tracestate.Should().NotContain("ot=");
+ }
+
[Fact]
public void Inject_IHeadersCollection()
{
@@ -404,6 +431,29 @@ public void ParseTraceStateWithLastParent()
traceState.Should().BeEquivalentTo(expected);
}
+ [Theory]
+ [InlineData("dd=s:1;o:rum", null)]
+ [InlineData("dd=s:1,ot=rv:ef284ace7a91e1;th:e6666666666668", "rv:ef284ace7a91e1;th:e6666666666668")]
+ [InlineData("ot=th:e6666666666668,dd=s:1", "th:e6666666666668")]
+ [InlineData("foo=bar,dd=s:1,ot=rv:1,baz=qux", "rv:1")]
+ [InlineData("dd=s:1,ot=", "")]
+ public void ParseTraceState_CapturesRawOtelTraceState(string header, string expectedOtTraceState)
+ {
+ var traceState = W3CTraceContextPropagator.ParseTraceState(header);
+ traceState.OtTraceState.Should().Be(expectedOtTraceState);
+ }
+
+ [Theory]
+ [InlineData("dd=s:1;o:rum,foo=bar", null, "foo=bar")]
+ [InlineData("dd=s:1,ot=rv:1;th:2,foo=bar", "rv:1;th:2", "foo=bar")]
+ [InlineData("foo=bar,ot=rv:1,dd=s:1,baz=qux", "rv:1", "foo=bar,baz=qux")]
+ public void SplitTraceStateValues_ExtractsOtValues(string header, string expectedOt, string expectedAdditional)
+ {
+ W3CTraceContextPropagator.SplitTraceStateValues(header, out _, out var otValues, out var additionalValues);
+ otValues.Should().Be(expectedOt);
+ additionalValues.Should().Be(expectedAdditional);
+ }
+
[Fact]
public void MissingLastParentId_ShouldBe_Zeroes()
{
@@ -800,6 +850,128 @@ public void Extract_MatchingSampled1_UsesTracestateSamplingPriority(int sampling
opts => opts.ExcludingMissingMembers());
}
+ [Fact]
+ public void Extract_CopiesOtelTraceStateOntoSpanContext()
+ {
+ var headers = new Mock(MockBehavior.Strict);
+
+ headers.Setup(h => h.GetValues("traceparent"))
+ .Returns(new[] { "00-000000000000000000000000075bcd15-000000003ade68b1-01" });
+
+ headers.Setup(h => h.GetValues("tracestate"))
+ .Returns(new[] { "dd=s:1,ot=rv:ef284ace7a91e1;th:e6666666666668" });
+
+ var result = W3CPropagator.Extract(headers.Object);
+
+ headers.Verify(h => h.GetValues("traceparent"), Times.Once());
+ headers.Verify(h => h.GetValues("tracestate"), Times.Once());
+ headers.VerifyNoOtherCalls();
+
+ result.SpanContext!.OtelTraceState.Should().Be("rv:ef284ace7a91e1;th:e6666666666668");
+ }
+
+ [Theory]
+ [InlineData("rv:ef284ace7a91e1;th:e6666666666668")] // A2: full rv;th forwarded unchanged
+ [InlineData("th:e6666666666668")] // A2b: th-only forwards th, fabricates no rv
+ public void Continuation_ForwardsOtelTraceStateUnchanged_RegardlessOfLocalRateConfig(string inboundOtelTraceState)
+ {
+ var headers = new Mock(MockBehavior.Strict);
+
+ headers.Setup(h => h.GetValues("traceparent"))
+ .Returns(new[] { "00-00000000000000000000000000000001-0000000000000001-01" });
+
+ headers.Setup(h => h.GetValues("tracestate"))
+ .Returns(new[] { $"dd=s:1,ot={inboundOtelTraceState}" });
+
+ var result = W3CPropagator.Extract(headers.Object);
+
+ result.SpanContext!.OtelTraceState.Should().Be(inboundOtelTraceState);
+
+ var tracestate = W3CTraceContextPropagator.CreateTraceStateHeader(result.SpanContext);
+ tracestate.Should().Contain($"ot={inboundOtelTraceState}");
+ }
+
+ [Fact]
+ public void Continuation_SampledWithoutOtMember_FabricatesNoOtelTraceState()
+ {
+ var headers = new Mock(MockBehavior.Strict);
+
+ headers.Setup(h => h.GetValues("traceparent"))
+ .Returns(new[] { "00-00000000000000000000000000000001-0000000000000001-01" });
+
+ headers.Setup(h => h.GetValues("tracestate"))
+ .Returns(new[] { "dd=s:1" });
+
+ var result = W3CPropagator.Extract(headers.Object);
+
+ result.SpanContext!.OtelTraceState.Should().BeNull();
+
+ var tracestate = W3CTraceContextPropagator.CreateTraceStateHeader(result.SpanContext);
+ tracestate.Should().NotContain("ot=");
+ }
+
+ [Theory]
+ [InlineData("th:zz;rv:ef284ace7a91e1")] // malformed th, well-formed rv
+ [InlineData("th:e6666666666668;rv:zz")] // well-formed th, malformed rv
+ [InlineData("th:zz;rv:zz")] // both malformed
+ [InlineData("unknownkey:whatever")] // unrecognized sub-key entirely
+ public void Continuation_MalformedOrUnknownOtContent_RoundTripsByteForByte(string malformedOtelTraceState)
+ {
+ var headers = new Mock(MockBehavior.Strict);
+
+ headers.Setup(h => h.GetValues("traceparent"))
+ .Returns(new[] { "00-00000000000000000000000000000001-0000000000000001-01" });
+
+ headers.Setup(h => h.GetValues("tracestate"))
+ .Returns(new[] { $"dd=s:1,ot={malformedOtelTraceState}" });
+
+ var result = W3CPropagator.Extract(headers.Object);
+
+ var tracestate = W3CTraceContextPropagator.CreateTraceStateHeader(result.SpanContext!);
+ tracestate.Should().Contain($"ot={malformedOtelTraceState}");
+ }
+
+ [Fact]
+ public void Continuation_MultiVendorTracestate_RoundTripsFully_WithDdThenOtOrdering()
+ {
+ var headers = new Mock(MockBehavior.Strict);
+
+ headers.Setup(h => h.GetValues("traceparent"))
+ .Returns(new[] { "00-00000000000000000000000000000001-0000000000000001-01" });
+
+ headers.Setup(h => h.GetValues("tracestate"))
+ .Returns(new[] { "foo1=bar1,dd=s:1,ot=rv:ef284ace7a91e1;th:e6666666666668;unknownsubkey:x,congo=t61rcWkgMzE" });
+
+ var result = W3CPropagator.Extract(headers.Object);
+
+ result.SpanContext!.OtelTraceState.Should().Be("rv:ef284ace7a91e1;th:e6666666666668;unknownsubkey:x");
+ result.SpanContext!.AdditionalW3CTraceState.Should().Be("foo1=bar1,congo=t61rcWkgMzE");
+
+ var tracestate = W3CTraceContextPropagator.CreateTraceStateHeader(result.SpanContext);
+ var ddIndex = tracestate.IndexOf("dd=", StringComparison.Ordinal);
+ var otIndex = tracestate.IndexOf("ot=", StringComparison.Ordinal);
+ ddIndex.Should().BeLessThan(otIndex);
+ }
+
+ [Fact]
+ public void RootTrace_ProbabilityKeepAtKnownRate_EmitsRfcWorkedExampleOtelTraceState()
+ {
+ // Simulates a brand-new root trace (no incoming ot=) sampled at rate=0.1
+ // with trace_id_low64 = 0xfff972474538efff, matching the RFC's worked example.
+ var traceContext = TraceContextTestHelpers.CreateTraceContextWithRootSpan(traceIdLower: 0xfff972474538efff);
+
+ traceContext.SetSamplingPriority(
+ priority: SamplingPriorityValues.UserKeep,
+ mechanism: SamplingMechanism.LocalTraceSamplingRule,
+ rate: 0.1f,
+ sample: true);
+
+ var spanContext = traceContext.RootSpan!.Context;
+ var tracestate = W3CTraceContextPropagator.CreateTraceStateHeader(spanContext);
+
+ tracestate.Should().Contain("ot=rv:ef284ace7a91e1;th:e6666666666668");
+ }
+
[Theory]
[InlineData(SamplingPriorityValues.AutoReject)]
[InlineData(SamplingPriorityValues.UserReject)]
diff --git a/tracer/test/Datadog.Trace.Tests/Sampling/TraceSamplerTests.cs b/tracer/test/Datadog.Trace.Tests/Sampling/TraceSamplerTests.cs
index f79d28eada8a..ffc6ea30f7b1 100644
--- a/tracer/test/Datadog.Trace.Tests/Sampling/TraceSamplerTests.cs
+++ b/tracer/test/Datadog.Trace.Tests/Sampling/TraceSamplerTests.cs
@@ -173,6 +173,25 @@ public async Task Choose_Between_Sampling_Mechanisms()
mechanism2.Should().Be(SamplingMechanism.AgentRate);
}
+ [Fact]
+ public async Task MakeSamplingDecision_ReturnsSampleField_MatchingKnuthOutcome()
+ {
+ var settings = TracerSettings.Create(new() { { ConfigurationKeys.ServiceName, ServiceName } });
+ await using var tracer = TracerHelper.CreateWithFakeAgent(settings);
+
+ using var scope = (Scope)tracer.StartActive(OperationName);
+ scope.Span.Context.TraceContext.Environment = Env;
+
+ var builder = new TraceSampler.Builder(new NoLimits());
+ builder.RegisterAgentSamplingRule(new AgentSamplingRule());
+ var sampler = builder.Build();
+ sampler.SetDefaultSampleRates(new Dictionary { { $"service:{ServiceName},env:{Env}", 1f } });
+
+ var decision = sampler.MakeSamplingDecision(scope.Span);
+
+ decision.Sample.Should().BeTrue();
+ }
+
private async Task RunSamplerTest(
ITraceSampler sampler,
int iterations,
diff --git a/tracer/test/Datadog.Trace.Tests/Tagging/TagsListTests.cs b/tracer/test/Datadog.Trace.Tests/Tagging/TagsListTests.cs
index 614693ddcd6f..f8873c436df5 100644
--- a/tracer/test/Datadog.Trace.Tests/Tagging/TagsListTests.cs
+++ b/tracer/test/Datadog.Trace.Tests/Tagging/TagsListTests.cs
@@ -174,7 +174,7 @@ public async Task Serialization_RootSpan()
deserializedSpan.Tags.Should().Contain(Tags.Propagated.DecisionMaker, SamplingMechanism.Default);
deserializedSpan.Tags.Should().Contain(Tags.Propagated.TraceIdUpper, hexStringTraceId);
deserializedSpan.Tags.Should().ContainKey(Tags.ProcessTags);
- deserializedSpan.Tags.Should().HaveCount(customTagCount + 6);
+ deserializedSpan.Tags.Should().HaveCount(customTagCount + 7);
deserializedSpan.Metrics.Should().Contain(Metrics.SamplingPriority, 1);
deserializedSpan.Metrics.Should().Contain(Metrics.SamplingLimitDecision, 0.75);
@@ -219,7 +219,7 @@ public async Task Serialization_ServiceEntrySpan()
deserializedSpan.Tags.Should().ContainKey(Tags.BaseService);
deserializedSpan.Tags[Tags.BaseService].Should().Be(_tracer.DefaultServiceName);
deserializedSpan.Tags.Should().ContainKey(Tags.ProcessTags);
- deserializedSpan.Tags.Should().HaveCount(customTagCount + 7);
+ deserializedSpan.Tags.Should().HaveCount(customTagCount + 8);
deserializedSpan.Metrics.Should().Contain(Metrics.SamplingLimitDecision, 0.75);
deserializedSpan.Metrics.Should().Contain(Metrics.TopLevelSpan, 1);
@@ -260,7 +260,7 @@ public async Task Serialization_ChildSpan()
deserializedSpan.Tags.Should().ContainKey(Tags.BaseService);
deserializedSpan.Tags[Tags.BaseService].Should().Be(_tracer.DefaultServiceName);
deserializedSpan.Tags.Should().ContainKey(Tags.ProcessTags);
- deserializedSpan.Tags.Should().HaveCount(customTagCount + 6);
+ deserializedSpan.Tags.Should().HaveCount(customTagCount + 7);
deserializedSpan.Metrics.Should().Contain(Metrics.SamplingLimitDecision, 0.75);
deserializedSpan.Metrics.Should().HaveCount(customTagCount + 1);
diff --git a/tracer/test/Datadog.Trace.Tests/TraceContextTests.cs b/tracer/test/Datadog.Trace.Tests/TraceContextTests.cs
index 961395c2b130..39e80bddd75b 100644
--- a/tracer/test/Datadog.Trace.Tests/TraceContextTests.cs
+++ b/tracer/test/Datadog.Trace.Tests/TraceContextTests.cs
@@ -7,6 +7,7 @@
using System.Threading.Tasks;
using Datadog.Trace.Agent;
using Datadog.Trace.Configuration;
+using Datadog.Trace.Propagators;
using Datadog.Trace.Sampling;
using Datadog.Trace.TestHelpers;
using Datadog.Trace.TestHelpers.TestTracer;
@@ -217,5 +218,188 @@ public async Task Null_Service_Names_Dont_Throw()
span.SetService(null, null);
span.Finish(); // should not throw
}
+
+ [Fact]
+ public void SetSamplingPriority_RootProbabilityKeep_DerivesRvTh_MatchesRfcWorkedExample()
+ {
+ // RFC worked example: trace_id_low64 = 0xfff972474538efff, rate = 0.1
+ // -> ot=rv:ef284ace7a91e1;th:e6666666666668
+ var traceContext = TraceContextTestHelpers.CreateTraceContextWithRootSpan(traceIdLower: 0xfff972474538efff);
+
+ traceContext.SetSamplingPriority(
+ priority: SamplingPriorityValues.UserKeep,
+ mechanism: SamplingMechanism.LocalTraceSamplingRule,
+ rate: 0.1f,
+ sample: true);
+
+ traceContext.OtelTraceState.Should().Be("rv:ef284ace7a91e1;th:e6666666666668");
+ }
+
+ [Fact]
+ public void SetSamplingPriority_RootProbabilityDrop_StillEmitsTh()
+ {
+ var traceContext = TraceContextTestHelpers.CreateTraceContextWithRootSpan(traceIdLower: 0xfff972474538efff);
+
+ traceContext.SetSamplingPriority(
+ priority: SamplingPriorityValues.UserReject,
+ mechanism: SamplingMechanism.LocalTraceSamplingRule,
+ rate: 0.1f,
+ sample: false);
+
+ traceContext.OtelTraceState.Should().Contain("th:e6666666666668");
+ }
+
+ [Fact]
+ public void SetSamplingPriority_ImprecisionClamp_ForcesAgreementWithDdDecision()
+ {
+ // RFC §3 example: trace_id_low64 = 0x03a93ee8b1999f00, rate = 0.1 disagrees pre-clamp
+ var traceIdLower = 0x03a93ee8b1999f00UL;
+ var sample = SamplingHelpers.SampleByRate(traceIdLower, 0.1);
+ var traceContext = TraceContextTestHelpers.CreateTraceContextWithRootSpan(traceIdLower);
+
+ traceContext.SetSamplingPriority(
+ priority: sample ? SamplingPriorityValues.UserKeep : SamplingPriorityValues.UserReject,
+ mechanism: SamplingMechanism.LocalTraceSamplingRule,
+ rate: 0.1f,
+ sample: sample);
+
+ var rv = OtelTraceStateHelpers.ExtractRv(traceContext.OtelTraceState)!.Value;
+ var th = ParseThForTest(traceContext.OtelTraceState);
+ (rv >= th).Should().Be(sample); // post-clamp, rv>=th must agree with DD's actual keep/drop decision
+ }
+
+ [Fact]
+ public void SetSamplingPriority_NonProbabilityMechanism_DoesNotDeriveOtelTraceState()
+ {
+ var traceContext = TraceContextTestHelpers.CreateTraceContextWithRootSpan(traceIdLower: 1);
+
+ traceContext.SetSamplingPriority(SamplingPriorityValues.UserKeep, SamplingMechanism.Manual);
+
+ traceContext.OtelTraceState.Should().BeNull();
+ }
+
+ [Fact]
+ public void SetSamplingPriority_RateLimiterDemotesKeep_StripsThButKeepsRv()
+ {
+ var traceContext = TraceContextTestHelpers.CreateTraceContextWithRootSpan(traceIdLower: 0xfff972474538efff);
+
+ // sample=true (probability said keep) but final priority is UserReject (limiter demoted it)
+ traceContext.SetSamplingPriority(
+ priority: SamplingPriorityValues.UserReject,
+ mechanism: SamplingMechanism.LocalTraceSamplingRule,
+ rate: 0.1f,
+ limiterRate: 0.05f,
+ sample: true);
+
+ traceContext.OtelTraceState.Should().Be("rv:ef284ace7a91e1");
+ }
+
+ [Fact]
+ public void TraceSampler_LimiterDemotesKeep_ErasesThOnTraceContext_ViaGetOrMakeSamplingDecision()
+ {
+ var builder = new TraceSampler.Builder(new TracerRateLimiter(maxTracesPerInterval: 0, intervalMilliseconds: null));
+ builder.RegisterRule(new GlobalSamplingRateRule(1.0f));
+ var sampler = builder.Build();
+
+ var tracer = new StubDatadogTracer(sampler);
+ var rootSpan = new Span(new SpanContext(0xfff972474538efffUL, RandomIdGenerator.Shared.NextSpanId()), DateTimeOffset.UtcNow);
+ var traceContext = new TraceContext(tracer);
+ traceContext.AddSpan(rootSpan);
+
+ traceContext.GetOrMakeSamplingDecision();
+
+ traceContext.OtelTraceState.Should().Be("rv:ef284ace7a91e1");
+ }
+
+ [Fact]
+ public void SetSamplingPriority_RateZero_ThStaysWithin56BitDomain()
+ {
+ var traceContext = TraceContextTestHelpers.CreateTraceContextWithRootSpan(traceIdLower: 0xfff972474538efff);
+
+ traceContext.SetSamplingPriority(
+ priority: SamplingPriorityValues.UserReject,
+ mechanism: SamplingMechanism.LocalTraceSamplingRule,
+ rate: 0.0f,
+ sample: false);
+
+ var otelState = traceContext.OtelTraceState;
+ otelState.Should().NotBeNull();
+ var th = ParseThForTest(otelState!);
+ th.Should().BeLessOrEqualTo((1UL << 56) - 1);
+ }
+
+ [Fact]
+ public void SetSamplingPriority_RateOne_InconsistentDropDoesNotUnderflow()
+ {
+ var traceContext = TraceContextTestHelpers.CreateTraceContextWithRootSpan(traceIdLower: 0xfff972474538efff);
+
+ traceContext.SetSamplingPriority(
+ priority: SamplingPriorityValues.UserReject,
+ mechanism: SamplingMechanism.LocalTraceSamplingRule,
+ rate: 1.0f,
+ sample: false);
+
+ var rv = OtelTraceStateHelpers.ExtractRv(traceContext.OtelTraceState)!.Value;
+ rv.Should().Be(0UL);
+ }
+
+ [Fact]
+ public void SetSamplingPriority_NaNRate_DoesNotThrow()
+ {
+ var traceContext = TraceContextTestHelpers.CreateTraceContextWithRootSpan(traceIdLower: 1);
+
+ Action act = () => traceContext.SetSamplingPriority(
+ priority: SamplingPriorityValues.UserKeep,
+ mechanism: SamplingMechanism.Default,
+ rate: float.NaN,
+ sample: true);
+
+ act.Should().NotThrow();
+ }
+
+ [Fact]
+ public void SetSamplingPriority_ManualOverride_StripsInheritedThButKeepsRv()
+ {
+ var traceContext = TraceContextTestHelpers.CreateTraceContextWithRootSpan(traceIdLower: 1);
+ traceContext.OtelTraceState = "rv:ef284ace7a91e1;th:e6666666666668";
+
+ traceContext.SetSamplingPriority(SamplingPriorityValues.UserKeep, SamplingMechanism.Manual);
+
+ traceContext.OtelTraceState.Should().Be("rv:ef284ace7a91e1");
+ }
+
+ [Fact]
+ public void SetSamplingPriority_AsmOverride_StripsInheritedThButKeepsRv()
+ {
+ var traceContext = TraceContextTestHelpers.CreateTraceContextWithRootSpan(traceIdLower: 1);
+ traceContext.OtelTraceState = "th:e6666666666668";
+
+ traceContext.SetSamplingPriority(SamplingPriorityValues.UserReject, SamplingMechanism.Asm);
+
+ traceContext.OtelTraceState.Should().BeNull();
+ }
+
+ [Fact]
+ public void SetSamplingPriority_ManualOverride_NoInheritedState_StaysNull()
+ {
+ var traceContext = TraceContextTestHelpers.CreateTraceContextWithRootSpan(traceIdLower: 1);
+
+ traceContext.SetSamplingPriority(SamplingPriorityValues.UserKeep, SamplingMechanism.Manual);
+
+ traceContext.OtelTraceState.Should().BeNull();
+ }
+
+ private static ulong ParseThForTest(string otelTraceState)
+ {
+ foreach (var item in otelTraceState.Split(';'))
+ {
+ if (item.StartsWith("th:", StringComparison.Ordinal))
+ {
+ return Convert.ToUInt64(item.Substring(3), 16);
+ }
+ }
+
+ throw new InvalidOperationException("no th found");
+ }
}
}
diff --git a/tracer/test/Datadog.Trace.Tests/TraceContextTests_KnuthSamplingRate.cs b/tracer/test/Datadog.Trace.Tests/TraceContextTests_KnuthSamplingRate.cs
index 9d35ef134b5a..c12fdb329e8e 100644
--- a/tracer/test/Datadog.Trace.Tests/TraceContextTests_KnuthSamplingRate.cs
+++ b/tracer/test/Datadog.Trace.Tests/TraceContextTests_KnuthSamplingRate.cs
@@ -20,6 +20,7 @@ public class TraceContextTests_KnuthSamplingRate
[InlineData(SamplingPriorityValues.UserKeep, SamplingMechanism.LocalTraceSamplingRule, 0.25f, "0.25")]
[InlineData(SamplingPriorityValues.UserKeep, SamplingMechanism.RemoteUserSamplingRule, 0.75f, "0.75")]
[InlineData(SamplingPriorityValues.UserKeep, SamplingMechanism.RemoteAdaptiveSamplingRule, 0.333333f, "0.333333")]
+ [InlineData(SamplingPriorityValues.AutoKeep, SamplingMechanism.Default, 0.5f, "0.5")]
public void SetSamplingPriority_SetsKsrTag_ForApplicableMechanisms(
int samplingPriority, string samplingMechanism, float rate, string expectedKsr)
{
@@ -52,7 +53,6 @@ public void SetSamplingPriority_SetsKsrTag_EvenForDropDecisions(
[Theory]
[InlineData(SamplingPriorityValues.UserKeep, SamplingMechanism.Manual, 0.5f)]
[InlineData(SamplingPriorityValues.UserKeep, SamplingMechanism.Asm, 0.5f)]
- [InlineData(SamplingPriorityValues.AutoKeep, SamplingMechanism.Default, 0.5f)]
public void SetSamplingPriority_DoesNotSetKsrTag_ForNonApplicableMechanisms(
int samplingPriority, string samplingMechanism, float rate)
{
diff --git a/tracer/test/Datadog.Trace.Tests/Util/StubDatadogTracer.cs b/tracer/test/Datadog.Trace.Tests/Util/StubDatadogTracer.cs
index ee8ab7ba170a..ec1e6ddf5864 100644
--- a/tracer/test/Datadog.Trace.Tests/Util/StubDatadogTracer.cs
+++ b/tracer/test/Datadog.Trace.Tests/Util/StubDatadogTracer.cs
@@ -3,12 +3,15 @@
// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc.
//
+#nullable enable
+
using System;
using System.Collections.Generic;
using Datadog.Trace.Agent;
using Datadog.Trace.Configuration;
using Datadog.Trace.Configuration.Schema;
using Datadog.Trace.Configuration.Telemetry;
+using Datadog.Trace.Sampling;
namespace Datadog.Trace.Tests.Util;
@@ -20,11 +23,21 @@ public StubDatadogTracer()
}
public StubDatadogTracer(TracerSettings settings)
+ : this(settings, traceSampler: null)
+ {
+ }
+
+ public StubDatadogTracer(ITraceSampler traceSampler)
+ : this(new TracerSettings(NullConfigurationSource.Instance), traceSampler)
+ {
+ }
+
+ public StubDatadogTracer(TracerSettings settings, ITraceSampler? traceSampler)
{
DefaultServiceName = "stub-service";
Settings = settings;
var namingSchema = new NamingSchema(SchemaVersion.V0, false, false, DefaultServiceName, null, null);
- PerTraceSettings = new PerTraceSettings(null, null, namingSchema, MutableSettings.CreateWithoutDefaultSources(Settings, new ConfigurationTelemetry()));
+ PerTraceSettings = new PerTraceSettings(traceSampler, null, namingSchema, MutableSettings.CreateWithoutDefaultSources(Settings, new ConfigurationTelemetry()));
}
public string DefaultServiceName { get; }
diff --git a/tracer/test/Datadog.Trace.Tests/Util/TraceContextTestHelpers.cs b/tracer/test/Datadog.Trace.Tests/Util/TraceContextTestHelpers.cs
new file mode 100644
index 000000000000..554c85cb56cf
--- /dev/null
+++ b/tracer/test/Datadog.Trace.Tests/Util/TraceContextTestHelpers.cs
@@ -0,0 +1,21 @@
+//
+// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License.
+// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc.
+//
+
+using System;
+using Datadog.Trace.Util;
+
+namespace Datadog.Trace.Tests.Util;
+
+internal static class TraceContextTestHelpers
+{
+ public static TraceContext CreateTraceContextWithRootSpan(ulong traceIdLower)
+ {
+ var traceContext = new TraceContext(new StubDatadogTracer());
+ var spanContext = new SpanContext(parent: SpanContext.None, traceContext, serviceName: null, traceId: (TraceId)traceIdLower, spanId: RandomIdGenerator.Shared.NextSpanId());
+ var rootSpan = new Span(spanContext, DateTimeOffset.UtcNow);
+ traceContext.AddSpan(rootSpan);
+ return traceContext;
+ }
+}