diff --git a/tracer/src/Datadog.Trace/Tags.cs b/tracer/src/Datadog.Trace/Tags.cs
index 5eff6f23aa60..48c2d0bed8e5 100644
--- a/tracer/src/Datadog.Trace/Tags.cs
+++ b/tracer/src/Datadog.Trace/Tags.cs
@@ -864,6 +864,13 @@ internal static class Propagated
/// A two char hex string with the product being the trace source
///
internal const string TraceSource = "_dd.p.ts";
+
+ ///
+ /// Tag used to propagate the Knuth sampling rate applied to the trace.
+ /// Set when a sampling decision is made using agent-based or rule-based sampling.
+ /// The value is the applied sampling rate formatted as a string with up to 6 significant digits.
+ ///
+ internal const string KnuthSamplingRate = "_dd.p.ksr";
}
}
}
diff --git a/tracer/src/Datadog.Trace/TraceContext.cs b/tracer/src/Datadog.Trace/TraceContext.cs
index aa6293909a4c..13aafc1bb12f 100644
--- a/tracer/src/Datadog.Trace/TraceContext.cs
+++ b/tracer/src/Datadog.Trace/TraceContext.cs
@@ -8,6 +8,7 @@
using System;
using System.Collections;
using System.Collections.Generic;
+using System.Globalization;
using System.Runtime.CompilerServices;
using System.Threading;
using Datadog.Trace.Agent;
@@ -329,6 +330,17 @@ 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
+ or Sampling.SamplingMechanism.LocalTraceSamplingRule
+ or Sampling.SamplingMechanism.RemoteAdaptiveSamplingRule
+ or Sampling.SamplingMechanism.RemoteUserSamplingRule)
+ {
+ // format with up to 6 decimal digits, no trailing zeros (per RFC)
+ Tags.TryAddTag(Trace.Tags.Propagated.KnuthSamplingRate, samplingRate.ToString("0.######", CultureInfo.InvariantCulture));
+ }
+
if (notifyDistributedTracer)
{
DistributedTracer.Instance.SetSamplingPriority(priority);
diff --git a/tracer/test/Datadog.Trace.TestHelpers/SpanTagAssertion.cs b/tracer/test/Datadog.Trace.TestHelpers/SpanTagAssertion.cs
index 37388deb3f16..a1a6d11e3943 100644
--- a/tracer/test/Datadog.Trace.TestHelpers/SpanTagAssertion.cs
+++ b/tracer/test/Datadog.Trace.TestHelpers/SpanTagAssertion.cs
@@ -28,6 +28,7 @@ public static void DefaultTagAssertions(SpanTagAssertion s) => s
.IsOptional("version")
.IsOptional("_dd.p.dm") // "decision maker", but contains the sampling mechanism
.IsOptional("_dd.p.tid") // contains the upper 64 bits of a 128-bit trace id
+ .IsOptional("_dd.p.ksr") // Knuth sampling rate, propagated tag added by the tracer
.IsOptional("_dd.parent_id") // Contains the 16 length hex decoded last found parent_id found set from either exising `p` tag on tracestate or headers
.IsOptional("error.msg")
.IsOptional("error.type")
diff --git a/tracer/test/Datadog.Trace.Tests/TraceContextTests_KnuthSamplingRate.cs b/tracer/test/Datadog.Trace.Tests/TraceContextTests_KnuthSamplingRate.cs
new file mode 100644
index 000000000000..9d35ef134b5a
--- /dev/null
+++ b/tracer/test/Datadog.Trace.Tests/TraceContextTests_KnuthSamplingRate.cs
@@ -0,0 +1,110 @@
+//
+// 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 Datadog.Trace.Sampling;
+using Datadog.Trace.Tests.Util;
+using FluentAssertions;
+using Xunit;
+
+namespace Datadog.Trace.Tests;
+
+public class TraceContextTests_KnuthSamplingRate
+{
+ [Theory]
+ [InlineData(SamplingPriorityValues.AutoKeep, SamplingMechanism.AgentRate, 0.5f, "0.5")]
+ [InlineData(SamplingPriorityValues.AutoKeep, SamplingMechanism.AgentRate, 1.0f, "1")]
+ [InlineData(SamplingPriorityValues.AutoKeep, SamplingMechanism.AgentRate, 0.1f, "0.1")]
+ [InlineData(SamplingPriorityValues.AutoKeep, SamplingMechanism.AgentRate, 0.7654321f, "0.765432")]
+ [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")]
+ public void SetSamplingPriority_SetsKsrTag_ForApplicableMechanisms(
+ int samplingPriority, string samplingMechanism, float rate, string expectedKsr)
+ {
+ var tracer = new StubDatadogTracer();
+ var traceContext = new TraceContext(tracer);
+
+ traceContext.SetSamplingPriority(samplingPriority, samplingMechanism, rate);
+
+ traceContext.Tags.GetTag(Tags.Propagated.KnuthSamplingRate).Should().Be(expectedKsr);
+ }
+
+ [Theory]
+ [InlineData(SamplingPriorityValues.AutoReject, SamplingMechanism.AgentRate, 0.5f)]
+ [InlineData(SamplingPriorityValues.UserReject, SamplingMechanism.LocalTraceSamplingRule, 0.25f)]
+ [InlineData(SamplingPriorityValues.UserReject, SamplingMechanism.RemoteUserSamplingRule, 0.75f)]
+ [InlineData(SamplingPriorityValues.UserReject, SamplingMechanism.RemoteAdaptiveSamplingRule, 0.1f)]
+ public void SetSamplingPriority_SetsKsrTag_EvenForDropDecisions(
+ int samplingPriority, string samplingMechanism, float rate)
+ {
+ var tracer = new StubDatadogTracer();
+ var traceContext = new TraceContext(tracer);
+
+ traceContext.SetSamplingPriority(samplingPriority, samplingMechanism, rate);
+
+ // KSR should be set regardless of keep/drop since it uses TryAddTag
+ // and records the original sampling rate
+ traceContext.Tags.GetTag(Tags.Propagated.KnuthSamplingRate).Should().NotBeNull();
+ }
+
+ [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)
+ {
+ var tracer = new StubDatadogTracer();
+ var traceContext = new TraceContext(tracer);
+
+ traceContext.SetSamplingPriority(samplingPriority, samplingMechanism, rate);
+
+ traceContext.Tags.GetTag(Tags.Propagated.KnuthSamplingRate).Should().BeNull();
+ }
+
+ [Fact]
+ public void SetSamplingPriority_DoesNotSetKsrTag_WhenRateIsNull()
+ {
+ var tracer = new StubDatadogTracer();
+ var traceContext = new TraceContext(tracer);
+
+ traceContext.SetSamplingPriority(SamplingPriorityValues.AutoKeep, SamplingMechanism.AgentRate, rate: null);
+
+ traceContext.Tags.GetTag(Tags.Propagated.KnuthSamplingRate).Should().BeNull();
+ }
+
+ [Fact]
+ public void SetSamplingPriority_PreservesOriginalKsrTag()
+ {
+ var tracer = new StubDatadogTracer();
+ var traceContext = new TraceContext(tracer);
+
+ traceContext.SetSamplingPriority(SamplingPriorityValues.AutoKeep, SamplingMechanism.AgentRate, 0.5f);
+ traceContext.Tags.GetTag(Tags.Propagated.KnuthSamplingRate).Should().Be("0.5");
+
+ // second call should NOT overwrite — TryAddTag preserves the original rate,
+ // consistent with AppliedSamplingRate ??= rate semantics
+ traceContext.SetSamplingPriority(SamplingPriorityValues.UserKeep, SamplingMechanism.LocalTraceSamplingRule, 0.75f);
+ traceContext.Tags.GetTag(Tags.Propagated.KnuthSamplingRate).Should().Be("0.5");
+ }
+
+ [Theory]
+ [InlineData(0.0f, "0")]
+ [InlineData(1.0f, "1")]
+ [InlineData(0.5f, "0.5")]
+ [InlineData(0.1f, "0.1")]
+ [InlineData(0.123456f, "0.123456")]
+ [InlineData(0.1234567f, "0.123457")]
+ [InlineData(0.00001f, "0.00001")]
+ public void KsrTag_FormattedWithUpToSixDecimalDigits(float rate, string expected)
+ {
+ var tracer = new StubDatadogTracer();
+ var traceContext = new TraceContext(tracer);
+
+ traceContext.SetSamplingPriority(SamplingPriorityValues.AutoKeep, SamplingMechanism.AgentRate, rate);
+
+ traceContext.Tags.GetTag(Tags.Propagated.KnuthSamplingRate).Should().Be(expected);
+ }
+}