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