From 684a837c8ef182b30d6df9a07d1c30262aa46c29 Mon Sep 17 00:00:00 2001 From: bm1549 Date: Tue, 10 Mar 2026 21:44:57 -0400 Subject: [PATCH 1/8] Add _dd.p.ksr propagated tag for Knuth sampling rate Co-Authored-By: Claude Opus 4.6 --- .../main/java/datadog/trace/core/DDSpan.java | 22 ++ .../core/propagation/PropagationTags.java | 8 + .../core/propagation/ptags/PTagsCodec.java | 11 + .../core/propagation/ptags/PTagsFactory.java | 21 ++ .../trace/core/KnuthSamplingRateTest.groovy | 205 ++++++++++++++++++ 5 files changed, 267 insertions(+) create mode 100644 dd-trace-core/src/test/groovy/datadog/trace/core/KnuthSamplingRateTest.groovy 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..98446f3d153 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,10 +646,32 @@ 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(formatKnuthSamplingRate(sampleRate)); + } } return this; } + /** + * Formats the sampling rate for the _dd.p.ksr propagated tag. Uses up to 6 significant digits + * with no trailing zeros, matching the Go/Python reference implementations (%.6g format). + */ + static String formatKnuthSamplingRate(double rate) { + // Use %.6g format: up to 6 significant digits, no trailing zeros + // This matches Go's strconv.FormatFloat(rate, 'g', 6, 64) and Python's f"{rate:.6g}" + String formatted = String.format("%.6g", rate); + // Remove trailing zeros after decimal point + if (formatted.contains(".")) { + formatted = formatted.replaceAll("0+$", ""); + formatted = formatted.replaceAll("\\.$", ""); + } + return formatted; + } + @Override public DDSpan setSpanSamplingPriority(double rate, int limit) { context.setSpanSamplingPriority(rate, limit); 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..7a202bd49f6 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,14 @@ 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. + * + * @param rate the formatted sampling rate string (up to 6 significant digits, no trailing zeros) + */ + public abstract void updateKnuthSamplingRate(String 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..091dee1f6de 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,9 @@ static class PTags extends PropagationTags { private volatile int traceSource; private volatile String debugPropagation; + // extracted Knuth sampling rate tag for easier updates + private volatile TagValue knuthSamplingRateTagValue; + // xDatadogTagsSize of the tagPairs, does not include the decision maker tag private volatile int xDatadogTagsSize = -1; @@ -265,6 +269,20 @@ public String getDebugPropagation() { return debugPropagation; } + @Override + public void updateKnuthSamplingRate(String rate) { + TagValue newValue = rate == null ? null : TagValue.from(rate); + if (!java.util.Objects.equals(knuthSamplingRateTagValue, newValue)) { + clearCachedHeader(DATADOG); + clearCachedHeader(W3C); + } + knuthSamplingRateTagValue = newValue; + } + + TagValue getKnuthSamplingRateTagValue() { + return knuthSamplingRateTagValue; + } + @Override public int getSamplingPriority() { return samplingPriority; @@ -390,6 +408,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, knuthSamplingRateTagValue); int currentProductTraceSource = traceSource; if (currentProductTraceSource != ProductTraceSource.UNSET) { size = 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..7819d19f1c9 --- /dev/null +++ b/dd-trace-core/src/test/groovy/datadog/trace/core/KnuthSamplingRateTest.groovy @@ -0,0 +1,205 @@ +package datadog.trace.core + +import datadog.trace.common.sampling.PrioritySampler +import datadog.trace.common.sampling.RateByServiceTraceSampler +import datadog.trace.common.sampling.RuleBasedTraceSampler +import datadog.trace.common.sampling.Sampler +import datadog.trace.common.writer.ListWriter +import datadog.trace.common.writer.ddagent.DDAgentApi +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 +import static datadog.trace.api.sampling.PrioritySampling.USER_DROP +import static datadog.trace.api.sampling.PrioritySampling.USER_KEEP + +class KnuthSamplingRateTest extends DDCoreSpecification { + static serializer = DDAgentApi.RESPONSE_ADAPTER + + def "formatKnuthSamplingRate produces correct format"() { + expect: + DDSpan.formatKnuthSamplingRate(rate) == 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" + } + + 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() + } +} From 636f02863747181aef3779d89eaab03f4792a3d4 Mon Sep 17 00:00:00 2001 From: bm1549 Date: Tue, 10 Mar 2026 22:32:42 -0400 Subject: [PATCH 2/8] fix: remove unused imports and update OT tests for _dd.p.ksr propagation - Remove unused imports in KnuthSamplingRateTest (CodeNarc violations) - Update OT31ApiTest and OT33ApiTest to expect _dd.p.ksr in x-datadog-tags when agent-rate sampler runs (UNSET priority) Co-Authored-By: Claude Opus 4.6 --- .../groovy/datadog/trace/core/KnuthSamplingRateTest.groovy | 3 --- .../src/ot31CompatibilityTest/groovy/OT31ApiTest.groovy | 3 +++ .../src/ot33CompatibilityTest/groovy/OT33ApiTest.groovy | 3 +++ 3 files changed, 6 insertions(+), 3 deletions(-) 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 index 7819d19f1c9..b277dc35cc9 100644 --- a/dd-trace-core/src/test/groovy/datadog/trace/core/KnuthSamplingRateTest.groovy +++ b/dd-trace-core/src/test/groovy/datadog/trace/core/KnuthSamplingRateTest.groovy @@ -2,7 +2,6 @@ package datadog.trace.core import datadog.trace.common.sampling.PrioritySampler import datadog.trace.common.sampling.RateByServiceTraceSampler -import datadog.trace.common.sampling.RuleBasedTraceSampler import datadog.trace.common.sampling.Sampler import datadog.trace.common.writer.ListWriter import datadog.trace.common.writer.ddagent.DDAgentApi @@ -13,8 +12,6 @@ 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 -import static datadog.trace.api.sampling.PrioritySampling.USER_DROP -import static datadog.trace.api.sampling.PrioritySampling.USER_KEEP class KnuthSamplingRateTest extends DDCoreSpecification { static serializer = DDAgentApi.RESPONSE_ADAPTER diff --git a/dd-trace-ot/src/ot31CompatibilityTest/groovy/OT31ApiTest.groovy b/dd-trace-ot/src/ot31CompatibilityTest/groovy/OT31ApiTest.groovy index 7e3b2ab110f..a231d4eaa5e 100644 --- a/dd-trace-ot/src/ot31CompatibilityTest/groovy/OT31ApiTest.groovy +++ b/dd-trace-ot/src/ot31CompatibilityTest/groovy/OT31ApiTest.groovy @@ -113,6 +113,9 @@ class OT31ApiTest extends DDSpecification { if (propagatedPriority > 0) { datadogTags << "_dd.p.dm=-$effectiveSamplingMechanism" } + if (contextPriority == UNSET) { + datadogTags << "_dd.p.ksr=1" + } if (traceId.toHighOrderLong() != 0) { datadogTags << "_dd.p.tid=" + LongStringUtils.toHexStringPadded(traceId.toHighOrderLong(), 16) } diff --git a/dd-trace-ot/src/ot33CompatibilityTest/groovy/OT33ApiTest.groovy b/dd-trace-ot/src/ot33CompatibilityTest/groovy/OT33ApiTest.groovy index 163a8ca2aae..69dfe7e479d 100644 --- a/dd-trace-ot/src/ot33CompatibilityTest/groovy/OT33ApiTest.groovy +++ b/dd-trace-ot/src/ot33CompatibilityTest/groovy/OT33ApiTest.groovy @@ -102,6 +102,9 @@ class OT33ApiTest extends DDSpecification { if (propagatedPriority > 0) { datadogTags << "_dd.p.dm=-$effectiveSamplingMechanism" } + if (contextPriority == UNSET) { + datadogTags << "_dd.p.ksr=1" + } if (traceId.toHighOrderLong() != 0) { datadogTags << "_dd.p.tid=" + LongStringUtils.toHexStringPadded(traceId.toHighOrderLong(), 16) } From 457c47199742a44e6436878456a6b71c033daa6b Mon Sep 17 00:00:00 2001 From: bm1549 Date: Tue, 10 Mar 2026 22:35:24 -0400 Subject: [PATCH 3/8] fix: add t.ksr to expected tracestate in OT compatibility tests The _dd.p.ksr propagated tag also appears in W3C tracestate as t.ksr. Update OT31 and OT33 test expectations for the UNSET context priority case where the agent-rate sampler runs. Co-Authored-By: Claude Opus 4.6 --- dd-trace-ot/src/ot31CompatibilityTest/groovy/OT31ApiTest.groovy | 1 + dd-trace-ot/src/ot33CompatibilityTest/groovy/OT33ApiTest.groovy | 1 + 2 files changed, 2 insertions(+) diff --git a/dd-trace-ot/src/ot31CompatibilityTest/groovy/OT31ApiTest.groovy b/dd-trace-ot/src/ot31CompatibilityTest/groovy/OT31ApiTest.groovy index a231d4eaa5e..5ef7139d63f 100644 --- a/dd-trace-ot/src/ot31CompatibilityTest/groovy/OT31ApiTest.groovy +++ b/dd-trace-ot/src/ot31CompatibilityTest/groovy/OT31ApiTest.groovy @@ -101,6 +101,7 @@ 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 : "") + + (contextPriority == UNSET ? ";t.ksr:1" : "") + ";t.tid:${traceId.toHexStringPadded(32).substring(0, 16)}" def expectedTextMap = [ "x-datadog-trace-id" : context.toTraceId(), diff --git a/dd-trace-ot/src/ot33CompatibilityTest/groovy/OT33ApiTest.groovy b/dd-trace-ot/src/ot33CompatibilityTest/groovy/OT33ApiTest.groovy index 69dfe7e479d..b382cceb7dc 100644 --- a/dd-trace-ot/src/ot33CompatibilityTest/groovy/OT33ApiTest.groovy +++ b/dd-trace-ot/src/ot33CompatibilityTest/groovy/OT33ApiTest.groovy @@ -90,6 +90,7 @@ 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 : "") + + (contextPriority == UNSET ? ";t.ksr:1" : "") + ";t.tid:${traceId.toHexStringPadded(32).substring(0, 16)}" def expectedTextMap = [ "x-datadog-trace-id" : context.toTraceId(), From b0067733f29493f8897e16845c3b7228da19b700 Mon Sep 17 00:00:00 2001 From: bm1549 Date: Tue, 10 Mar 2026 22:47:06 -0400 Subject: [PATCH 4/8] Fix forbidden API usage in formatKnuthSamplingRate Replace String#replaceAll (a forbidden API in this codebase) with manual character-based trailing-zero stripping logic that has the same semantics but avoids the regex-based method. Co-Authored-By: Claude Sonnet 4.6 --- .../src/main/java/datadog/trace/core/DDSpan.java | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) 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 98446f3d153..aaa09c25f6d 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 @@ -664,10 +664,16 @@ static String formatKnuthSamplingRate(double rate) { // Use %.6g format: up to 6 significant digits, no trailing zeros // This matches Go's strconv.FormatFloat(rate, 'g', 6, 64) and Python's f"{rate:.6g}" String formatted = String.format("%.6g", rate); - // Remove trailing zeros after decimal point + // Remove trailing zeros after decimal point (avoid forbidden String#replaceAll) if (formatted.contains(".")) { - formatted = formatted.replaceAll("0+$", ""); - formatted = formatted.replaceAll("\\.$", ""); + int end = formatted.length(); + while (end > 0 && formatted.charAt(end - 1) == '0') { + end--; + } + if (end > 0 && formatted.charAt(end - 1) == '.') { + end--; + } + formatted = formatted.substring(0, end); } return formatted; } From 1ce9e46c93b1595181574efa9926e72484ddd7d6 Mon Sep 17 00:00:00 2001 From: bm1549 Date: Tue, 10 Mar 2026 23:17:44 -0400 Subject: [PATCH 5/8] fix: correct _dd.p.ksr ordering in OT x-datadog-tags expectations The PTagsCodec headerValue method outputs tags in order: dm, tid, ksr. The test datadogTags list had ksr before tid, causing a comparison failure. Reorder to match the actual output: dm, tid, ksr. Co-Authored-By: Claude Opus 4.6 --- .../ot31CompatibilityTest/groovy/OT31ApiTest.groovy | 10 +++++----- .../ot33CompatibilityTest/groovy/OT33ApiTest.groovy | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/dd-trace-ot/src/ot31CompatibilityTest/groovy/OT31ApiTest.groovy b/dd-trace-ot/src/ot31CompatibilityTest/groovy/OT31ApiTest.groovy index 5ef7139d63f..10966d67d84 100644 --- a/dd-trace-ot/src/ot31CompatibilityTest/groovy/OT31ApiTest.groovy +++ b/dd-trace-ot/src/ot31CompatibilityTest/groovy/OT31ApiTest.groovy @@ -101,8 +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 : "") + - (contextPriority == UNSET ? ";t.ksr:1" : "") + - ";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(), @@ -114,12 +114,12 @@ class OT31ApiTest extends DDSpecification { if (propagatedPriority > 0) { datadogTags << "_dd.p.dm=-$effectiveSamplingMechanism" } - if (contextPriority == UNSET) { - datadogTags << "_dd.p.ksr=1" - } 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 b382cceb7dc..0ce61afec90 100644 --- a/dd-trace-ot/src/ot33CompatibilityTest/groovy/OT33ApiTest.groovy +++ b/dd-trace-ot/src/ot33CompatibilityTest/groovy/OT33ApiTest.groovy @@ -90,8 +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 : "") + - (contextPriority == UNSET ? ";t.ksr:1" : "") + - ";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(), @@ -103,12 +103,12 @@ class OT33ApiTest extends DDSpecification { if (propagatedPriority > 0) { datadogTags << "_dd.p.dm=-$effectiveSamplingMechanism" } - if (contextPriority == UNSET) { - datadogTags << "_dd.p.ksr=1" - } 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(',')) } From 767094759d929f0c6686b2bf758045335733f4f0 Mon Sep 17 00:00:00 2001 From: bm1549 Date: Tue, 10 Mar 2026 23:44:50 -0400 Subject: [PATCH 6/8] fix: add _dd.p.ksr to expected meta maps in DDAgentApiTest The ksr implementation now adds _dd.p.ksr tag to spans with agent sampling rate, so the msgpack serialization test expectations need to include it. Co-Authored-By: Claude Opus 4.6 --- .../groovy/datadog/trace/common/writer/DDAgentApiTest.groovy | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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, From b46e9de55bad8a24d1fd337f3cb72e0c8537d5c5 Mon Sep 17 00:00:00 2001 From: bm1549 Date: Wed, 11 Mar 2026 00:12:12 -0400 Subject: [PATCH 7/8] fix: update propagation tests to include _dd.p.ksr in expected headers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - OpenTelemetryTest: fix x-datadog-tags ordering (tid before ksr) - DatadogPropagatorTest: add ksr to expected tags when UNSET priority - OpenTracing32Test: add ksr and tid handling for UNSET priority case All follow PTagsCodec ordering: dm → tid → ksr Co-Authored-By: Claude Opus 4.6 --- .../src/test/groovy/OpenTelemetryTest.groovy | 34 +++++++++++++------ .../propagation/DatadogPropagatorTest.groovy | 3 ++ .../src/test/groovy/OpenTracing32Test.groovy | 18 +++++++--- 3 files changed, 41 insertions(+), 14 deletions(-) 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..a9da54c23e5 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 datadogTags = [] if (propagatedMechanism != UNKNOWN) { - expectedDatadogTags = "_dd.p.dm=-" + propagatedMechanism - expectedTracestate+= ";t.dm:-" + propagatedMechanism + datadogTags << "_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) { + datadogTags << "_dd.p.tid=" + traceId.toHexStringPadded(32).substring(0, 16) + } + if (contextPriority == UNSET) { + datadogTags << "_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 (!datadogTags.empty) { + expectedTextMap.put("x-datadog-tags", datadogTags.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.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 From 13b7d5e495451a71ce939ea0a88fcd935f47b75c Mon Sep 17 00:00:00 2001 From: bm1549 Date: Wed, 11 Mar 2026 00:14:12 -0400 Subject: [PATCH 8/8] fix: add ksr and tid handling to OpenTracing31Test inject extract Update the test inject extract expectations to include _dd.p.ksr=1 in x-datadog-tags and t.ksr:1 in tracestate for the UNSET sampling case, following PTagsCodec ordering: dm -> tid -> ksr. Co-Authored-By: Claude Opus 4.6 --- .../src/test/groovy/OpenTracing31Test.groovy | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) 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