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); + } +}