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..8d360ebd2a9 --- /dev/null +++ b/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/InheritedSamplingState.java @@ -0,0 +1,121 @@ +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, with one more pending check. */ + InheritedSamplingState withCheck( + int samplingPriority, + String tracestate, + CharSequence decisionMaker, + CharSequence knuthSamplingRate, + boolean sampled) { + return new InheritedSamplingState( + samplingPriority, + tracestate, + decisionMaker, + knuthSamplingRate, + rawOtelTraceState, + otelOriginalSize, + withCheck(pendingChecks, sampled)); + } + + boolean isResolved() { + return resolved != null; + } + + CharSequence rawOtelTraceState() { + return rawOtelTraceState; + } + + 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/PTagsFactory.java b/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/PTagsFactory.java index fb197efa226..170302bacee 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 @@ -202,10 +202,11 @@ static class PTags extends PropagationTags { orgPropagationMarkerTagValue, llmObsTagValues, null, - null); + null, + 0); } - // 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, @@ -218,7 +219,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; @@ -226,7 +228,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; @@ -255,7 +262,8 @@ static PTags withError(PTagsFactory factory, String error) { null, LLMObsTagValues.EMPTY, null, - null); + null, + 0); pTags.error = error; return pTags; } @@ -265,6 +273,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); @@ -283,14 +295,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; } } @@ -432,16 +451,57 @@ 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; + } + TagValue decisionMaker = + nextDecisionMakerTagValue(samplingPriority, samplingMechanism, updateDecisionMaker); + clearCachedHeader(W3C); + samplingState = + ((InheritedSamplingState) current) + .withCheck( + samplingPriority, + tracestate, + decisionMaker, + knuthSamplingRate, + samplingPriority > 0); + return true; } private static OtelTraceState reconcileOtelTraceState( @@ -899,6 +959,11 @@ public void updateW3CTracestateFrom(PropagationTags source) { } PTags sourcePTags = (PTags) source; SamplingState sourceState = sourcePTags.samplingState(); + if (sourceState instanceof InheritedSamplingState + && !((InheritedSamplingState) sourceState).isResolved()) { + setW3CTracestate(sourceState.getTracestate(), (InheritedSamplingState) sourceState); + return; + } CharSequence sourceOtelTraceState = sourceState.getOtelTraceState(); setW3CTracestate( sourceState.getTracestate(), @@ -927,6 +992,28 @@ private void setW3CTracestate(String tracestate, OtelTraceState otelTraceState) } } + /** Takes over an extracted otel member that hasn't been parsed, without parsing it. */ + private void setW3CTracestate(String tracestate, InheritedSamplingState source) { + synchronized (samplingStateLock) { + clearCachedHeader(W3C); + int samplingPriority = samplingState.getSamplingPriority(); + byte pendingChecks = source.pendingChecks(); + if (samplingPriority != PrioritySampling.UNSET) { + pendingChecks = InheritedSamplingState.withCheck(pendingChecks, samplingPriority > 0); + } + this.tracestate = tracestate; + this.samplingState = + new InheritedSamplingState( + samplingPriority, + tracestate, + getDecisionMakerTagValue(), + getKnuthSamplingRateTagValue(), + source.rawOtelTraceState(), + source.otelOriginalSize(), + pendingChecks); + } + } + private OtelTraceState getOtelTraceState() { return (OtelTraceState) samplingState.getOtelTraceState(); } 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 94c909ff82f..85faf60a0ca 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 @@ -98,18 +98,19 @@ PropagationTags fromHeaderValue(PTagsFactory tagsFactory, String value) { } } - 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 empty(tagsFactory, value, otelTraceState); + return empty(tagsFactory, value, rawOtelTraceState, otelOriginalSize); } List tagPairs = null; @@ -195,7 +196,8 @@ PropagationTags fromHeaderValue(PTagsFactory tagsFactory, String value) { firstMemberStart, ddMemberStart, ddMemberValueEnd, - otelTraceState); + rawOtelTraceState, + otelOriginalSize); } if (tagKey.equals(DECISION_MAKER_TAG)) { decisionMakerTagValue = tagValue; @@ -264,7 +266,8 @@ PropagationTags fromHeaderValue(PTagsFactory tagsFactory, String value) { llmObsParentIdTagValue, llmObsSampleRateTagValue, llmObsSamplingDecisionTagValue), - otelTraceState); + rawOtelTraceState, + otelOriginalSize); } @Override @@ -1004,12 +1007,12 @@ private static int memberContributionSize( /** Creates tags that preserve unmanaged W3C tracestate members without sampling state. */ static W3CPTags empty(PTagsFactory factory, String original) { - return empty(factory, original, null); + return empty(factory, original, null, 0); } private static W3CPTags empty( - PTagsFactory factory, String original, OtelTraceState otelTraceState) { - return empty(factory, original, 0, -1, -1, otelTraceState); + PTagsFactory factory, String original, CharSequence rawOtelTraceState, int otelOriginalSize) { + return empty(factory, original, 0, -1, -1, rawOtelTraceState, otelOriginalSize); } private static W3CPTags empty( @@ -1018,7 +1021,8 @@ private static W3CPTags empty( int firstMemberStart, int ddMemberStart, int ddMemberValueEnd, - OtelTraceState otelTraceState) { + CharSequence rawOtelTraceState, + int otelOriginalSize) { return new W3CPTags( factory, null, @@ -1035,7 +1039,8 @@ private static W3CPTags empty( null, null, LLMObsTagValues.EMPTY, - otelTraceState); + rawOtelTraceState, + otelOriginalSize); } private static class W3CPTags extends PTags { @@ -1072,7 +1077,8 @@ public W3CPTags( CharSequence lastParentId, TagValue orgPropagationMarkerTagValue, LLMObsTagValues llmObsTagValues, - OtelTraceState otelTraceState) { + CharSequence rawOtelTraceState, + int otelOriginalSize) { super( factory, tagPairs, @@ -1085,7 +1091,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/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(); + } +}