diff --git a/dd-java-agent/instrumentation/opentelemetry/opentelemetry-0.3/src/test/groovy/OpenTelemetryTest.groovy b/dd-java-agent/instrumentation/opentelemetry/opentelemetry-0.3/src/test/groovy/OpenTelemetryTest.groovy
index dd7647676ae..3f24dad8eb5 100644
--- a/dd-java-agent/instrumentation/opentelemetry/opentelemetry-0.3/src/test/groovy/OpenTelemetryTest.groovy
+++ b/dd-java-agent/instrumentation/opentelemetry/opentelemetry-0.3/src/test/groovy/OpenTelemetryTest.groovy
@@ -271,24 +271,38 @@ class OpenTelemetryTest extends InstrumentationSpecification {
httpPropagator.inject(context, textMap, new TextMapSetter())
then:
- def expectedTraceparent = "00-${span.delegate.traceId.toHexStringPadded(32)}" +
- "-${DDSpanId.toHexStringPadded(span.delegate.spanId)}" +
+ def traceId = span.delegate.traceId as DDTraceId
+ def spanId = span.delegate.spanId
+ def expectedTraceparent = "00-${traceId.toHexStringPadded(32)}" +
+ "-${DDSpanId.toHexStringPadded(spanId)}" +
"-" + (propagatedPriority > 0 ? "01" : "00")
- def expectedTracestate = "dd=s:${propagatedPriority};p:${DDSpanId.toHexStringPadded(span.delegate.spanId)}"
- def expectedDatadogTags = null
+ def expectedTracestate = "dd=s:${propagatedPriority};p:${DDSpanId.toHexStringPadded(spanId)}"
+ def expectedDataTags = []
if (propagatedMechanism != UNKNOWN) {
- expectedDatadogTags = "_dd.p.dm=-" + propagatedMechanism
- expectedTracestate+= ";t.dm:-" + propagatedMechanism
+ expectedDataTags << "_dd.p.dm=-" + propagatedMechanism
+ expectedTracestate += ";t.dm:-" + propagatedMechanism
+ }
+ if (traceId.toHighOrderLong() != 0) {
+ expectedTracestate += ";t.tid:${traceId.toHexStringPadded(32).substring(0, 16)}"
+ }
+ if (contextPriority == UNSET) {
+ expectedTracestate += ";t.ksr:1"
+ }
+ if (traceId.toHighOrderLong() != 0) {
+ expectedDataTags << "_dd.p.tid=" + traceId.toHexStringPadded(32).substring(0, 16)
+ }
+ if (contextPriority == UNSET) {
+ expectedDataTags << "_dd.p.ksr=1"
}
def expectedTextMap = [
- "x-datadog-trace-id" : "$span.delegate.traceId",
- "x-datadog-parent-id" : "$span.delegate.spanId",
+ "x-datadog-trace-id" : "$traceId",
+ "x-datadog-parent-id" : "$spanId",
"x-datadog-sampling-priority": propagatedPriority.toString(),
"traceparent" : expectedTraceparent,
"tracestate" : expectedTracestate,
]
- if (expectedDatadogTags != null) {
- expectedTextMap.put("x-datadog-tags", expectedDatadogTags)
+ if (!expectedDataTags.empty) {
+ expectedTextMap.put("x-datadog-tags", expectedDataTags.join(','))
}
textMap == expectedTextMap
diff --git a/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.4/src/test/groovy/opentelemetry14/context/propagation/DatadogPropagatorTest.groovy b/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.4/src/test/groovy/opentelemetry14/context/propagation/DatadogPropagatorTest.groovy
index 061138bb76e..43355b189cb 100644
--- a/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.4/src/test/groovy/opentelemetry14/context/propagation/DatadogPropagatorTest.groovy
+++ b/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.4/src/test/groovy/opentelemetry14/context/propagation/DatadogPropagatorTest.groovy
@@ -37,6 +37,9 @@ class DatadogPropagatorTest extends AgentPropagatorTest {
if (traceId.length() == 32) {
tags+= '_dd.p.tid='+ traceId.substring(0, 16)
}
+ if (sampling == UNSET) {
+ tags+= '_dd.p.ksr=1'
+ }
assert headers['x-datadog-tags'] == tags.join(',')
assert headers['x-datadog-sampling-priority'] == samplingPriority
}
diff --git a/dd-java-agent/instrumentation/opentracing/opentracing-0.31/src/test/groovy/OpenTracing31Test.groovy b/dd-java-agent/instrumentation/opentracing/opentracing-0.31/src/test/groovy/OpenTracing31Test.groovy
index e17b06ad232..2c41794bcaa 100644
--- a/dd-java-agent/instrumentation/opentracing/opentracing-0.31/src/test/groovy/OpenTracing31Test.groovy
+++ b/dd-java-agent/instrumentation/opentracing/opentracing-0.31/src/test/groovy/OpenTracing31Test.groovy
@@ -284,13 +284,21 @@ class OpenTracing31Test extends InstrumentationSpecification {
"-${DDSpanId.toHexStringPadded(context.delegate.spanId)}" +
"-" + (propagatedPriority > 0 ? "01" : "00")
def expectedTracestate = "dd=s:${propagatedPriority};p:${DDSpanId.toHexStringPadded(context.delegate.spanId)}"
- def expectedDatadogTags = null
+ def datadogTags = []
if (propagatedPriority > 0) {
def effectiveSamplingMechanism = contextPriority == UNSET ? AGENT_RATE : samplingMechanism
- expectedDatadogTags = "_dd.p.dm=-" + effectiveSamplingMechanism
- expectedTracestate+= ";t.dm:-" + effectiveSamplingMechanism
+ datadogTags << "_dd.p.dm=-" + effectiveSamplingMechanism
+ expectedTracestate += ";t.dm:-" + effectiveSamplingMechanism
+ }
+ def traceId = context.delegate.traceId as DDTraceId
+ if (traceId.toHighOrderLong() != 0) {
+ expectedTracestate += ";t.tid:${traceId.toHexStringPadded(32).substring(0, 16)}"
+ datadogTags << "_dd.p.tid=" + traceId.toHexStringPadded(32).substring(0, 16)
+ }
+ if (contextPriority == UNSET) {
+ expectedTracestate += ";t.ksr:1"
+ datadogTags << "_dd.p.ksr=1"
}
-
def expectedTextMap = [
"x-datadog-trace-id" : "$context.delegate.traceId",
"x-datadog-parent-id" : "$context.delegate.spanId",
@@ -298,8 +306,8 @@ class OpenTracing31Test extends InstrumentationSpecification {
"traceparent" : expectedTraceparent,
"tracestate" : expectedTracestate,
]
- if (expectedDatadogTags != null) {
- expectedTextMap.put("x-datadog-tags", expectedDatadogTags)
+ if (!datadogTags.empty) {
+ expectedTextMap.put("x-datadog-tags", datadogTags.join(','))
}
textMap == expectedTextMap
diff --git a/dd-java-agent/instrumentation/opentracing/opentracing-0.32/src/test/groovy/OpenTracing32Test.groovy b/dd-java-agent/instrumentation/opentracing/opentracing-0.32/src/test/groovy/OpenTracing32Test.groovy
index 09e01fad297..e262f947a14 100644
--- a/dd-java-agent/instrumentation/opentracing/opentracing-0.32/src/test/groovy/OpenTracing32Test.groovy
+++ b/dd-java-agent/instrumentation/opentracing/opentracing-0.32/src/test/groovy/OpenTracing32Test.groovy
@@ -2,6 +2,7 @@ import datadog.trace.agent.test.InstrumentationSpecification
import datadog.trace.api.DDSpanId
import datadog.trace.api.DDTags
import datadog.trace.api.DDTraceId
+import datadog.trace.api.internal.util.LongStringUtils
import datadog.trace.api.interceptor.MutableSpan
import datadog.trace.bootstrap.instrumentation.api.AgentTracer
import datadog.trace.bootstrap.instrumentation.api.ResourceNamePriorities
@@ -299,12 +300,21 @@ class OpenTracing32Test extends InstrumentationSpecification {
"-${DDSpanId.toHexStringPadded(context.delegate.spanId)}" +
"-" + (propagatedPriority > 0 ? "01" : "00")
def expectedTracestate = "dd=s:${propagatedPriority};p:${DDSpanId.toHexStringPadded(context.delegate.spanId)}"
- def expectedDatadogTags = null
+ def datadogTags = []
if (propagatedPriority > 0) {
def effectiveSamplingMechanism = contextPriority == UNSET ? AGENT_RATE : samplingMechanism
- expectedDatadogTags = "_dd.p.dm=-" + effectiveSamplingMechanism
+ datadogTags << "_dd.p.dm=-" + effectiveSamplingMechanism
expectedTracestate+= ";t.dm:-" + effectiveSamplingMechanism
}
+ def traceId = context.delegate.traceId as DDTraceId
+ if (traceId.toHighOrderLong() != 0) {
+ expectedTracestate+= ";t.tid:${traceId.toHexStringPadded(32).substring(0, 16)}"
+ datadogTags << "_dd.p.tid=" + LongStringUtils.toHexStringPadded(traceId.toHighOrderLong(), 16)
+ }
+ if (contextPriority == UNSET) {
+ expectedTracestate+= ";t.ksr:1"
+ datadogTags << "_dd.p.ksr=1"
+ }
def expectedTextMap = [
"x-datadog-trace-id" : "$context.delegate.traceId",
"x-datadog-parent-id" : "$context.delegate.spanId",
@@ -312,8 +322,8 @@ class OpenTracing32Test extends InstrumentationSpecification {
"traceparent" : expectedTraceparent,
"tracestate" : expectedTracestate
]
- if (expectedDatadogTags != null) {
- expectedTextMap.put("x-datadog-tags", expectedDatadogTags)
+ if (!datadogTags.empty) {
+ expectedTextMap.put("x-datadog-tags", datadogTags.join(','))
}
textMap == expectedTextMap
diff --git a/dd-trace-core/src/jmh/java/datadog/trace/core/propagation/ptags/KnuthSamplingRateFormatBenchmark.java b/dd-trace-core/src/jmh/java/datadog/trace/core/propagation/ptags/KnuthSamplingRateFormatBenchmark.java
new file mode 100644
index 00000000000..238b80f79a4
--- /dev/null
+++ b/dd-trace-core/src/jmh/java/datadog/trace/core/propagation/ptags/KnuthSamplingRateFormatBenchmark.java
@@ -0,0 +1,114 @@
+package datadog.trace.core.propagation.ptags;
+
+import static java.util.concurrent.TimeUnit.SECONDS;
+
+import datadog.trace.core.propagation.PropagationTags;
+import java.util.Locale;
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.BenchmarkMode;
+import org.openjdk.jmh.annotations.Fork;
+import org.openjdk.jmh.annotations.Level;
+import org.openjdk.jmh.annotations.Measurement;
+import org.openjdk.jmh.annotations.Mode;
+import org.openjdk.jmh.annotations.OutputTimeUnit;
+import org.openjdk.jmh.annotations.Param;
+import org.openjdk.jmh.annotations.Scope;
+import org.openjdk.jmh.annotations.Setup;
+import org.openjdk.jmh.annotations.State;
+import org.openjdk.jmh.annotations.Threads;
+import org.openjdk.jmh.annotations.Warmup;
+import org.openjdk.jmh.infra.Blackhole;
+
+/**
+ * Benchmarks for formatting the Knuth sampling rate (_dd.p.ksr tag value).
+ *
+ *
The format requirement is %.6g semantics: 6 significant figures, no trailing zeros, using
+ * fixed notation for values in [1e-4, 1] and scientific notation for smaller values.
+ *
+ *
Run with:
+ *
+ *
+ * ./gradlew :dd-trace-core:jmhJar
+ * java -jar dd-trace-core/build/libs/dd-trace-core-*-jmh.jar KnuthSamplingRateFormatBenchmark \
+ * -t 8 -prof gc
+ *
+ *
+ * Use {@code -t 8} (or higher) to surface GC pressure from multi-threaded allocation. Use {@code
+ * -prof gc} to see alloc rate (bytes/op) per benchmark — that's the primary signal for whether the
+ * hot path is allocation-free.
+ */
+@State(Scope.Thread)
+@Warmup(iterations = 3, time = 10, timeUnit = SECONDS)
+@Measurement(iterations = 5, time = 10, timeUnit = SECONDS)
+@BenchmarkMode(Mode.Throughput)
+@OutputTimeUnit(SECONDS)
+@Threads(8)
+@Fork(value = 1)
+public class KnuthSamplingRateFormatBenchmark {
+
+ /**
+ * Representative sampling rates. Most real-world rates are in [0.001, 1.0]. The 0.0001 value
+ * exercises the edge of the fixed-notation range.
+ */
+ @Param({"0.5", "0.1", "0.01", "0.001", "0.0001", "0.123456789", "0.999999"})
+ double rate;
+
+ PTagsFactory.PTags ptags;
+
+ @Setup(Level.Trial)
+ public void setUp() {
+ ptags = (PTagsFactory.PTags) PropagationTags.factory().empty();
+ ptags.updateKnuthSamplingRate(rate);
+ }
+
+ /** Baseline: old implementation using String.format + substring trimming. */
+ @Benchmark
+ public void stringFormat(Blackhole bh) {
+ bh.consume(stringFormatImpl(rate));
+ }
+
+ /** Custom formatter: char-array arithmetic, no Formatter allocation. */
+ @Benchmark
+ public void customFormat(Blackhole bh) {
+ bh.consume(PTagsFactory.PTags.formatKnuthSamplingRate(rate));
+ }
+
+ /**
+ * Cached TagValue: the full getKnuthSamplingRateTagValue() hot-path after caching. Should be
+ * near-zero allocation (volatile read only).
+ */
+ @Benchmark
+ public void cachedTagValue(Blackhole bh) {
+ bh.consume(ptags.getKnuthSamplingRateTagValue());
+ }
+
+ /**
+ * Models the per-trace allocation cost: resets the instance cache (simulating a new PTags), then
+ * calls updateKnuthSamplingRate. This is what every trace root pays. With the static cache
+ * applied, this should also be near-zero allocation after warmup.
+ */
+ @Benchmark
+ public void updateRateFreshTrace(Blackhole bh) {
+ ptags.updateKnuthSamplingRate(Double.NaN); // reset instance cache, like a new PTags
+ ptags.updateKnuthSamplingRate(rate);
+ bh.consume(ptags.getKnuthSamplingRateTagValue());
+ }
+
+ // ---- old implementation for comparison ----
+
+ static String stringFormatImpl(double rate) {
+ String formatted = String.format(Locale.ROOT, "%.6g", rate);
+ int dotIndex = formatted.indexOf('.');
+ if (dotIndex >= 0) {
+ int end = formatted.length();
+ while (end > dotIndex + 1 && formatted.charAt(end - 1) == '0') {
+ end--;
+ }
+ if (formatted.charAt(end - 1) == '.') {
+ end--;
+ }
+ formatted = formatted.substring(0, end);
+ }
+ return formatted;
+ }
+}
diff --git a/dd-trace-core/src/main/java/datadog/trace/core/DDSpan.java b/dd-trace-core/src/main/java/datadog/trace/core/DDSpan.java
index d460d5ea1bb..772cbe90866 100644
--- a/dd-trace-core/src/main/java/datadog/trace/core/DDSpan.java
+++ b/dd-trace-core/src/main/java/datadog/trace/core/DDSpan.java
@@ -646,6 +646,12 @@ public DDSpan setSamplingPriority(
int samplingPriority, CharSequence rate, double sampleRate, int samplingMechanism) {
if (context.setSamplingPriority(samplingPriority, samplingMechanism)) {
setMetric(rate, sampleRate);
+ if (samplingMechanism == SamplingMechanism.AGENT_RATE
+ || samplingMechanism == SamplingMechanism.LOCAL_USER_RULE
+ || samplingMechanism == SamplingMechanism.REMOTE_USER_RULE
+ || samplingMechanism == SamplingMechanism.REMOTE_ADAPTIVE_RULE) {
+ context.getPropagationTags().updateKnuthSamplingRate(sampleRate);
+ }
}
return this;
}
diff --git a/dd-trace-core/src/main/java/datadog/trace/core/propagation/PropagationTags.java b/dd-trace-core/src/main/java/datadog/trace/core/propagation/PropagationTags.java
index aea99b16f77..05576f9cceb 100644
--- a/dd-trace-core/src/main/java/datadog/trace/core/propagation/PropagationTags.java
+++ b/dd-trace-core/src/main/java/datadog/trace/core/propagation/PropagationTags.java
@@ -130,6 +130,16 @@ public interface Factory {
public abstract String getDebugPropagation();
+ /**
+ * Updates the Knuth sampling rate (_dd.p.ksr) propagated tag. This records the sampling rate that
+ * was applied when making an agent-based or rule-based sampling decision. The rate is formatted
+ * with up to 6 significant digits and no trailing zeros, matching the Go/Python reference
+ * implementations (%.6g format).
+ *
+ * @param rate the sampling rate value
+ */
+ public abstract void updateKnuthSamplingRate(double rate);
+
public HashMap createTagMap() {
HashMap result = new HashMap<>();
fillTagMap(result);
diff --git a/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/PTagsCodec.java b/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/PTagsCodec.java
index 2dde4f84a87..9447cb5d021 100644
--- a/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/PTagsCodec.java
+++ b/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/PTagsCodec.java
@@ -18,6 +18,7 @@ abstract class PTagsCodec {
protected static final TagKey TRACE_ID_TAG = TagKey.from("tid");
protected static final TagKey TRACE_SOURCE_TAG = TagKey.from("ts");
protected static final TagKey DEBUG_TAG = TagKey.from("debug");
+ protected static final TagKey KNUTH_SAMPLING_RATE_TAG = TagKey.from("ksr");
protected static final String PROPAGATION_ERROR_MALFORMED_TID = "malformed_tid ";
protected static final String PROPAGATION_ERROR_INCONSISTENT_TID = "inconsistent_tid ";
protected static final TagKey UPSTREAM_SERVICES_DEPRECATED_TAG = TagKey.from("upstream_services");
@@ -49,6 +50,11 @@ static String headerValue(PTagsCodec codec, PTags ptags) {
if (ptags.getDebugPropagation() != null) {
size = codec.appendTag(sb, DEBUG_TAG, TagValue.from(ptags.getDebugPropagation()), size);
}
+ if (ptags.getKnuthSamplingRateTagValue() != null) {
+ size =
+ codec.appendTag(
+ sb, KNUTH_SAMPLING_RATE_TAG, ptags.getKnuthSamplingRateTagValue(), size);
+ }
Iterator it = ptags.getTagPairs().iterator();
while (it.hasNext() && !codec.isTooLarge(sb, size)) {
TagElement tagKey = it.next();
@@ -103,6 +109,11 @@ static void fillTagMap(PTags propagationTags, Map tagMap) {
tagMap.put(
DEBUG_TAG.forType(Encoding.DATADOG).toString(), propagationTags.getDebugPropagation());
}
+ if (propagationTags.getKnuthSamplingRateTagValue() != null) {
+ tagMap.put(
+ KNUTH_SAMPLING_RATE_TAG.forType(Encoding.DATADOG).toString(),
+ propagationTags.getKnuthSamplingRateTagValue().forType(Encoding.DATADOG).toString());
+ }
if (propagationTags.getTraceIdHighOrderBitsHexTagValue() != null) {
tagMap.put(
TRACE_ID_TAG.forType(Encoding.DATADOG).toString(),
diff --git a/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/PTagsFactory.java b/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/PTagsFactory.java
index 203e4ee6681..f6d817b47f5 100644
--- a/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/PTagsFactory.java
+++ b/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/PTagsFactory.java
@@ -3,6 +3,7 @@
import static datadog.trace.core.propagation.PropagationTags.HeaderType.DATADOG;
import static datadog.trace.core.propagation.PropagationTags.HeaderType.W3C;
import static datadog.trace.core.propagation.ptags.PTagsCodec.DECISION_MAKER_TAG;
+import static datadog.trace.core.propagation.ptags.PTagsCodec.KNUTH_SAMPLING_RATE_TAG;
import static datadog.trace.core.propagation.ptags.PTagsCodec.TRACE_ID_TAG;
import static datadog.trace.core.propagation.ptags.PTagsCodec.TRACE_SOURCE_TAG;
@@ -90,6 +91,15 @@ static class PTags extends PropagationTags {
private volatile int traceSource;
private volatile String debugPropagation;
+ private volatile double knuthSamplingRate = Double.NaN;
+ private volatile TagValue knuthSamplingRateTagValue;
+
+ // Static cache for the most-recently-seen rate → TagValue. In steady state a service uses one
+ // rate, so this eliminates the char[] + String allocation on every new PTags instance.
+ // Writes are benign-racy: two threads computing the same rate produce equal TagValues.
+ private static volatile double cachedKsrRate = Double.NaN;
+ private static volatile TagValue cachedKsrTagValue;
+
// xDatadogTagsSize of the tagPairs, does not include the decision maker tag
private volatile int xDatadogTagsSize = -1;
@@ -265,6 +275,161 @@ public String getDebugPropagation() {
return debugPropagation;
}
+ @Override
+ public void updateKnuthSamplingRate(double rate) {
+ if (Double.compare(knuthSamplingRate, rate) != 0) {
+ clearCachedHeader(DATADOG);
+ clearCachedHeader(W3C);
+ knuthSamplingRate = rate;
+ if (Double.isNaN(rate)) {
+ knuthSamplingRateTagValue = null;
+ } else {
+ TagValue tv;
+ if (Double.compare(cachedKsrRate, rate) == 0) {
+ tv = cachedKsrTagValue;
+ } else {
+ tv = TagValue.from(formatKnuthSamplingRate(rate));
+ cachedKsrTagValue = tv;
+ cachedKsrRate = rate;
+ }
+ knuthSamplingRateTagValue = tv;
+ }
+ }
+ }
+
+ /**
+ * Formats a sampling rate with up to 6 significant digits and no trailing zeros, matching
+ * {@code %.6g} semantics (fixed notation for values in [1e-4, 1], scientific for smaller).
+ *
+ * Uses char-array arithmetic to avoid {@link java.util.Formatter} allocations entirely.
+ */
+ static String formatKnuthSamplingRate(double rate) {
+ if (rate <= 0.0) return "0";
+ if (rate >= 1.0) return "1";
+
+ if (rate < 1e-4) {
+ return formatScientific6g(rate);
+ }
+
+ return formatFixed6g(rate);
+ }
+
+ /** Fixed notation for rates in [1e-4, 1): "0.DDDDDDDDD" with trailing zeros trimmed. */
+ private static String formatFixed6g(double rate) {
+ // Choose a multiplier so Math.round(rate * multiplier) is a 6-significant-figure integer.
+ // For rate in [10^-k, 10^-(k-1)) the first sig fig is at decimal position k, so we need
+ // k+5 total fractional digits:
+ // [0.1, 1.0) -> scale=6, multiplier=1e6
+ // [0.01, 0.1) -> scale=7, multiplier=1e7
+ // [0.001, 0.01) -> scale=8, multiplier=1e8
+ // [1e-4, 0.001) -> scale=9, multiplier=1e9
+ final int scale;
+ final long multiplier;
+ if (rate >= 0.1) {
+ scale = 6;
+ multiplier = 1_000_000L;
+ } else if (rate >= 0.01) {
+ scale = 7;
+ multiplier = 10_000_000L;
+ } else if (rate >= 0.001) {
+ scale = 8;
+ multiplier = 100_000_000L;
+ } else {
+ scale = 9;
+ multiplier = 1_000_000_000L;
+ }
+
+ long rounded = Math.round(rate * multiplier);
+ if (rounded == 0) return "0";
+ if (rounded >= multiplier) return "1"; // rounding pushed value to 1.0
+
+ // Build "0." + and trim trailing zeros in a single right-to-left pass.
+ char[] buf = new char[2 + scale];
+ buf[0] = '0';
+ buf[1] = '.';
+ int end = 2; // exclusive end; updated on first non-zero digit found from the right
+ for (int i = 2 + scale - 1; i >= 2; i--) {
+ int d = (int) (rounded % 10);
+ rounded /= 10;
+ buf[i] = (char) ('0' + d);
+ if (d != 0 && end == 2) {
+ end = i + 1;
+ }
+ }
+
+ return new String(buf, 0, end);
+ }
+
+ /** Scientific notation for rates below 1e-4: "X.XXXXXe-YY" with mantissa zeros trimmed. */
+ private static String formatScientific6g(double rate) {
+ // Normalize to [1, 10) by repeated multiply — at most ~15 iterations for realistic rates.
+ int exp = 0;
+ double normalized = rate;
+ while (normalized < 1.0) {
+ normalized *= 10;
+ exp--;
+ }
+
+ // Round mantissa to 6 significant figures (integer in [100000, 999999]).
+ long sig = Math.round(normalized * 100000.0);
+ if (sig >= 1000000) {
+ sig /= 10;
+ exp++;
+ if (exp >= -4) {
+ // Rounding pushed the value into fixed-notation range (always exactly 0.0001).
+ return "0.0001";
+ }
+ }
+
+ // Build "X.XXXXXe-YY" trimming mantissa trailing zeros.
+ // Max: "X.XXXXXe-XXX" = 13 chars.
+ char[] buf = new char[13];
+ int pos = 0;
+
+ // Integer part (always a single digit 1-9).
+ buf[pos++] = (char) ('0' + (int) (sig / 100000));
+ sig %= 100000;
+
+ // Fractional part (5 digits, trim trailing zeros).
+ if (sig > 0) {
+ buf[pos++] = '.';
+ char[] frac = new char[5];
+ int fracEnd = 0;
+ for (int i = 4; i >= 0; i--) {
+ frac[i] = (char) ('0' + (int) (sig % 10));
+ sig /= 10;
+ if (frac[i] != '0' && fracEnd == 0) {
+ fracEnd = i + 1;
+ }
+ }
+ for (int i = 0; i < fracEnd; i++) {
+ buf[pos++] = frac[i];
+ }
+ }
+
+ // Exponent: "e-YY" (always negative here, at least 2 digits).
+ buf[pos++] = 'e';
+ buf[pos++] = '-';
+ int absExp = -exp;
+ if (absExp < 10) {
+ buf[pos++] = '0';
+ buf[pos++] = (char) ('0' + absExp);
+ } else if (absExp < 100) {
+ buf[pos++] = (char) ('0' + absExp / 10);
+ buf[pos++] = (char) ('0' + absExp % 10);
+ } else {
+ buf[pos++] = (char) ('0' + absExp / 100);
+ buf[pos++] = (char) ('0' + (absExp / 10) % 10);
+ buf[pos++] = (char) ('0' + absExp % 10);
+ }
+
+ return new String(buf, 0, pos);
+ }
+
+ TagValue getKnuthSamplingRateTagValue() {
+ return knuthSamplingRateTagValue;
+ }
+
@Override
public int getSamplingPriority() {
return samplingPriority;
@@ -390,6 +555,9 @@ int getXDatadogTagsSize() {
size = PTagsCodec.calcXDatadogTagsSize(getTagPairs());
size = PTagsCodec.calcXDatadogTagsSize(size, DECISION_MAKER_TAG, decisionMakerTagValue);
size = PTagsCodec.calcXDatadogTagsSize(size, TRACE_ID_TAG, traceIdHighOrderBitsHexTagValue);
+ size =
+ PTagsCodec.calcXDatadogTagsSize(
+ size, KNUTH_SAMPLING_RATE_TAG, getKnuthSamplingRateTagValue());
int currentProductTraceSource = traceSource;
if (currentProductTraceSource != ProductTraceSource.UNSET) {
size =
diff --git a/dd-trace-core/src/test/groovy/datadog/trace/common/writer/DDAgentApiTest.groovy b/dd-trace-core/src/test/groovy/datadog/trace/common/writer/DDAgentApiTest.groovy
index 270d47569e3..a6ec2ec5cba 100644
--- a/dd-trace-core/src/test/groovy/datadog/trace/common/writer/DDAgentApiTest.groovy
+++ b/dd-trace-core/src/test/groovy/datadog/trace/common/writer/DDAgentApiTest.groovy
@@ -164,7 +164,7 @@ class DDAgentApiTest extends DDCoreSpecification {
[[buildSpan(1L, "service.name", "my-service", PropagationTags.factory().fromHeaderValue(PropagationTags.HeaderType.DATADOG, "_dd.p.usr=123"))]] | [[new TreeMap<>([
"duration" : 10,
"error" : 0,
- "meta" : ["thread.name": Thread.currentThread().getName(), "_dd.p.usr": "123", "_dd.p.dm": "-1", "_dd.svc_src" : "m"] +
+ "meta" : ["thread.name": Thread.currentThread().getName(), "_dd.p.usr": "123", "_dd.p.dm": "-1", "_dd.p.ksr": "1", "_dd.svc_src" : "m"] +
(Config.get().isExperimentalPropagateProcessTagsEnabled() ? ["_dd.tags.process" : ProcessTags.getTagsForSerialization().toString()] : []),
"metrics" : [
(DDSpanContext.PRIORITY_SAMPLING_KEY) : 1,
@@ -185,7 +185,7 @@ class DDAgentApiTest extends DDCoreSpecification {
[[buildSpan(100L, "resource.name", "my-resource", PropagationTags.factory().fromHeaderValue(PropagationTags.HeaderType.DATADOG, "_dd.p.usr=123"))]] | [[new TreeMap<>([
"duration" : 10,
"error" : 0,
- "meta" : ["thread.name": Thread.currentThread().getName(), "_dd.p.usr": "123", "_dd.p.dm": "-1"] +
+ "meta" : ["thread.name": Thread.currentThread().getName(), "_dd.p.usr": "123", "_dd.p.dm": "-1", "_dd.p.ksr": "1"] +
(Config.get().isExperimentalPropagateProcessTagsEnabled() ? ["_dd.tags.process" : ProcessTags.getTagsForSerialization().toString()] : []),
"metrics" : [
(DDSpanContext.PRIORITY_SAMPLING_KEY) : 1,
diff --git a/dd-trace-core/src/test/groovy/datadog/trace/core/KnuthSamplingRateTest.groovy b/dd-trace-core/src/test/groovy/datadog/trace/core/KnuthSamplingRateTest.groovy
new file mode 100644
index 00000000000..7c8ccacf7bd
--- /dev/null
+++ b/dd-trace-core/src/test/groovy/datadog/trace/core/KnuthSamplingRateTest.groovy
@@ -0,0 +1,229 @@
+package datadog.trace.core
+
+import datadog.trace.common.sampling.PrioritySampler
+import datadog.trace.common.sampling.RateByServiceTraceSampler
+import datadog.trace.common.sampling.Sampler
+import datadog.trace.common.writer.ListWriter
+import datadog.trace.common.writer.ddagent.DDAgentApi
+import datadog.trace.core.propagation.PropagationTags
+import datadog.trace.core.test.DDCoreSpecification
+
+import static datadog.trace.api.config.TracerConfig.TRACE_RATE_LIMIT
+import static datadog.trace.api.config.TracerConfig.TRACE_SAMPLE_RATE
+import static datadog.trace.api.config.TracerConfig.TRACE_SAMPLING_RULES
+import static datadog.trace.api.config.TracerConfig.TRACE_SAMPLING_SERVICE_RULES
+import static datadog.trace.api.sampling.PrioritySampling.SAMPLER_KEEP
+
+class KnuthSamplingRateTest extends DDCoreSpecification {
+ static serializer = DDAgentApi.RESPONSE_ADAPTER
+
+ def "updateKnuthSamplingRate formats rate correctly"() {
+ setup:
+ def pTags = PropagationTags.factory().empty()
+
+ when:
+ pTags.updateKnuthSamplingRate(rate)
+ def tagMap = pTags.createTagMap()
+
+ then:
+ tagMap.get('_dd.p.ksr') == expected
+
+ where:
+ rate | expected
+ 1.0d | "1"
+ 0.5d | "0.5"
+ 0.1d | "0.1"
+ 0.0d | "0"
+ 0.765432d | "0.765432"
+ 0.7654321d | "0.765432"
+ 0.123456d | "0.123456"
+ 0.100000d | "0.1"
+ 0.250d | "0.25"
+ // [0.01, 0.1) magnitude bucket (scale=7)
+ 0.05d | "0.05"
+ 0.0123456789d | "0.0123457"
+ // [0.001, 0.01) magnitude bucket (scale=8)
+ 0.001d | "0.001"
+ 0.00500d | "0.005"
+ 0.00123456789d | "0.00123457"
+ // [0.0001, 0.001) magnitude bucket (scale=9)
+ 0.0001d | "0.0001"
+ 0.000500d | "0.0005"
+ 0.000123456789d | "0.000123457"
+ // rounding boundary: 0.9999995 rounds up to 1.0
+ 0.9999995d | "1"
+ // scientific notation (rate < 1e-4)
+ 0.00001d | "1e-05"
+ 0.000050d | "5e-05"
+ 1.23456789e-5d | "1.23457e-05"
+ 1e-7d | "1e-07"
+ 5.5e-10d | "5.5e-10"
+ }
+
+ def "agent rate sampler sets ksr propagated tag"() {
+ setup:
+ def serviceSampler = new RateByServiceTraceSampler()
+ def tracer = tracerBuilder().writer(new ListWriter()).build()
+ String response = '{"rate_by_service": {"service:,env:":' + rate + '}}'
+ serviceSampler.onResponse("traces", serializer.fromJson(response))
+
+ when:
+ DDSpan span = tracer.buildSpan("fakeOperation")
+ .withServiceName("spock")
+ .withTag("env", "test")
+ .ignoreActiveSpan().start()
+ serviceSampler.setSamplingPriority(span)
+
+ def propagationMap = span.context.propagationTags.createTagMap()
+ def ksr = propagationMap.get('_dd.p.ksr')
+
+ then:
+ ksr == expectedKsr
+
+ cleanup:
+ tracer.close()
+
+ where:
+ rate | expectedKsr
+ 1.0 | "1"
+ 0.5 | "0.5"
+ 0.0 | "0"
+ }
+
+ def "rule-based sampler sets ksr propagated tag when rule matches"() {
+ setup:
+ Properties properties = new Properties()
+ properties.setProperty(TRACE_SAMPLING_RULES, jsonRules)
+ properties.setProperty(TRACE_RATE_LIMIT, "50")
+ def tracer = tracerBuilder().writer(new ListWriter()).build()
+
+ when:
+ Sampler sampler = Sampler.Builder.forConfig(properties)
+ DDSpan span = tracer.buildSpan("operation")
+ .withServiceName("service")
+ .withTag("env", "bar")
+ .ignoreActiveSpan().start()
+ ((PrioritySampler) sampler).setSamplingPriority(span)
+
+ def propagationMap = span.context.propagationTags.createTagMap()
+ def ksr = propagationMap.get('_dd.p.ksr')
+
+ then:
+ ksr == expectedKsr
+
+ cleanup:
+ tracer.close()
+
+ where:
+ jsonRules | expectedKsr
+ // Matching rule with rate 1 -> ksr is "1"
+ '[{"service": "service", "sample_rate": 1}]' | "1"
+ // Matching rule with rate 0.5 -> ksr is "0.5"
+ '[{"service": "service", "sample_rate": 0.5}]' | "0.5"
+ // Matching rule with rate 0 -> ksr is "0" (drop, but ksr still set)
+ '[{"service": "service", "sample_rate": 0}]' | "0"
+ }
+
+ def "rule-based sampler fallback to agent sampler sets ksr"() {
+ setup:
+ Properties properties = new Properties()
+ // Rule that does NOT match "service"
+ properties.setProperty(TRACE_SAMPLING_RULES, '[{"service": "nomatch", "sample_rate": 0.5}]')
+ properties.setProperty(TRACE_RATE_LIMIT, "50")
+ def tracer = tracerBuilder().writer(new ListWriter()).build()
+
+ when:
+ Sampler sampler = Sampler.Builder.forConfig(properties)
+ DDSpan span = tracer.buildSpan("operation")
+ .withServiceName("service")
+ .withTag("env", "bar")
+ .ignoreActiveSpan().start()
+ ((PrioritySampler) sampler).setSamplingPriority(span)
+
+ def propagationMap = span.context.propagationTags.createTagMap()
+ def ksr = propagationMap.get('_dd.p.ksr')
+
+ then:
+ // When falling back to agent sampler, ksr should still be set (agent rate = 1.0 by default)
+ ksr == "1"
+ span.getSamplingPriority() == SAMPLER_KEEP
+
+ cleanup:
+ tracer.close()
+ }
+
+ def "service rule sampler sets ksr propagated tag"() {
+ setup:
+ Properties properties = new Properties()
+ properties.setProperty(TRACE_SAMPLING_SERVICE_RULES, "service:0.75")
+ properties.setProperty(TRACE_RATE_LIMIT, "50")
+ def tracer = tracerBuilder().writer(new ListWriter()).build()
+
+ when:
+ Sampler sampler = Sampler.Builder.forConfig(properties)
+ DDSpan span = tracer.buildSpan("operation")
+ .withServiceName("service")
+ .withTag("env", "bar")
+ .ignoreActiveSpan().start()
+ ((PrioritySampler) sampler).setSamplingPriority(span)
+
+ def propagationMap = span.context.propagationTags.createTagMap()
+ def ksr = propagationMap.get('_dd.p.ksr')
+
+ then:
+ ksr == "0.75"
+
+ cleanup:
+ tracer.close()
+ }
+
+ def "default rate sampler sets ksr propagated tag"() {
+ setup:
+ Properties properties = new Properties()
+ properties.setProperty(TRACE_SAMPLE_RATE, "0.25")
+ properties.setProperty(TRACE_RATE_LIMIT, "50")
+ def tracer = tracerBuilder().writer(new ListWriter()).build()
+
+ when:
+ Sampler sampler = Sampler.Builder.forConfig(properties)
+ DDSpan span = tracer.buildSpan("operation")
+ .withServiceName("service")
+ .withTag("env", "bar")
+ .ignoreActiveSpan().start()
+ ((PrioritySampler) sampler).setSamplingPriority(span)
+
+ def propagationMap = span.context.propagationTags.createTagMap()
+ def ksr = propagationMap.get('_dd.p.ksr')
+
+ then:
+ ksr == "0.25"
+
+ cleanup:
+ tracer.close()
+ }
+
+ def "ksr is propagated via x-datadog-tags header"() {
+ setup:
+ def serviceSampler = new RateByServiceTraceSampler()
+ def tracer = tracerBuilder().writer(new ListWriter()).build()
+ String response = '{"rate_by_service": {"service:,env:":0.5}}'
+ serviceSampler.onResponse("traces", serializer.fromJson(response))
+
+ when:
+ DDSpan span = tracer.buildSpan("fakeOperation")
+ .withServiceName("spock")
+ .withTag("env", "test")
+ .ignoreActiveSpan().start()
+ serviceSampler.setSamplingPriority(span)
+
+ def headerValue = span.context.propagationTags.headerValue(
+ datadog.trace.core.propagation.PropagationTags.HeaderType.DATADOG)
+
+ then:
+ headerValue != null
+ headerValue.contains("_dd.p.ksr=0.5")
+
+ cleanup:
+ tracer.close()
+ }
+}
diff --git a/dd-trace-ot/src/ot31CompatibilityTest/groovy/OT31ApiTest.groovy b/dd-trace-ot/src/ot31CompatibilityTest/groovy/OT31ApiTest.groovy
index 7e3b2ab110f..10966d67d84 100644
--- a/dd-trace-ot/src/ot31CompatibilityTest/groovy/OT31ApiTest.groovy
+++ b/dd-trace-ot/src/ot31CompatibilityTest/groovy/OT31ApiTest.groovy
@@ -101,7 +101,8 @@ class OT31ApiTest extends DDSpecification {
def effectiveSamplingMechanism = contextPriority == UNSET ? AGENT_RATE : samplingMechanism
def expectedTracestate = "dd=s:${propagatedPriority};p:${DDSpanId.toHexStringPadded(spanId)}" +
(propagatedPriority > 0 ? ";t.dm:-" + effectiveSamplingMechanism : "") +
- ";t.tid:${traceId.toHexStringPadded(32).substring(0, 16)}"
+ ";t.tid:${traceId.toHexStringPadded(32).substring(0, 16)}" +
+ (contextPriority == UNSET ? ";t.ksr:1" : "")
def expectedTextMap = [
"x-datadog-trace-id" : context.toTraceId(),
"x-datadog-parent-id" : context.toSpanId(),
@@ -116,6 +117,9 @@ class OT31ApiTest extends DDSpecification {
if (traceId.toHighOrderLong() != 0) {
datadogTags << "_dd.p.tid=" + LongStringUtils.toHexStringPadded(traceId.toHighOrderLong(), 16)
}
+ if (contextPriority == UNSET) {
+ datadogTags << "_dd.p.ksr=1"
+ }
if (!datadogTags.empty) {
expectedTextMap.put("x-datadog-tags", datadogTags.join(','))
}
diff --git a/dd-trace-ot/src/ot33CompatibilityTest/groovy/OT33ApiTest.groovy b/dd-trace-ot/src/ot33CompatibilityTest/groovy/OT33ApiTest.groovy
index 163a8ca2aae..0ce61afec90 100644
--- a/dd-trace-ot/src/ot33CompatibilityTest/groovy/OT33ApiTest.groovy
+++ b/dd-trace-ot/src/ot33CompatibilityTest/groovy/OT33ApiTest.groovy
@@ -90,7 +90,8 @@ class OT33ApiTest extends DDSpecification {
def effectiveSamplingMechanism = contextPriority == UNSET ? AGENT_RATE : samplingMechanism
def expectedTracestate = "dd=s:${propagatedPriority};p:${DDSpanId.toHexStringPadded(spanId)}" +
(propagatedPriority > 0 ? ";t.dm:-" + effectiveSamplingMechanism : "") +
- ";t.tid:${traceId.toHexStringPadded(32).substring(0, 16)}"
+ ";t.tid:${traceId.toHexStringPadded(32).substring(0, 16)}" +
+ (contextPriority == UNSET ? ";t.ksr:1" : "")
def expectedTextMap = [
"x-datadog-trace-id" : context.toTraceId(),
"x-datadog-parent-id" : context.toSpanId(),
@@ -105,6 +106,9 @@ class OT33ApiTest extends DDSpecification {
if (traceId.toHighOrderLong() != 0) {
datadogTags << "_dd.p.tid=" + LongStringUtils.toHexStringPadded(traceId.toHighOrderLong(), 16)
}
+ if (contextPriority == UNSET) {
+ datadogTags << "_dd.p.ksr=1"
+ }
if (!datadogTags.empty) {
expectedTextMap.put("x-datadog-tags", datadogTags.join(','))
}