diff --git a/dd-trace-core/src/main/java/datadog/trace/core/propagation/HttpCodec.java b/dd-trace-core/src/main/java/datadog/trace/core/propagation/HttpCodec.java index 38783860e9b..61e9a8f105e 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/propagation/HttpCodec.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/propagation/HttpCodec.java @@ -286,8 +286,7 @@ private void applyTraceContextToFirstContext( ExtractedContext traceContext, ExtractionCache extractionCache) { // Propagate newly extracted W3C tracestate to first valid context - String extractedTracestate = traceContext.getPropagationTags().getW3CTracestate(); - firstContext.getPropagationTags().updateW3CTracestate(extractedTracestate); + firstContext.getPropagationTags().updateW3CTracestateFrom(traceContext.getPropagationTags()); // Check if parent spans differ to reconcile them if (firstContext.getSpanId() != traceContext.getSpanId()) { // Override parent span id with W3C one 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 3a0c57a4dd8..0ebe630c87a 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 @@ -95,6 +95,11 @@ public interface Factory { */ public abstract void updateW3CTracestate(String tracestate); + /** Updates the original W3C tracestate header from {@code source}. */ + public void updateW3CTracestateFrom(PropagationTags source) { + updateW3CTracestate(source.getW3CTracestate()); + } + /** * Constructs a header value that includes valid propagated _dd.p.* tags and possibly a new * sampling decision tag _dd.p.dm based on the current state. Returns null if the value length 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 new file mode 100644 index 00000000000..dc6456e88ec --- /dev/null +++ b/dd-trace-core/src/main/java/datadog/trace/core/propagation/ptags/OtelTraceState.java @@ -0,0 +1,36 @@ +package datadog.trace.core.propagation.ptags; + +final class OtelTraceState { + private final CharSequence value; + private final int originalPosition; + private final int originalSize; + + private OtelTraceState(CharSequence value, int originalPosition, int originalSize) { + this.value = value; + this.originalPosition = originalPosition; + this.originalSize = originalSize; + } + + static OtelTraceState parse(CharSequence raw, int originalPosition, int originalSize) { + if (raw == null || raw.length() == 0) { + return null; + } + return new OtelTraceState(raw, originalPosition, originalSize); + } + + CharSequence getValue() { + return value; + } + + int length() { + return value.length(); + } + + int getOriginalPosition() { + return originalPosition; + } + + int getOriginalSize() { + return originalSize; + } +} 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 0b5184d448a..84f3c700269 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 @@ -112,6 +112,8 @@ static class PTags extends PropagationTags { private volatile TagValue orgPropagationMarkerTagValue; + private volatile OtelTraceState otelTraceState; + // Static cache for the most-recently-seen rate → TagValue. In steady state a service uses one // rate, so this eliminates the char[] + String allocation on every new PTags instance. // Writes are benign-racy: two threads computing the same rate produce equal TagValues. @@ -540,7 +542,34 @@ public String getW3CTracestate() { @Override public void updateW3CTracestate(String tracestate) { + setW3CTracestate(tracestate, W3CPTagsCodec.extractOtelTraceState(tracestate)); + } + + @Override + public void updateW3CTracestateFrom(PropagationTags source) { + if (!(source instanceof PTags)) { + super.updateW3CTracestateFrom(source); + return; + } + PTags sourcePTags = (PTags) source; + setW3CTracestate(sourcePTags.tracestate, sourcePTags.getOtelTraceState()); + } + + private void setW3CTracestate(String tracestate, OtelTraceState otelTraceState) { + clearCachedHeader(W3C); this.tracestate = tracestate; + this.otelTraceState = otelTraceState; + } + + OtelTraceState getOtelTraceState() { + return otelTraceState; + } + + void setOtelTraceState(OtelTraceState otelTraceState) { + if (this.otelTraceState != otelTraceState) { + this.otelTraceState = otelTraceState; + clearCachedHeader(W3C); + } } String getError() { 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 c0018544188..9cccf3fd45c 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 @@ -9,6 +9,7 @@ import datadog.trace.core.propagation.PropagationTags; import datadog.trace.core.propagation.ptags.PTagsFactory.PTags; import datadog.trace.core.propagation.ptags.TagElement.Encoding; +import datadog.trace.util.SubSequence; import java.util.ArrayList; import java.util.List; import java.util.concurrent.TimeUnit; @@ -21,6 +22,7 @@ public class W3CPTagsCodec extends PTagsCodec { private static final int MAX_HEADER_SIZE = 256; private static final String DATADOG_MEMBER_KEY = "dd="; + private static final String OTEL_MEMBER_KEY = "ot="; private static final int EMPTY_SIZE = DATADOG_MEMBER_KEY.length(); // 3 private static final char MEMBER_SEPARATOR = ','; private static final char ELEMENT_SEPARATOR = ';'; @@ -49,44 +51,66 @@ PropagationTags fromHeaderValue(PTagsFactory tagsFactory, String value) { int ddMemberValueEnd = -1; // dd member value end position including OWS (exclusive) int memberIndex = 0; int ddMemberIndex = -1; + int otelMemberStart = -1; + int otelMemberValueStart = -1; + int otelMemberValueEnd = -1; + int otelMemberPosition = -1; while (memberStart < len) { if (memberIndex == MAX_MEMBER_COUNT) { // TODO should we return one with an error? // TODO should we try to pick up the `dd` member anyway? return tagsFactory.empty(); } - if (ddMemberIndex == -1 && value.startsWith(DATADOG_MEMBER_KEY, memberStart)) { - ddMemberStart = memberStart; - ddMemberIndex = memberIndex; - } // Validate the member key - int pos = validateMemberKey(value, memberStart); - if (pos < 0) { + int memberValueStart = validateMemberKey(value, memberStart); + if (memberValueStart < 0) { // TODO should we return one with an error? return tagsFactory.empty(); } - if (ddMemberValueStart == -1 && ddMemberIndex != -1) { - ddMemberValueStart = pos; - } - pos = validateMemberValue(value, pos); - if (pos < 0) { + int memberValueEnd = validateMemberValue(value, memberValueStart); + if (memberValueEnd < 0) { // TODO should we return one with an error? return tagsFactory.empty(); } - if (ddMemberValueEnd == -1 && ddMemberIndex != -1) { - ddMemberValueEnd = pos; + + boolean datadogMember = + ddMemberIndex == -1 && value.startsWith(DATADOG_MEMBER_KEY, memberStart); + boolean otelMember = + !datadogMember && otelMemberStart == -1 && value.startsWith(OTEL_MEMBER_KEY, memberStart); + if (datadogMember) { + ddMemberStart = memberStart; + ddMemberValueStart = memberValueStart; + ddMemberIndex = memberIndex; + ddMemberValueEnd = memberValueEnd; + + } else if (otelMember) { + otelMemberStart = memberStart; + otelMemberValueStart = memberValueStart; + otelMemberValueEnd = memberValueEnd; + otelMemberPosition = memberIndex; } - memberStart = findNextMember(value, pos); + + memberIndex++; + memberStart = findNextMember(value, memberValueEnd); if (memberStart < 0) { // TODO should we return one with an error? return tagsFactory.empty(); } - memberIndex++; + } + + OtelTraceState otelTraceState = null; + if (otelMemberStart != -1) { + int valueEnd = stripTrailingOWC(value, otelMemberValueStart, otelMemberValueEnd); + otelTraceState = + OtelTraceState.parse( + SubSequence.of(value, otelMemberValueStart, valueEnd), + otelMemberPosition, + 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); + return empty(tagsFactory, value, otelTraceState); } List tagPairs = null; @@ -158,7 +182,13 @@ PropagationTags fromHeaderValue(PTagsFactory tagsFactory, String value) { if (tagKey.equals(TRACE_ID_TAG)) { return tagsFactory.createInvalid(PROPAGATION_ERROR_MALFORMED_TID + tagValue); } - return empty(tagsFactory, value, firstMemberStart, ddMemberStart, ddMemberValueEnd); + return empty( + tagsFactory, + value, + firstMemberStart, + ddMemberStart, + ddMemberValueEnd, + otelTraceState); } if (tagKey.equals(DECISION_MAKER_TAG)) { decisionMakerTagValue = tagValue; @@ -201,7 +231,8 @@ PropagationTags fromHeaderValue(PTagsFactory tagsFactory, String value) { ddMemberValueEnd, maxUnknownSize, lastParentId, - orgPropagationMarkerTagValue); + orgPropagationMarkerTagValue, + otelTraceState); } @Override @@ -215,16 +246,24 @@ protected int estimateHeaderSize(PTags pTags) { if (pTags.getSamplingPriority() != PrioritySampling.UNSET) { size += 5; // 's:-?[0-9]' + delimiter } + boolean includesOriginalTracestate = false; if (pTags instanceof W3CPTags) { W3CPTags w3CPTags = (W3CPTags) pTags; size += w3CPTags.maxUnknownSize; if (w3CPTags.ddMemberStart != -1) { size += (w3CPTags.tracestate.length() - (w3CPTags.ddMemberValueEnd - w3CPTags.ddMemberStart)); + includesOriginalTracestate = true; } } else if (pTags.tracestate != null) { // We assume there is no Datadog list-member size += pTags.tracestate.length(); + includesOriginalTracestate = true; + } + OtelTraceState otelTraceState = pTags.getOtelTraceState(); + if (otelTraceState != null) { + size -= includesOriginalTracestate ? otelTraceState.getOriginalSize() : 0; + size += OTEL_MEMBER_KEY.length() + otelTraceState.length() + 1; } return size; } @@ -290,9 +329,8 @@ protected int appendSuffix(StringBuilder sb, PTags ptags, int size) { sb.setLength(0); size = 0; } - // Append all other non-Datadog list-members - int newSize = cleanUpAndAppendSuffix(sb, ptags, size); - if (newSize != size) { + // Append the managed OTel member and all other non-Datadog list-members + if (appendOtelAndVendorMembers(sb, ptags, size != 0)) { // We don't care about the total size in bytes here, but only the fact that we added something // that should be returned size = Math.max(size, EMPTY_SIZE + 1); @@ -698,50 +736,129 @@ private static int cleanUpAndAppendUnknown(StringBuilder sb, W3CPTags w3CPTags, return size; } - private static int cleanUpAndAppendSuffix(StringBuilder sb, PTags ptags, int size) { + private static boolean appendOtelAndVendorMembers( + StringBuilder sb, PTags ptags, boolean hasDatadogMember) { String original = ptags.tracestate; - if (original == null) { - return size; - } - int ddMemberStart = (ptags instanceof W3CPTags) ? ((W3CPTags) ptags).ddMemberStart : -1; - int remainingMemberAllowed = size == 0 ? MAX_MEMBER_COUNT : MAX_MEMBER_COUNT - 1; - int len = original.length(); - int memberStart = findNextMember(original, 0); - while (memberStart < len) { + OtelTraceState otelTraceState = ptags.getOtelTraceState(); + int remainingMembers = MAX_MEMBER_COUNT - (hasDatadogMember ? 1 : 0); + int otherMemberPosition = 0; + int originalMemberPosition = 0; + int otelMemberPositionOffset = 0; + int otelMemberOriginalPosition = + otelTraceState == null ? -1 : otelTraceState.getOriginalPosition(); + boolean otelTraceStateAppended = false; + boolean memberAppended = false; + int len = original == null ? 0 : original.length(); + int memberStart = original == null ? 0 : findNextMember(original, 0); + while (memberStart < len && remainingMembers > 0) { // Look for member end position int memberEnd = original.indexOf(MEMBER_SEPARATOR, memberStart); if (memberEnd < 0) { memberEnd = len; } - // Try to define Datadog member start if not already found - if (ddMemberStart == -1) { - if (original.startsWith(DATADOG_MEMBER_KEY, memberStart)) { - ddMemberStart = memberStart; - } - } - // Skip Datadog member (already added with prefix and tags) - if (memberStart != ddMemberStart) { - if (sb.length() > 0) { - sb.append(MEMBER_SEPARATOR); - size++; + boolean datadogMember = original.startsWith(DATADOG_MEMBER_KEY, memberStart); + boolean managedMember = datadogMember || original.startsWith(OTEL_MEMBER_KEY, memberStart); + // offset to correct for dd members that were dropped/relocated before ot's original position + if (datadogMember && originalMemberPosition < otelMemberOriginalPosition) { + otelMemberPositionOffset++; + } + if (!managedMember) { + if (otelTraceState != null + && !otelTraceStateAppended + && otelMemberOriginalPosition - otelMemberPositionOffset == otherMemberPosition) { + appendMember(sb, OTEL_MEMBER_KEY, otelTraceState.getValue()); + remainingMembers--; + otelTraceStateAppended = true; + memberAppended = true; + if (remainingMembers == 0) { + break; + } } int end = stripTrailingOWC(original, memberStart, memberEnd); - sb.append(original, memberStart, end); - size += (end - memberStart); - remainingMemberAllowed--; - } - // Check if remaining members are allowed - if (remainingMemberAllowed == 0) { - memberStart = len; - } else { - memberStart = findNextMember(original, memberEnd + 1); + appendMember(sb, original, memberStart, end); + remainingMembers--; + otherMemberPosition++; + memberAppended = true; + } + originalMemberPosition++; + memberStart = findNextMember(original, memberEnd + 1); + } + if (otelTraceState != null + && !otelTraceStateAppended + && remainingMembers > 0 + && otelMemberOriginalPosition - otelMemberPositionOffset == otherMemberPosition) { + appendMember(sb, OTEL_MEMBER_KEY, otelTraceState.getValue()); + memberAppended = true; + } + return memberAppended; + } + + private static void appendMember(StringBuilder sb, String member, int start, int end) { + if (sb.length() != 0) { + sb.append(MEMBER_SEPARATOR); + } + sb.append(member, start, end); + } + + private static void appendMember(StringBuilder sb, String key, CharSequence value) { + if (sb.length() != 0) { + sb.append(MEMBER_SEPARATOR); + } + sb.append(key).append(value); + } + + static OtelTraceState extractOtelTraceState(String tracestate) { + if (tracestate == null || tracestate.isEmpty()) { + return null; + } + int memberPosition = 0; + int firstMemberStart = findNextMember(tracestate, 0); + int memberStart = firstMemberStart; + int otelMemberStart = -1; + int otelMemberValueStart = -1; + int otelMemberValueEnd = -1; + while (memberStart < tracestate.length()) { + int memberValueStart = validateMemberKey(tracestate, memberStart); + if (memberValueStart < 0) { + return null; + } + int memberValueEnd = validateMemberValue(tracestate, memberValueStart); + if (memberValueEnd < 0) { + return null; + } + if (tracestate.startsWith(OTEL_MEMBER_KEY, memberStart)) { + otelMemberStart = memberStart; + otelMemberValueStart = memberValueStart; + otelMemberValueEnd = memberValueEnd; + break; } + memberPosition++; + memberStart = findNextMember(tracestate, memberValueEnd); } - return size; + if (otelMemberStart == -1) { + return null; + } + int valueEnd = stripTrailingOWC(tracestate, otelMemberValueStart, otelMemberValueEnd); + return OtelTraceState.parse( + SubSequence.of(tracestate, otelMemberValueStart, valueEnd), + memberPosition, + memberContributionSize(tracestate, firstMemberStart, otelMemberStart, otelMemberValueEnd)); + } + + private static int memberContributionSize( + String tracestate, int firstMemberStart, int memberStart, int memberEnd) { + int memberSize = memberEnd - memberStart; + boolean isOnlyMember = memberStart == firstMemberStart && memberEnd == tracestate.length(); + return isOnlyMember ? memberSize : memberSize + 1; } static W3CPTags empty(PTagsFactory factory, String original) { - return empty(factory, original, 0, -1, -1); + return empty(factory, original, extractOtelTraceState(original)); + } + + private static W3CPTags empty( + PTagsFactory factory, String original, OtelTraceState otelTraceState) { + return empty(factory, original, 0, -1, -1, otelTraceState); } private static W3CPTags empty( @@ -749,7 +866,8 @@ private static W3CPTags empty( String original, int firstMemberStart, int ddMemberStart, - int ddMemberValueEnd) { + int ddMemberValueEnd, + OtelTraceState otelTraceState) { return new W3CPTags( factory, null, @@ -764,7 +882,8 @@ private static W3CPTags empty( ddMemberValueEnd, 0, null, - null); + null, + otelTraceState); } private static class W3CPTags extends PTags { @@ -799,7 +918,8 @@ public W3CPTags( int ddMemberValueEnd, int maxUnknownSize, CharSequence lastParentId, - TagValue orgPropagationMarkerTagValue) { + TagValue orgPropagationMarkerTagValue, + OtelTraceState otelTraceState) { super( factory, tagPairs, @@ -815,6 +935,7 @@ public W3CPTags( this.ddMemberStart = ddMemberStart; this.ddMemberValueEnd = ddMemberValueEnd; this.maxUnknownSize = maxUnknownSize; + setOtelTraceState(otelTraceState); } @Override diff --git a/dd-trace-core/src/test/java/datadog/trace/core/propagation/W3CPropagationTagsTest.java b/dd-trace-core/src/test/java/datadog/trace/core/propagation/W3CPropagationTagsTest.java index 491b8fb32ab..6f920526138 100644 --- a/dd-trace-core/src/test/java/datadog/trace/core/propagation/W3CPropagationTagsTest.java +++ b/dd-trace-core/src/test/java/datadog/trace/core/propagation/W3CPropagationTagsTest.java @@ -241,6 +241,9 @@ static IntStream memberCountArguments() { "dd before other | 'dd=s:0;t.dm:934086a687-3,other=whatever' | 'dd=s:0;t.dm:934086a687-3,other=whatever' | [_dd.p.dm: 934086a687-3] ", "some before dd before other | 'some=thing,dd=s:0;t.dm:934086a687-3,other=whatever' | 'dd=s:0;t.dm:934086a687-3,some=thing,other=whatever' | [_dd.p.dm: 934086a687-3] ", "no dd | 'some=thing,other=whatever' | 'some=thing,other=whatever' | [:] ", + "duplicate dd before ot | 'dd=s:1,dd=s:0,ot=rv:ef284ace7a91e1' | 'dd=s:1,ot=rv:ef284ace7a91e1' | [:] ", + "dd offset before ot | 'first=value,dd=s:1,sec=value,dd=s:0,ot=rv:ef284ace7a91e1' | 'dd=s:1,first=value,sec=value,ot=rv:ef284ace7a91e1' | [:] ", + "dd after ot does not offset | 'first=value,ot=rv:ef284ace7a91e1,dd=s:1,sec=value' | 'dd=s:1,first=value,ot=rv:ef284ace7a91e1,sec=value' | [:] ", "dd with origin and dm | 'dd=s:0;o:some;t.dm:934086a686-4' | 'dd=s:0;o:some;t.dm:934086a686-4' | [_dd.p.dm: 934086a686-4] ", "dd with unknown key | 'dd=s:0;x:unknown;t.dm:934086a686-4' | 'dd=s:0;t.dm:934086a686-4;x:unknown' | [_dd.p.dm: 934086a686-4] ", "other before dd with unknown | 'other=whatever,dd=s:0;x:unknown;t.dm:934086a686-4' | 'dd=s:0;t.dm:934086a686-4;x:unknown,other=whatever' | [_dd.p.dm: 934086a686-4] ", diff --git a/dd-trace-core/src/test/java/datadog/trace/core/propagation/ptags/OtelTraceStateParsingTest.java b/dd-trace-core/src/test/java/datadog/trace/core/propagation/ptags/OtelTraceStateParsingTest.java new file mode 100644 index 00000000000..97a6accae40 --- /dev/null +++ b/dd-trace-core/src/test/java/datadog/trace/core/propagation/ptags/OtelTraceStateParsingTest.java @@ -0,0 +1,43 @@ +package datadog.trace.core.propagation.ptags; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; + +import datadog.trace.util.SubSequence; +import org.junit.jupiter.api.Test; + +class OtelTraceStateParsingTest { + private static final String VALUE = "rv:0123456789abcd"; + private static final int INHERITED_POSITION = 2; + private static final int ORIGINAL_MEMBER_CONTRIBUTION_SIZE = 21; + + @Test + void ignoresAbsentValues() { + assertNull(OtelTraceState.parse(null, INHERITED_POSITION, ORIGINAL_MEMBER_CONTRIBUTION_SIZE)); + assertNull(OtelTraceState.parse("", INHERITED_POSITION, ORIGINAL_MEMBER_CONTRIBUTION_SIZE)); + } + + @Test + void retainsValueAndMemberMetadata() { + SubSequence value = SubSequence.of(VALUE, 0, VALUE.length()); + OtelTraceState state = + OtelTraceState.parse(value, INHERITED_POSITION, ORIGINAL_MEMBER_CONTRIBUTION_SIZE); + + assertNotNull(state); + assertSame(value, state.getValue()); + assertEquals(VALUE.length(), state.length()); + assertEquals(INHERITED_POSITION, state.getOriginalPosition()); + assertEquals(ORIGINAL_MEMBER_CONTRIBUTION_SIZE, state.getOriginalSize()); + } + + @Test + void extractsOriginalMemberPosition() { + OtelTraceState state = + W3CPTagsCodec.extractOtelTraceState("first=value,dd=s:1,dd=s:0,ot=" + VALUE); + + assertNotNull(state); + assertEquals(3, state.getOriginalPosition()); + } +}