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 22cee02db2c..87c6f7ad12a 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
@@ -67,6 +67,14 @@ public CharSequence getOtelTraceState() {
return otelTraceState;
}
+ /**
+ * The {@code ot} member as it's written, equal in content to {@link #getOtelTraceState()}. A
+ * subclass may provide it without building the parsed member.
+ */
+ public CharSequence getOtelTraceStateForWrite() {
+ return getOtelTraceState();
+ }
+
public CharSequence getDecisionMaker() {
return decisionMaker;
}
@@ -79,7 +87,7 @@ public boolean preservesInheritedOtelPosition() {
if (tracestate == null) {
return false;
}
- CharSequence otelTraceState = getOtelTraceState();
+ CharSequence otelTraceState = getOtelTraceStateForWrite();
if (otelTraceState == null) {
return false;
}
diff --git a/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/InheritedSamplingState.java b/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/InheritedSamplingState.java
new file mode 100644
index 00000000000..df0de673d59
--- /dev/null
+++ b/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/InheritedSamplingState.java
@@ -0,0 +1,133 @@
+package datadog.trace.core.propagation.ptags;
+
+import datadog.trace.core.propagation.PropagationTags.SamplingState;
+import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;
+
+/**
+ * The sampling state for an extracted trace whose OpenTelemetry {@code ot} tracestate member hasn't
+ * been parsed yet. Extraction used to parse the member and check it against the inherited decision
+ * on the request thread, though only W3C injection, OTLP export and a local change of decision ever
+ * read it. This keeps the raw member and parses it on first read, so a leaf service exporting to
+ * the Datadog agent never pays for it.
+ *
+ *
Consistency checks made before the first read are recorded as pending and applied when the
+ * member is parsed. Each check drops the threshold when {@code rv >= th} doesn't match the sampling
+ * decision checked against, and dropping it is idempotent and leaves the state consistent with any
+ * decision. So any sequence of checks has the same effect as one per decision checked, in any
+ * order, and two bits record them without changing the output.
+ */
+final class InheritedSamplingState extends SamplingState {
+ static final byte NO_CHECKS = 0;
+ private static final byte CHECKED_SAMPLED = 1;
+ private static final byte CHECKED_NOT_SAMPLED = 2;
+
+ /** Marks a member that parsed to nothing, distinct from not parsed yet. */
+ private static final Object NO_OTEL_TRACE_STATE = new Object();
+
+ private static final AtomicReferenceFieldUpdater
+ RESOLVED_UPDATER =
+ AtomicReferenceFieldUpdater.newUpdater(
+ InheritedSamplingState.class, Object.class, "resolved");
+
+ private final CharSequence rawOtelTraceState;
+ private final int otelOriginalSize;
+ private final byte pendingChecks;
+
+ /** {@code null} until parsed, then the {@link OtelTraceState} or {@link #NO_OTEL_TRACE_STATE}. */
+ private volatile Object resolved;
+
+ InheritedSamplingState(
+ int samplingPriority,
+ String tracestate,
+ CharSequence decisionMaker,
+ CharSequence knuthSamplingRate,
+ CharSequence rawOtelTraceState,
+ int otelOriginalSize,
+ byte pendingChecks) {
+ super(samplingPriority, tracestate, null, decisionMaker, knuthSamplingRate);
+ this.rawOtelTraceState = rawOtelTraceState;
+ this.otelOriginalSize = otelOriginalSize;
+ this.pendingChecks = pendingChecks;
+ }
+
+ /** Returns the pending checks with one more against {@code sampled} added. */
+ static byte withCheck(byte pendingChecks, boolean sampled) {
+ return (byte) (pendingChecks | (sampled ? CHECKED_SAMPLED : CHECKED_NOT_SAMPLED));
+ }
+
+ /** A copy of this unparsed member under a new decision and {@code pendingChecks}. */
+ InheritedSamplingState withDecision(
+ int samplingPriority,
+ String tracestate,
+ CharSequence decisionMaker,
+ CharSequence knuthSamplingRate,
+ byte pendingChecks) {
+ return new InheritedSamplingState(
+ samplingPriority,
+ tracestate,
+ decisionMaker,
+ knuthSamplingRate,
+ rawOtelTraceState,
+ otelOriginalSize,
+ pendingChecks);
+ }
+
+ boolean isResolved() {
+ return resolved != null;
+ }
+
+ /**
+ * Writes the raw member when parsing it and applying the pending checks would leave it as is,
+ * which is the usual case when relaying a trace unchanged, so injection doesn't have to parse it.
+ */
+ @Override
+ public CharSequence getOtelTraceStateForWrite() {
+ if (resolved == null
+ && OtelTraceState.isUnchangedByChecks(
+ rawOtelTraceState,
+ (pendingChecks & CHECKED_SAMPLED) != 0,
+ (pendingChecks & CHECKED_NOT_SAMPLED) != 0)) {
+ return rawOtelTraceState;
+ }
+ return getOtelTraceState();
+ }
+
+ int otelOriginalSize() {
+ return otelOriginalSize;
+ }
+
+ byte pendingChecks() {
+ return pendingChecks;
+ }
+
+ /**
+ * Parses the member and applies the pending checks on first read. Racing readers agree on one
+ * instance, so identity checks on otel states keep skipping redundant work.
+ */
+ @Override
+ public CharSequence getOtelTraceState() {
+ Object current = resolved;
+ if (current == null) {
+ current = resolve();
+ }
+ return current == NO_OTEL_TRACE_STATE ? null : (OtelTraceState) current;
+ }
+
+ private Object resolve() {
+ OtelTraceState otelTraceState = OtelTraceState.parse(rawOtelTraceState, otelOriginalSize);
+ if ((pendingChecks & CHECKED_SAMPLED) != 0) {
+ otelTraceState = checked(otelTraceState, true);
+ }
+ if ((pendingChecks & CHECKED_NOT_SAMPLED) != 0) {
+ otelTraceState = checked(otelTraceState, false);
+ }
+ Object built = otelTraceState == null ? NO_OTEL_TRACE_STATE : otelTraceState;
+ return RESOLVED_UPDATER.compareAndSet(this, null, built) ? built : resolved;
+ }
+
+ private static OtelTraceState checked(OtelTraceState otelTraceState, boolean sampled) {
+ return otelTraceState == null || otelTraceState.isConsistentWith(sampled)
+ ? otelTraceState
+ : otelTraceState.withoutThreshold();
+ }
+}
diff --git a/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/OtelTraceState.java b/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/OtelTraceState.java
index 267e5b9664b..38cee710a26 100644
--- a/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/OtelTraceState.java
+++ b/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/OtelTraceState.java
@@ -137,6 +137,58 @@ static OtelTraceState parse(CharSequence raw, int originalSize) {
true);
}
+ /**
+ * Whether {@link #parse} would keep {@code raw} as it is, and the consistency checks against
+ * {@code sampled} and not {@code sampled} that are requested would leave it unchanged too,
+ * without parsing it. Accepts only what parse keeps unchanged; when this returns false, parse
+ * decides.
+ */
+ static boolean isUnchangedByChecks(
+ CharSequence raw, boolean checkSampled, boolean checkNotSampled) {
+ if (raw == null || raw.length() == 0 || raw.length() > MAX_VALUE_LENGTH) {
+ return false;
+ }
+ int randomValueStart = -1;
+ int randomValueEnd = -1;
+ int thresholdStart = -1;
+ int thresholdEnd = -1;
+ int start = 0;
+ while (true) {
+ int end = indexOf(raw, ';', start);
+ if (end < 0) {
+ end = raw.length();
+ }
+ int valueStart = start + RANDOM_VALUE_KEY.length();
+ if (startsWith(raw, start, end, RANDOM_VALUE_KEY)) {
+ if (randomValueStart >= 0 || !isLowerHex(raw, valueStart, end, 14, 14)) {
+ return false;
+ }
+ randomValueStart = valueStart;
+ randomValueEnd = end;
+ } else if (startsWith(raw, start, end, THRESHOLD_KEY)) {
+ if (thresholdStart >= 0 || !isLowerHex(raw, valueStart, end, 1, 14)) {
+ return false;
+ }
+ thresholdStart = valueStart;
+ thresholdEnd = end;
+ } else if (!isUnknownField(raw, start, end)) {
+ return false;
+ }
+ if (end == raw.length()) {
+ break;
+ }
+ start = end + 1;
+ }
+ if (randomValueStart < 0 || thresholdStart < 0) {
+ // with nothing to compare, every check is consistent
+ return true;
+ }
+ boolean consistentWithSampled =
+ parseHex(raw, randomValueStart, randomValueEnd)
+ >= parseThreshold(raw, thresholdStart, thresholdEnd);
+ return (!checkSampled || consistentWithSampled) && (!checkNotSampled || !consistentWithSampled);
+ }
+
private static OtelTraceState parseCanonical(CharSequence value, int originalSize) {
int randomValueStart = -1;
int randomValueEnd = -1;
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 8ee73ca73e1..9add16a41e0 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
@@ -100,7 +100,8 @@ PropagationTags createValid(
orgPropagationMarkerTagValue,
llmObsTagValues,
null,
- null);
+ null,
+ 0);
}
PropagationTags createInvalid(String error) {
@@ -201,7 +202,7 @@ static class PTags extends PropagationTags {
*/
private volatile CharSequence lastParentId;
- // Takes tracestate/otelTraceState up-front to build SamplingState in one allocation.
+ // Takes tracestate and the raw otel member up-front to build SamplingState in one allocation.
PTags(
PTagsFactory factory,
List tagPairs,
@@ -214,7 +215,8 @@ static class PTags extends PropagationTags {
TagValue orgPropagationMarkerTagValue,
@Nonnull LLMObsTagValues llmObsTagValues,
String tracestate,
- OtelTraceState otelTraceState) {
+ CharSequence rawOtelTraceState,
+ int otelOriginalSize) {
assert tagPairs == null || tagPairs.size() % 2 == 0;
this.factory = factory;
this.tagPairs = tagPairs;
@@ -222,7 +224,12 @@ static class PTags extends PropagationTags {
this.traceSource = traceSource;
this.tracestate = tracestate;
this.samplingState =
- initialSamplingState(samplingPriority, tracestate, otelTraceState, decisionMakerTagValue);
+ initialSamplingState(
+ samplingPriority,
+ tracestate,
+ rawOtelTraceState,
+ otelOriginalSize,
+ decisionMakerTagValue);
this.origin = origin;
this.lastParentId = lastParentId;
this.orgPropagationMarkerTagValue = orgPropagationMarkerTagValue;
@@ -251,7 +258,8 @@ static PTags withError(PTagsFactory factory, String error) {
null,
LLMObsTagValues.EMPTY,
null,
- null);
+ null,
+ 0);
pTags.error = error;
return pTags;
}
@@ -261,6 +269,10 @@ public void updateTraceSamplingPriority(int samplingPriority, int samplingMechan
synchronized (samplingStateLock) {
if (samplingPriority != PrioritySampling.UNSET && canChangeDecisionMaker
|| samplingMechanism == SamplingMechanism.EXTERNAL_OVERRIDE) {
+ if (deferOtelCheck(
+ samplingPriority, samplingMechanism, getKnuthSamplingRateTagValue(), true)) {
+ return;
+ }
OtelTraceState nextOtelTraceState =
reconcileOtelTraceState(getOtelTraceState(), samplingPriority, samplingMechanism);
installSamplingState(samplingPriority, samplingMechanism, nextOtelTraceState);
@@ -279,14 +291,21 @@ public boolean tryUpdateTraceSamplingPriority(
if (!allowOverride && current.getSamplingPriority() != PrioritySampling.UNSET) {
return false;
}
+ boolean updateDecisionMaker =
+ canChangeDecisionMaker || samplingMechanism == SamplingMechanism.EXTERNAL_OVERRIDE;
+ TagValue knuthSamplingRate = getKnuthSamplingRateTagValue();
+ if (deferOtelCheck(
+ samplingPriority, samplingMechanism, knuthSamplingRate, updateDecisionMaker)) {
+ return true;
+ }
OtelTraceState nextOtelTraceState =
reconcileOtelTraceState(getOtelTraceState(), samplingPriority, samplingMechanism);
installSamplingState(
samplingPriority,
samplingMechanism,
nextOtelTraceState,
- getKnuthSamplingRateTagValue(),
- canChangeDecisionMaker || samplingMechanism == SamplingMechanism.EXTERNAL_OVERRIDE);
+ knuthSamplingRate,
+ updateDecisionMaker);
return true;
}
}
@@ -428,16 +447,62 @@ private TagValue nextDecisionMakerTagValue(
private static SamplingState initialSamplingState(
int samplingPriority,
String tracestate,
- OtelTraceState otelTraceState,
+ CharSequence rawOtelTraceState,
+ int otelOriginalSize,
TagValue decisionMakerTagValue) {
+ if (rawOtelTraceState != null) {
+ return new InheritedSamplingState(
+ samplingPriority,
+ tracestate,
+ decisionMakerTagValue,
+ null,
+ rawOtelTraceState,
+ otelOriginalSize,
+ InheritedSamplingState.NO_CHECKS);
+ }
if (samplingPriority == PrioritySampling.UNSET
&& tracestate == null
- && otelTraceState == null
&& decisionMakerTagValue == null) {
return EMPTY_SAMPLING_STATE;
}
- return new SamplingState(
- samplingPriority, tracestate, otelTraceState, decisionMakerTagValue, null);
+ return new SamplingState(samplingPriority, tracestate, null, decisionMakerTagValue, null);
+ }
+
+ /**
+ * Records a decision against an extracted otel member that hasn't been parsed, without parsing
+ * it. For an external override or unknown mechanism, reconciling the member is only a
+ * consistency check, which can wait for the first read; a local decision rewrites the member
+ * and parses it. Called with the sampling state lock held.
+ */
+ private boolean deferOtelCheck(
+ int samplingPriority,
+ int samplingMechanism,
+ TagValue knuthSamplingRate,
+ boolean updateDecisionMaker) {
+ SamplingState current = samplingState;
+ if (!(current instanceof InheritedSamplingState)
+ || ((InheritedSamplingState) current).isResolved()
+ || (samplingMechanism != SamplingMechanism.EXTERNAL_OVERRIDE
+ && samplingMechanism != SamplingMechanism.UNKNOWN)) {
+ return false;
+ }
+ InheritedSamplingState inherited = (InheritedSamplingState) current;
+ TagValue decisionMaker =
+ nextDecisionMakerTagValue(samplingPriority, samplingMechanism, updateDecisionMaker);
+ byte pendingChecks =
+ InheritedSamplingState.withCheck(inherited.pendingChecks(), samplingPriority > 0);
+ if (inherited.getSamplingPriority() == samplingPriority
+ && pendingChecks == inherited.pendingChecks()
+ && Objects.equals(inherited.getDecisionMaker(), decisionMaker)
+ && Objects.equals(inherited.getKnuthSamplingRate(), knuthSamplingRate)) {
+ // nothing changes, e.g. the root span re-applying the extracted priority
+ return true;
+ }
+ clearCachedHeader(W3C);
+ samplingState =
+ inherited.withDecision(
+ samplingPriority, tracestate, decisionMaker, knuthSamplingRate, pendingChecks);
+ return true;
}
private static OtelTraceState reconcileOtelTraceState(
@@ -889,18 +954,33 @@ public void updateW3CTracestate(String tracestate) {
@Override
public void updateW3CTracestateFrom(W3CTracestate source, int overridingPriority) {
- OtelTraceState otelTraceState = source.otelTraceState;
+ if (source.rawOtelTraceState == null) {
+ setW3CTracestate(source.tracestate, W3CPTagsCodec.extractOtelTraceState(source.tracestate));
+ return;
+ }
+ // Keep the otel member unparsed; its consistency checks wait for the first read
+ byte pendingChecks = InheritedSamplingState.NO_CHECKS;
if (overridingPriority != PrioritySampling.UNSET) {
// as the trace context's own priority override reconciles its member
- otelTraceState =
- reconcileOtelTraceState(
- otelTraceState, overridingPriority, SamplingMechanism.EXTERNAL_OVERRIDE);
- }
- setW3CTracestate(
- source.tracestate,
- otelTraceState != null
- ? otelTraceState
- : W3CPTagsCodec.extractOtelTraceState(source.tracestate));
+ pendingChecks = InheritedSamplingState.withCheck(pendingChecks, overridingPriority > 0);
+ }
+ synchronized (samplingStateLock) {
+ clearCachedHeader(W3C);
+ int samplingPriority = samplingState.getSamplingPriority();
+ if (samplingPriority != PrioritySampling.UNSET) {
+ pendingChecks = InheritedSamplingState.withCheck(pendingChecks, samplingPriority > 0);
+ }
+ this.tracestate = source.tracestate;
+ this.samplingState =
+ new InheritedSamplingState(
+ samplingPriority,
+ source.tracestate,
+ getDecisionMakerTagValue(),
+ getKnuthSamplingRateTagValue(),
+ source.rawOtelTraceState,
+ source.otelOriginalSize,
+ pendingChecks);
+ }
}
private void setW3CTracestate(String tracestate, OtelTraceState otelTraceState) {
diff --git a/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/W3CPTagsCodec.java b/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/W3CPTagsCodec.java
index 43b0d41a4b0..49db92a3ab9 100644
--- a/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/W3CPTagsCodec.java
+++ b/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/W3CPTagsCodec.java
@@ -116,18 +116,19 @@ private static R parse(String value, Result result) {
}
}
- OtelTraceState otelTraceState = null;
+ // The otel member is kept raw and parsed on first read; see InheritedSamplingState
+ CharSequence rawOtelTraceState = null;
+ int otelOriginalSize = 0;
if (otelMemberStart != -1) {
int valueEnd = stripTrailingOWC(value, otelMemberValueStart, otelMemberValueEnd);
- otelTraceState =
- OtelTraceState.parse(
- SubSequence.of(value, otelMemberValueStart, valueEnd),
- memberContributionSize(value, firstMemberStart, otelMemberStart, otelMemberValueEnd));
+ rawOtelTraceState = SubSequence.of(value, otelMemberValueStart, valueEnd);
+ otelOriginalSize =
+ memberContributionSize(value, firstMemberStart, otelMemberStart, otelMemberValueEnd);
}
if (ddMemberIndex == -1) {
// There was no dd member, so create an empty one with the _suffix_
- return result.withoutDatadogTags(value, 0, -1, -1, otelTraceState);
+ return result.withoutDatadogTags(value, 0, -1, -1, rawOtelTraceState, otelOriginalSize);
}
List tagPairs = null;
@@ -208,7 +209,12 @@ private static R parse(String value, Result result) {
return result.invalid(PROPAGATION_ERROR_MALFORMED_TID + tagValue);
}
return result.withoutDatadogTags(
- value, firstMemberStart, ddMemberStart, ddMemberValueEnd, otelTraceState);
+ value,
+ firstMemberStart,
+ ddMemberStart,
+ ddMemberValueEnd,
+ rawOtelTraceState,
+ otelOriginalSize);
}
if (tagKey.equals(DECISION_MAKER_TAG)) {
decisionMakerTagValue = tagValue;
@@ -276,7 +282,8 @@ private static R parse(String value, Result result) {
llmObsParentIdTagValue,
llmObsSampleRateTagValue,
llmObsSamplingDecisionTagValue),
- otelTraceState);
+ rawOtelTraceState,
+ otelOriginalSize);
}
@Override
@@ -316,10 +323,13 @@ protected int estimateHeaderSize(
size += originalTracestate.length();
includesOriginalTracestate = true;
}
- CharSequence otelTraceState = samplingState.getOtelTraceState();
+ CharSequence otelTraceState = samplingState.getOtelTraceStateForWrite();
if (otelTraceState != null) {
if (includesOriginalTracestate && otelTraceState instanceof OtelTraceState) {
size -= ((OtelTraceState) otelTraceState).getOriginalSize();
+ } else if (includesOriginalTracestate && samplingState instanceof InheritedSamplingState) {
+ // the unparsed member, written as it arrived
+ size -= ((InheritedSamplingState) samplingState).otelOriginalSize();
}
size += OTEL_MEMBER_KEY.length() + otelTraceState.length() + 1;
}
@@ -389,7 +399,7 @@ protected int appendSuffix(StringBuilder sb, PTags ptags, int size, SamplingStat
if (size == 0) {
String original = samplingState.getTracestate();
String trimmed = original == null ? null : original.trim();
- if (canForwardRawTracestate(trimmed, samplingState.getOtelTraceState())) {
+ if (canForwardRawTracestate(trimmed, samplingState.getOtelTraceStateForWrite())) {
sb.append(trimmed);
return EMPTY_SIZE + 1;
}
@@ -852,7 +862,7 @@ private static int cleanUpAndAppendUnknown(StringBuilder sb, W3CPTags w3CPTags,
private static boolean appendOtelAndVendorMembers(
StringBuilder sb, SamplingState samplingState, boolean hasDatadogMember) {
String original = samplingState.getTracestate();
- CharSequence otelTraceState = samplingState.getOtelTraceState();
+ CharSequence otelTraceState = samplingState.getOtelTraceStateForWrite();
int remainingMembers = MAX_MEMBER_COUNT - (hasDatadogMember ? 1 : 0);
boolean memberAppended = false;
boolean preserveOtelPosition = samplingState.preservesInheritedOtelPosition();
@@ -910,7 +920,7 @@ public static boolean isUnchangedInheritedOtelMember(
public static String rebuildTracestate(SamplingState samplingState) {
String original = samplingState.getTracestate();
- CharSequence otelTraceState = samplingState.getOtelTraceState();
+ CharSequence otelTraceState = samplingState.getOtelTraceStateForWrite();
// TODO Consider a raw passthrough for unchanged state after checking dd= is not duplicated.
StringBuilder result = new StringBuilder(MAX_HEADER_SIZE);
int memberCount = 0;
@@ -1028,7 +1038,8 @@ R withoutDatadogTags(
int firstMemberStart,
int ddMemberStart,
int ddMemberValueEnd,
- OtelTraceState otelTraceState);
+ CharSequence rawOtelTraceState,
+ int otelOriginalSize);
R parsed(
List tagPairs,
@@ -1045,7 +1056,8 @@ R parsed(
CharSequence lastParentId,
TagValue orgPropagationMarkerTagValue,
LLMObsTagValues llmObsTagValues,
- OtelTraceState otelTraceState);
+ CharSequence rawOtelTraceState,
+ int otelOriginalSize);
}
private static final class PTagsResult implements Result {
@@ -1071,9 +1083,16 @@ public PropagationTags withoutDatadogTags(
int firstMemberStart,
int ddMemberStart,
int ddMemberValueEnd,
- OtelTraceState otelTraceState) {
+ CharSequence rawOtelTraceState,
+ int otelOriginalSize) {
return empty(
- tagsFactory, value, firstMemberStart, ddMemberStart, ddMemberValueEnd, otelTraceState);
+ tagsFactory,
+ value,
+ firstMemberStart,
+ ddMemberStart,
+ ddMemberValueEnd,
+ rawOtelTraceState,
+ otelOriginalSize);
}
@Override
@@ -1092,7 +1111,8 @@ public PropagationTags parsed(
CharSequence lastParentId,
TagValue orgPropagationMarkerTagValue,
LLMObsTagValues llmObsTagValues,
- OtelTraceState otelTraceState) {
+ CharSequence rawOtelTraceState,
+ int otelOriginalSize) {
return new W3CPTags(
tagsFactory,
tagPairs,
@@ -1109,7 +1129,8 @@ public PropagationTags parsed(
lastParentId,
orgPropagationMarkerTagValue,
llmObsTagValues,
- otelTraceState);
+ rawOtelTraceState,
+ otelOriginalSize);
}
}
@@ -1131,8 +1152,10 @@ public W3CTracestate withoutDatadogTags(
int firstMemberStart,
int ddMemberStart,
int ddMemberValueEnd,
- OtelTraceState otelTraceState) {
- return new W3CTracestate(value, otelTraceState, PrioritySampling.UNSET, null);
+ CharSequence rawOtelTraceState,
+ int otelOriginalSize) {
+ return new W3CTracestate(
+ value, rawOtelTraceState, otelOriginalSize, PrioritySampling.UNSET, null);
}
@Override
@@ -1151,14 +1174,16 @@ public W3CTracestate parsed(
CharSequence lastParentId,
TagValue orgPropagationMarkerTagValue,
LLMObsTagValues llmObsTagValues,
- OtelTraceState otelTraceState) {
- return new W3CTracestate(value, otelTraceState, samplingPriority, lastParentId);
+ CharSequence rawOtelTraceState,
+ int otelOriginalSize) {
+ return new W3CTracestate(
+ value, rawOtelTraceState, otelOriginalSize, samplingPriority, lastParentId);
}
};
/** Creates tags that preserve unmanaged W3C tracestate members without sampling state. */
static W3CPTags empty(PTagsFactory factory, String original) {
- return empty(factory, original, 0, -1, -1, null);
+ return empty(factory, original, 0, -1, -1, null, 0);
}
private static W3CPTags empty(
@@ -1167,7 +1192,8 @@ private static W3CPTags empty(
int firstMemberStart,
int ddMemberStart,
int ddMemberValueEnd,
- OtelTraceState otelTraceState) {
+ CharSequence rawOtelTraceState,
+ int otelOriginalSize) {
return new W3CPTags(
factory,
null,
@@ -1184,7 +1210,8 @@ private static W3CPTags empty(
null,
null,
LLMObsTagValues.EMPTY,
- otelTraceState);
+ rawOtelTraceState,
+ otelOriginalSize);
}
private static class W3CPTags extends PTags {
@@ -1221,7 +1248,8 @@ public W3CPTags(
CharSequence lastParentId,
TagValue orgPropagationMarkerTagValue,
LLMObsTagValues llmObsTagValues,
- OtelTraceState otelTraceState) {
+ CharSequence rawOtelTraceState,
+ int otelOriginalSize) {
super(
factory,
tagPairs,
@@ -1234,7 +1262,8 @@ public W3CPTags(
orgPropagationMarkerTagValue,
llmObsTagValues,
original,
- otelTraceState);
+ rawOtelTraceState,
+ otelOriginalSize);
this.firstMemberStart = firstMemberStart;
this.ddMemberStart = ddMemberStart;
this.ddMemberValueEnd = ddMemberValueEnd;
diff --git a/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/W3CTracestate.java b/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/W3CTracestate.java
index addee3f0998..905a86f4025 100644
--- a/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/W3CTracestate.java
+++ b/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/W3CTracestate.java
@@ -7,24 +7,29 @@
* without building propagation tags for it. Obtained from {@link #parse(String)}.
*/
public final class W3CTracestate {
- static final W3CTracestate NONE = new W3CTracestate(null, null, PrioritySampling.UNSET, null);
+ static final W3CTracestate NONE = new W3CTracestate(null, null, 0, PrioritySampling.UNSET, null);
/** The header value, or null if it's rejected. */
final String tracestate;
- /** The {@code ot} member, or null if there is none. */
- final OtelTraceState otelTraceState;
+ /** The {@code ot} member's value, unparsed, or null if there is none. */
+ final CharSequence rawOtelTraceState;
+
+ /** What the {@code ot} member contributes to the size of {@link #tracestate}. */
+ final int otelOriginalSize;
private final int samplingPriority;
private final CharSequence lastParentId;
W3CTracestate(
String tracestate,
- OtelTraceState otelTraceState,
+ CharSequence rawOtelTraceState,
+ int otelOriginalSize,
int samplingPriority,
CharSequence lastParentId) {
this.tracestate = tracestate;
- this.otelTraceState = otelTraceState;
+ this.rawOtelTraceState = rawOtelTraceState;
+ this.otelOriginalSize = otelOriginalSize;
this.samplingPriority = samplingPriority;
this.lastParentId = lastParentId;
}
diff --git a/dd-trace-core/src/test/java/datadog/trace/core/propagation/ptags/DeferredOtelCheckTest.java b/dd-trace-core/src/test/java/datadog/trace/core/propagation/ptags/DeferredOtelCheckTest.java
new file mode 100644
index 00000000000..e23ba38ceec
--- /dev/null
+++ b/dd-trace-core/src/test/java/datadog/trace/core/propagation/ptags/DeferredOtelCheckTest.java
@@ -0,0 +1,39 @@
+package datadog.trace.core.propagation.ptags;
+
+import static datadog.trace.api.sampling.PrioritySampling.SAMPLER_DROP;
+import static datadog.trace.api.sampling.SamplingMechanism.EXTERNAL_OVERRIDE;
+import static datadog.trace.core.propagation.PropagationTags.HeaderType.W3C;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertSame;
+
+import datadog.trace.core.propagation.PropagationTags;
+import org.junit.jupiter.api.Test;
+
+/** A decision against an unparsed {@code ot} member records its consistency check for later. */
+class DeferredOtelCheckTest {
+ // rv >= th, so the member is only consistent with a sampled decision
+ private static final String TRACESTATE = "dd=s:0;p:123456789abcdef0,ot=th:8;rv:c8b1e2d3f4a596";
+
+ @Test
+ void samePriorityStillRecordsANewCheck() {
+ PropagationTags tags = PropagationTags.factory().fromHeaderValue(W3C, TRACESTATE);
+ assertEquals(SAMPLER_DROP, tags.getSamplingPriority());
+
+ tags.updateTraceSamplingPriority(SAMPLER_DROP, EXTERNAL_OVERRIDE);
+
+ CharSequence otelTraceState = tags.samplingState().getOtelTraceState();
+ assertFalse(otelTraceState.toString().contains("th:"), otelTraceState.toString());
+ }
+
+ @Test
+ void repeatedDecisionKeepsTheState() {
+ PropagationTags tags = PropagationTags.factory().fromHeaderValue(W3C, TRACESTATE);
+ tags.updateTraceSamplingPriority(SAMPLER_DROP, EXTERNAL_OVERRIDE);
+ PropagationTags.SamplingState afterFirst = tags.samplingState();
+
+ tags.updateTraceSamplingPriority(SAMPLER_DROP, EXTERNAL_OVERRIDE);
+
+ assertSame(afterFirst, tags.samplingState());
+ }
+}
diff --git a/dd-trace-core/src/test/java/datadog/trace/core/propagation/ptags/InheritedOtelExtractionTest.java b/dd-trace-core/src/test/java/datadog/trace/core/propagation/ptags/InheritedOtelExtractionTest.java
new file mode 100644
index 00000000000..9de4ac7a74f
--- /dev/null
+++ b/dd-trace-core/src/test/java/datadog/trace/core/propagation/ptags/InheritedOtelExtractionTest.java
@@ -0,0 +1,100 @@
+package datadog.trace.core.propagation.ptags;
+
+import static datadog.trace.api.ConfigDefaults.DEFAULT_TRACE_X_DATADOG_TAGS_MAX_LENGTH;
+import static datadog.trace.api.TracePropagationStyle.DATADOG;
+import static datadog.trace.api.TracePropagationStyle.TRACECONTEXT;
+import static datadog.trace.bootstrap.instrumentation.api.ContextVisitors.stringValuesMap;
+import static datadog.trace.core.propagation.PropagationTags.HeaderType.W3C;
+import static java.util.Arrays.asList;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import datadog.trace.api.Config;
+import datadog.trace.bootstrap.instrumentation.api.AgentSpan;
+import datadog.trace.core.CoreTracer;
+import datadog.trace.core.DDCoreJavaSpecification;
+import datadog.trace.core.propagation.ExtractedContext;
+import datadog.trace.core.propagation.HttpCodec;
+import datadog.trace.core.propagation.PropagationTags;
+import java.util.HashMap;
+import java.util.LinkedHashSet;
+import java.util.Map;
+import org.junit.jupiter.api.Test;
+
+/** Real extraction leaves the {@code ot} member unparsed until something reads it. */
+class InheritedOtelExtractionTest extends DDCoreJavaSpecification {
+ private static final String TRACE_PARENT =
+ "00-00000000000000000000000000000001-123456789abcdef0-01";
+ private static final String OTEL_MEMBER = "ot=rv:ef284ace7a91e1;th:e6666666666668";
+
+ @Test
+ void leafFromDatadogUpstreamNeverParses() {
+ Map headers = new HashMap<>();
+ headers.put("x-datadog-trace-id", "1");
+ headers.put("x-datadog-parent-id", "2");
+ headers.put("x-datadog-sampling-priority", "1");
+ headers.put("traceparent", TRACE_PARENT);
+ headers.put("tracestate", "dd=s:1;p:123456789abcdef0," + OTEL_MEMBER);
+
+ PropagationTags tags = extractAndFinishLeaf(headers);
+
+ assertUnparsed(tags);
+ assertTrue(tags.headerValue(W3C).contains(OTEL_MEMBER));
+ }
+
+ @Test
+ void leafFromOtelUpstreamNeverParses() {
+ Map headers = new HashMap<>();
+ headers.put("traceparent", TRACE_PARENT);
+ headers.put("tracestate", OTEL_MEMBER);
+
+ PropagationTags tags = extractAndFinishLeaf(headers);
+
+ assertUnparsed(tags);
+ assertTrue(tags.headerValue(W3C).contains(OTEL_MEMBER));
+ }
+
+ @Test
+ void inconsistentInheritedThresholdIsStillDropped() {
+ // rv < th means drop, but the upstream kept the trace: the threshold can't be forwarded
+ Map headers = new HashMap<>();
+ headers.put("traceparent", TRACE_PARENT);
+ headers.put("tracestate", "ot=rv:0000000000000a;th:8");
+
+ PropagationTags tags = extractAndFinishLeaf(headers);
+
+ assertUnparsed(tags);
+ String outbound = tags.headerValue(W3C);
+ assertTrue(outbound.contains("ot=rv:0000000000000a"));
+ assertFalse(outbound.contains("th:8"));
+ }
+
+ private PropagationTags extractAndFinishLeaf(Map headers) {
+ Config config = mock(Config.class);
+ when(config.getTracePropagationStylesToExtract())
+ .thenReturn(new LinkedHashSet<>(asList(DATADOG, TRACECONTEXT)));
+ when(config.getxDatadogTagsMaxLength()).thenReturn(DEFAULT_TRACE_X_DATADOG_TAGS_MAX_LENGTH);
+ when(config.isApmTracingEnabled()).thenReturn(true);
+
+ CoreTracer tracer = tracerBuilder().build();
+ try {
+ HttpCodec.Extractor extractor = HttpCodec.createExtractor(config, tracer::captureTraceConfig);
+ ExtractedContext extracted =
+ assertInstanceOf(ExtractedContext.class, extractor.extract(headers, stringValuesMap()));
+ AgentSpan span = tracer.buildSpan("test", "leaf").asChildOf(extracted).start();
+ span.finish();
+ return extracted.getPropagationTags();
+ } finally {
+ tracer.close();
+ }
+ }
+
+ private static void assertUnparsed(PropagationTags tags) {
+ InheritedSamplingState state =
+ assertInstanceOf(InheritedSamplingState.class, tags.samplingState());
+ assertFalse(state.isResolved(), "a leaf must not parse the inherited otel member");
+ }
+}
diff --git a/dd-trace-core/src/test/java/datadog/trace/core/propagation/ptags/InheritedSamplingStateTest.java b/dd-trace-core/src/test/java/datadog/trace/core/propagation/ptags/InheritedSamplingStateTest.java
new file mode 100644
index 00000000000..9ece70a7a76
--- /dev/null
+++ b/dd-trace-core/src/test/java/datadog/trace/core/propagation/ptags/InheritedSamplingStateTest.java
@@ -0,0 +1,97 @@
+package datadog.trace.core.propagation.ptags;
+
+import static datadog.trace.api.sampling.PrioritySampling.SAMPLER_KEEP;
+import static datadog.trace.core.propagation.ptags.InheritedSamplingState.NO_CHECKS;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.junit.jupiter.api.Test;
+import org.tabletest.junit.TableTest;
+
+class InheritedSamplingStateTest {
+ private static final int ORIGINAL_SIZE = 40;
+
+ /**
+ * Parsing on first read with pending checks gives what parsing on extract and checking at each
+ * step gave, for every combination of checks and in either order.
+ */
+ @TableTest({
+ "scenario | raw ",
+ "keep consistent | 'rv:ef284ace7a91e1;th:e6666666666668' ",
+ "drop consistent | 'rv:0000000000000a;th:8' ",
+ "random value only | 'rv:ef284ace7a91e1' ",
+ "threshold only | 'th:8' ",
+ "unknown field kept | 'rv:0000000000000a;th:8;future:value' ",
+ "threshold and unknown field | 'th:8;future:value' ",
+ "unknown fields only | 'simplefield;blah;foo' ",
+ "invalid random value | 'rv:zz;th:8;future:value' ",
+ "duplicate random value | 'rv:ef284ace7a91e1;rv:0000000000000a;th:8'",
+ "nothing valid | 'rv:invalid;th:invalid' "
+ })
+ void resolvesAsEagerParseThenChecks(String raw) {
+ for (boolean checkSampled : new boolean[] {false, true}) {
+ for (boolean checkNotSampled : new boolean[] {false, true}) {
+ byte checks = NO_CHECKS;
+ if (checkSampled) {
+ checks = InheritedSamplingState.withCheck(checks, true);
+ }
+ if (checkNotSampled) {
+ checks = InheritedSamplingState.withCheck(checks, false);
+ }
+ InheritedSamplingState state = state(raw, checks);
+
+ String sampledFirst = eager(raw, checkSampled, checkNotSampled, true);
+ String notSampledFirst = eager(raw, checkSampled, checkNotSampled, false);
+ assertEquals(sampledFirst, notSampledFirst, "check order mustn't matter");
+ assertEquals(sampledFirst, String.valueOf(state.getOtelTraceState()));
+ }
+ }
+ }
+
+ @Test
+ void parsesOnlyOnFirstRead() {
+ InheritedSamplingState state = state("rv:ef284ace7a91e1;th:e6666666666668", NO_CHECKS);
+
+ assertFalse(state.isResolved());
+ state.getOtelTraceState();
+ assertTrue(state.isResolved());
+ }
+
+ @Test
+ void repeatedReadsReturnOneInstance() {
+ InheritedSamplingState state = state("rv:ef284ace7a91e1;th:e6666666666668", NO_CHECKS);
+
+ assertSame(state.getOtelTraceState(), state.getOtelTraceState());
+ }
+
+ @Test
+ void keepsOriginalSizeForTheHeaderEstimate() {
+ InheritedSamplingState state = state("rv:ef284ace7a91e1;th:e6666666666668", NO_CHECKS);
+
+ assertEquals(ORIGINAL_SIZE, ((OtelTraceState) state.getOtelTraceState()).getOriginalSize());
+ }
+
+ private static InheritedSamplingState state(String raw, byte checks) {
+ return new InheritedSamplingState(SAMPLER_KEEP, null, null, null, raw, ORIGINAL_SIZE, checks);
+ }
+
+ /** What extraction did before deferring: parse, then check at each step. */
+ private static String eager(
+ String raw, boolean checkSampled, boolean checkNotSampled, boolean sampledFirst) {
+ OtelTraceState state = OtelTraceState.parse(raw, ORIGINAL_SIZE);
+ if (sampledFirst) {
+ state = checkSampled ? check(state, true) : state;
+ state = checkNotSampled ? check(state, false) : state;
+ } else {
+ state = checkNotSampled ? check(state, false) : state;
+ state = checkSampled ? check(state, true) : state;
+ }
+ return String.valueOf(state);
+ }
+
+ private static OtelTraceState check(OtelTraceState state, boolean sampled) {
+ return state == null || state.isConsistentWith(sampled) ? state : state.withoutThreshold();
+ }
+}
diff --git a/dd-trace-core/src/test/java/datadog/trace/core/propagation/ptags/OtelMemberForWriteTest.java b/dd-trace-core/src/test/java/datadog/trace/core/propagation/ptags/OtelMemberForWriteTest.java
new file mode 100644
index 00000000000..d44a8eb26be
--- /dev/null
+++ b/dd-trace-core/src/test/java/datadog/trace/core/propagation/ptags/OtelMemberForWriteTest.java
@@ -0,0 +1,101 @@
+package datadog.trace.core.propagation.ptags;
+
+import static datadog.trace.api.sampling.PrioritySampling.SAMPLER_KEEP;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import datadog.trace.util.SubSequence;
+import java.util.Objects;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Writing an unparsed {@code ot} member gives the same content as parsing it and applying its
+ * pending checks, and skips the parse when the member would come out unchanged.
+ */
+class OtelMemberForWriteTest {
+ private static final String[] MEMBERS = {
+ "rv:c8b1e2d3f4a596;th:8",
+ "th:8;rv:c8b1e2d3f4a596",
+ "rv:0000000000000a;th:8",
+ "rv:c8b1e2d3f4a596;th:0",
+ "rv:c8b1e2d3f4a596",
+ "th:8",
+ "rv:c8b1e2d3f4a596;th:8;x:y",
+ "x:y",
+ // normalized or rejected by the parser
+ "rv:C8B1E2D3F4A596;th:8",
+ "rv:c8b1;th:8",
+ "rv:c8b1e2d3f4a596;rv:c8b1e2d3f4a596",
+ "th:8;th:4",
+ "th:xyz",
+ "rv:c8b1e2d3f4a596;;th:8",
+ "garbage",
+ "rv:zz"
+ };
+
+ @Test
+ void writtenContentMatchesTheParsedMember() {
+ for (String member : MEMBERS) {
+ for (int checks = 0; checks < 4; ++checks) {
+ CharSequence parsed = state(member, checks).getOtelTraceState();
+ CharSequence written = state(member, checks).getOtelTraceStateForWrite();
+ assertEquals(
+ parsed == null ? null : parsed.toString(),
+ written == null ? null : written.toString(),
+ member + " checks=" + checks);
+ }
+ }
+ }
+
+ @Test
+ void unchangedMemberIsWrittenRawWithoutParsing() {
+ InheritedSamplingState consistent = state("rv:c8b1e2d3f4a596;th:8", sampledCheck());
+ CharSequence written = consistent.getOtelTraceStateForWrite();
+ assertTrue(written instanceof SubSequence, String.valueOf(written));
+ assertFalse(consistent.isResolved());
+
+ InheritedSamplingState inconsistent = state("rv:c8b1e2d3f4a596;th:8", notSampledCheck());
+ assertTrue(inconsistent.getOtelTraceStateForWrite() instanceof OtelTraceState);
+ }
+
+ @Test
+ void parsedMemberIsWrittenAsParsed() {
+ InheritedSamplingState state = state("rv:c8b1e2d3f4a596;th:8", sampledCheck());
+ CharSequence parsed = state.getOtelTraceState();
+ assertSame(parsed, state.getOtelTraceStateForWrite());
+ }
+
+ private static byte sampledCheck() {
+ return InheritedSamplingState.withCheck(InheritedSamplingState.NO_CHECKS, true);
+ }
+
+ private static byte notSampledCheck() {
+ return InheritedSamplingState.withCheck(InheritedSamplingState.NO_CHECKS, false);
+ }
+
+ private static InheritedSamplingState state(String member, int checks) {
+ byte pending = InheritedSamplingState.NO_CHECKS;
+ if ((checks & 1) != 0) {
+ pending = InheritedSamplingState.withCheck(pending, true);
+ }
+ if ((checks & 2) != 0) {
+ pending = InheritedSamplingState.withCheck(pending, false);
+ }
+ return state(member, pending);
+ }
+
+ private static InheritedSamplingState state(String member, byte pending) {
+ String tracestate = "dd=s:1,ot=" + member;
+ int start = tracestate.indexOf("ot=") + 3;
+ return new InheritedSamplingState(
+ SAMPLER_KEEP,
+ tracestate,
+ null,
+ null,
+ SubSequence.of(tracestate, start, tracestate.length()),
+ Objects.requireNonNull(member).length() + 4,
+ pending);
+ }
+}