66import static datadog .trace .bootstrap .instrumentation .api .Tags .SPAN_KIND_PRODUCER ;
77import static datadog .trace .bootstrap .instrumentation .api .Tags .SPAN_KIND_SERVER ;
88import static java .util .Arrays .asList ;
9+ import static java .util .Arrays .copyOfRange ;
910import static java .util .Collections .emptyList ;
1011import static org .junit .jupiter .api .Assertions .assertEquals ;
1112import static org .junit .jupiter .api .Assertions .assertFalse ;
1718import com .google .protobuf .CodedInputStream ;
1819import com .google .protobuf .WireFormat ;
1920import datadog .trace .api .DD128bTraceId ;
21+ import datadog .trace .api .DDTraceId ;
2022import datadog .trace .api .TracePropagationStyle ;
2123import datadog .trace .api .sampling .PrioritySampling ;
2224import datadog .trace .api .sampling .SamplingMechanism ;
@@ -130,6 +132,9 @@ static final class SpanSpec {
130132 */
131133 boolean use128BitTraceId ;
132134
135+ /** Trace origin carried in the extracted parent context; {@code null} = no origin. */
136+ String origin ;
137+
133138 SpanSpec (
134139 String resourceName ,
135140 String operationName ,
@@ -173,20 +178,40 @@ SpanSpec use128BitTraceId() {
173178 this .use128BitTraceId = true ;
174179 return this ;
175180 }
181+
182+ /**
183+ * Sets the origin propagated via an {@link ExtractedContext} parent so that {@code
184+ * metadata.getOrigin()} is non-null and the {@code _dd.origin} attribute is written.
185+ */
186+ SpanSpec origin (String origin ) {
187+ this .origin = origin ;
188+ return this ;
189+ }
176190 }
177191
178- /** Descriptor for a single span link: the index of the target span and optional attributes. */
192+ /**
193+ * Descriptor for a single span link: target span index, optional attributes, tracestate, and
194+ * flags.
195+ */
179196 static final class LinkSpec {
180197 final int targetIndex ;
181198 final SpanAttributes attributes ;
199+ final String traceState ;
200+ final byte traceFlags ;
182201
183202 LinkSpec (int targetIndex ) {
184- this (targetIndex , SpanAttributes .EMPTY );
203+ this (targetIndex , SpanAttributes .EMPTY , "" , SpanLink . DEFAULT_FLAGS );
185204 }
186205
187206 LinkSpec (int targetIndex , SpanAttributes attributes ) {
207+ this (targetIndex , attributes , "" , SpanLink .DEFAULT_FLAGS );
208+ }
209+
210+ LinkSpec (int targetIndex , SpanAttributes attributes , String traceState , byte traceFlags ) {
188211 this .targetIndex = targetIndex ;
189212 this .attributes = attributes ;
213+ this .traceState = traceState ;
214+ this .traceFlags = traceFlags ;
190215 }
191216 }
192217
@@ -195,6 +220,13 @@ static final class LinkSpec {
195220 private static final long BASE_MICROS = 1_700_000_000_000_000L ;
196221 private static final long DURATION_MICROS = 500_000L ; // 500 ms
197222
223+ /**
224+ * A known 128-bit trace ID used by {@link SpanSpec#use128BitTraceId} test cases. High-order bits
225+ * are non-zero so the test can assert the proto encodes them correctly.
226+ */
227+ static final DD128bTraceId TRACE_ID_128BIT =
228+ DD128bTraceId .from (0x0123456789abcdefL , 0xfedcba9876543210L );
229+
198230 private static SpanSpec span (String resourceName , String operationName , String spanType ) {
199231 return new SpanSpec (
200232 resourceName ,
@@ -351,6 +383,44 @@ private static SpanSpec linkedSpanWithAttrs(
351383 new LinkSpec (targetIndex , attributes ));
352384 }
353385
386+ /** A span with one {@link SpanLink} carrying the given W3C tracestate string. */
387+ private static SpanSpec linkedSpanWithTracestate (
388+ String resourceName , int targetIndex , String traceState ) {
389+ return new SpanSpec (
390+ resourceName ,
391+ "op.linked" ,
392+ "web" ,
393+ null ,
394+ BASE_MICROS ,
395+ BASE_MICROS + DURATION_MICROS ,
396+ false ,
397+ null ,
398+ 0 ,
399+ null ,
400+ new HashMap <>(),
401+ -1 ,
402+ new LinkSpec (targetIndex , SpanAttributes .EMPTY , traceState , SpanLink .DEFAULT_FLAGS ));
403+ }
404+
405+ /** A span with one {@link SpanLink} carrying the given trace flags. */
406+ private static SpanSpec linkedSpanWithFlags (
407+ String resourceName , int targetIndex , byte traceFlags ) {
408+ return new SpanSpec (
409+ resourceName ,
410+ "op.linked" ,
411+ "web" ,
412+ null ,
413+ BASE_MICROS ,
414+ BASE_MICROS + DURATION_MICROS ,
415+ false ,
416+ null ,
417+ 0 ,
418+ null ,
419+ new HashMap <>(),
420+ -1 ,
421+ new LinkSpec (targetIndex , SpanAttributes .EMPTY , "" , traceFlags ));
422+ }
423+
354424 private static Map <String , Object > tags (Object ... keyValues ) {
355425 Map <String , Object > map = new HashMap <>();
356426 for (int i = 0 ; i < keyValues .length ; i += 2 ) {
@@ -441,6 +511,16 @@ static Stream<Arguments> cases() {
441511 "attr.linked" ,
442512 0 ,
443513 SpanAttributes .builder ().put ("link.source" , "test" ).build ()))),
514+ Arguments .of (
515+ "span link with tracestate — Link.trace_state field written" ,
516+ asList (
517+ span ("anchor.op" , "anchor.op" , "web" ),
518+ linkedSpanWithTracestate ("tracestate.linked" , 0 , "vendor=abc;p=123" ))),
519+ Arguments .of (
520+ "span link with non-default flags — extra flag bit preserved alongside SAMPLED" ,
521+ asList (
522+ span ("anchor.op" , "anchor.op" , "web" ),
523+ linkedSpanWithFlags ("flags.linked" , 0 , (byte ) 0x02 ))),
444524
445525 // ── metadata paths ────────────────────────────────────────────────────
446526 Arguments .of (
@@ -449,6 +529,9 @@ static Stream<Arguments> cases() {
449529 Arguments .of (
450530 "span with http status code — http.status_code written via setHttpStatusCode" ,
451531 asList (span ("GET /resource" , "servlet.request" , "web" ).httpStatusCode (404 ))),
532+ Arguments .of (
533+ "span with origin — _dd.origin attribute written" ,
534+ asList (span ("GET /api" , "servlet.request" , "web" ).origin ("rum" ))),
452535 Arguments .of (
453536 "span with 128-bit trace ID — high-order trace_id bytes non-zero" ,
454537 asList (span ("GET /api" , "servlet.request" , "web" ).use128BitTraceId ())),
@@ -538,13 +621,6 @@ void testCollectSpans(String caseName, List<SpanSpec> specs) throws IOException
538621
539622 // ── span construction ─────────────────────────────────────────────────────
540623
541- /**
542- * A known 128-bit trace ID used by {@link SpanSpec#use128BitTraceId} test cases. High-order bits
543- * are non-zero so the test can assert the proto encodes them correctly.
544- */
545- static final DD128bTraceId TRACE_ID_128BIT =
546- DD128bTraceId .from (0x0123456789abcdefL , 0xfedcba9876543210L );
547-
548624 /** Builds {@link DDSpan} instances from the given specs, collecting them in order. */
549625 private static List <DDSpan > buildSpans (List <SpanSpec > specs ) {
550626 List <DDSpan > spans = new ArrayList <>(specs .size ());
@@ -560,6 +636,17 @@ private static List<DDSpan> buildSpans(List<SpanSpec> specs) {
560636 PropagationTags .factory ().empty (),
561637 TracePropagationStyle .DATADOG );
562638 agentSpan = TRACER .startSpan ("test" , spec .operationName , parent128 , spec .startMicros );
639+ } else if (spec .origin != null ) {
640+ ExtractedContext parentWithOrigin =
641+ new ExtractedContext (
642+ DDTraceId .ONE ,
643+ 0L ,
644+ PrioritySampling .UNSET ,
645+ spec .origin ,
646+ PropagationTags .factory ().empty (),
647+ TracePropagationStyle .DATADOG );
648+ agentSpan =
649+ TRACER .startSpan ("test" , spec .operationName , parentWithOrigin , spec .startMicros );
563650 } else if (spec .parentIndex >= 0 ) {
564651 agentSpan =
565652 TRACER .startSpan (
@@ -606,13 +693,11 @@ private static List<DDSpan> buildSpans(List<SpanSpec> specs) {
606693
607694 for (LinkSpec link : spec .links ) {
608695 agentSpan .addLink (
609- link .attributes .isEmpty ()
610- ? SpanLink .from (spans .get (link .targetIndex ).context ())
611- : SpanLink .from (
612- spans .get (link .targetIndex ).context (),
613- SpanLink .DEFAULT_FLAGS ,
614- "" ,
615- link .attributes ));
696+ SpanLink .from (
697+ spans .get (link .targetIndex ).context (),
698+ link .traceFlags ,
699+ link .traceState ,
700+ link .attributes ));
616701 }
617702
618703 agentSpan .finish (spec .finishMicros );
@@ -760,8 +845,7 @@ private static void verifySpan(CodedInputStream sp, DDSpan span, SpanSpec spec,
760845 assertEquals (16 , parsedTraceId .length , "trace_id must be 16 bytes [" + caseName + "]" );
761846 if (spec .use128BitTraceId ) {
762847 // high-order bytes occupy parsedTraceId[8..15] (little-endian in the wire format)
763- long highOrderBytes =
764- readLittleEndianLong (java .util .Arrays .copyOfRange (parsedTraceId , 8 , 16 ));
848+ long highOrderBytes = readLittleEndianLong (copyOfRange (parsedTraceId , 8 , 16 ));
765849 assertNotEquals (
766850 0L ,
767851 highOrderBytes ,
@@ -859,6 +943,11 @@ private static void verifySpan(CodedInputStream sp, DDSpan span, SpanSpec spec,
859943 + caseName
860944 + "]" );
861945 }
946+ if (spec .origin != null ) {
947+ assertTrue (
948+ attrKeys .contains ("_dd.origin" ),
949+ "attributes must include '_dd.origin' when origin is set [" + caseName + "]" );
950+ }
862951
863952 // ── status (field 15) ─────────────────────────────────────────────────────
864953 if (spec .error ) {
@@ -892,7 +981,9 @@ private static void verifyLink(CodedInputStream link, LinkSpec linkSpec, String
892981 throws IOException {
893982 byte [] traceId = null ;
894983 byte [] spanId = null ;
984+ String parsedTraceState = null ;
895985 Set <String > linkAttrKeys = new HashSet <>();
986+ int parsedFlags = 0 ;
896987 while (!link .isAtEnd ()) {
897988 int tag = link .readTag ();
898989 switch (WireFormat .getTagFieldNumber (tag )) {
@@ -902,9 +993,15 @@ private static void verifyLink(CodedInputStream link, LinkSpec linkSpec, String
902993 case 2 :
903994 spanId = link .readBytes ().toByteArray ();
904995 break ;
996+ case 3 :
997+ parsedTraceState = link .readString ();
998+ break ;
905999 case 4 :
9061000 linkAttrKeys .add (readKeyValueKey (link .readBytes ().newCodedInput ()));
9071001 break ;
1002+ case 6 :
1003+ parsedFlags = link .readFixed32 ();
1004+ break ;
9081005 default :
9091006 link .skipField (tag );
9101007 }
@@ -913,6 +1010,14 @@ private static void verifyLink(CodedInputStream link, LinkSpec linkSpec, String
9131010 assertEquals (16 , traceId .length , "Link.trace_id must be 16 bytes [" + caseName + "]" );
9141011 assertNotNull (spanId , "Link.span_id must be present [" + caseName + "]" );
9151012 assertEquals (8 , spanId .length , "Link.span_id must be 8 bytes [" + caseName + "]" );
1013+ if (!linkSpec .traceState .isEmpty ()) {
1014+ assertEquals (
1015+ linkSpec .traceState , parsedTraceState , "Link.trace_state mismatch [" + caseName + "]" );
1016+ }
1017+ // SpanLink.from() ORs in the SAMPLED_FLAG (0x01) when the target context has positive
1018+ // sampling priority, which all test anchor spans have via the default tracer sampler.
1019+ int expectedFlags = Byte .toUnsignedInt ((byte ) (linkSpec .traceFlags | 0x01 ));
1020+ assertEquals (expectedFlags , parsedFlags , "Link.flags mismatch [" + caseName + "]" );
9161021 for (String expectedKey : linkSpec .attributes .asMap ().keySet ()) {
9171022 assertTrue (
9181023 linkAttrKeys .contains (expectedKey ),
0 commit comments