1717import com .google .protobuf .CodedInputStream ;
1818import com .google .protobuf .WireFormat ;
1919import datadog .trace .api .DD128bTraceId ;
20+ import datadog .trace .api .DDTraceId ;
2021import datadog .trace .api .TracePropagationStyle ;
2122import datadog .trace .api .sampling .PrioritySampling ;
2223import datadog .trace .api .sampling .SamplingMechanism ;
@@ -173,20 +174,43 @@ SpanSpec use128BitTraceId() {
173174 this .use128BitTraceId = true ;
174175 return this ;
175176 }
177+
178+ /**
179+ * Sets the origin propagated via an {@link ExtractedContext} parent so that {@code
180+ * metadata.getOrigin()} is non-null and the {@code _dd.origin} attribute is written.
181+ */
182+ SpanSpec origin (String origin ) {
183+ this .origin = origin ;
184+ return this ;
185+ }
186+
187+ /** Trace origin carried in the extracted parent context; {@code null} = no origin. */
188+ String origin ;
176189 }
177190
178- /** Descriptor for a single span link: the index of the target span and optional attributes. */
191+ /**
192+ * Descriptor for a single span link: target span index, optional attributes, tracestate, and
193+ * flags.
194+ */
179195 static final class LinkSpec {
180196 final int targetIndex ;
181197 final SpanAttributes attributes ;
198+ final String traceState ;
199+ final byte traceFlags ;
182200
183201 LinkSpec (int targetIndex ) {
184- this (targetIndex , SpanAttributes .EMPTY );
202+ this (targetIndex , SpanAttributes .EMPTY , "" , SpanLink . DEFAULT_FLAGS );
185203 }
186204
187205 LinkSpec (int targetIndex , SpanAttributes attributes ) {
206+ this (targetIndex , attributes , "" , SpanLink .DEFAULT_FLAGS );
207+ }
208+
209+ LinkSpec (int targetIndex , SpanAttributes attributes , String traceState , byte traceFlags ) {
188210 this .targetIndex = targetIndex ;
189211 this .attributes = attributes ;
212+ this .traceState = traceState ;
213+ this .traceFlags = traceFlags ;
190214 }
191215 }
192216
@@ -329,6 +353,10 @@ private static SpanSpec linkedSpan(String resourceName, int... targetIndices) {
329353 links );
330354 }
331355
356+ /**
357+ * A span with one {@link SpanLink} pointing to the span at {@code targetIndex}, carrying the
358+ * given {@link SpanAttributes}.
359+ */
332360 /**
333361 * A span with one {@link SpanLink} pointing to the span at {@code targetIndex}, carrying the
334362 * given {@link SpanAttributes}.
@@ -351,6 +379,44 @@ private static SpanSpec linkedSpanWithAttrs(
351379 new LinkSpec (targetIndex , attributes ));
352380 }
353381
382+ /** A span with one {@link SpanLink} carrying the given W3C tracestate string. */
383+ private static SpanSpec linkedSpanWithTracestate (
384+ String resourceName , int targetIndex , String traceState ) {
385+ return new SpanSpec (
386+ resourceName ,
387+ "op.linked" ,
388+ "web" ,
389+ null ,
390+ BASE_MICROS ,
391+ BASE_MICROS + DURATION_MICROS ,
392+ false ,
393+ null ,
394+ 0 ,
395+ null ,
396+ new HashMap <>(),
397+ -1 ,
398+ new LinkSpec (targetIndex , SpanAttributes .EMPTY , traceState , SpanLink .DEFAULT_FLAGS ));
399+ }
400+
401+ /** A span with one {@link SpanLink} carrying the given trace flags. */
402+ private static SpanSpec linkedSpanWithFlags (
403+ String resourceName , int targetIndex , byte traceFlags ) {
404+ return new SpanSpec (
405+ resourceName ,
406+ "op.linked" ,
407+ "web" ,
408+ null ,
409+ BASE_MICROS ,
410+ BASE_MICROS + DURATION_MICROS ,
411+ false ,
412+ null ,
413+ 0 ,
414+ null ,
415+ new HashMap <>(),
416+ -1 ,
417+ new LinkSpec (targetIndex , SpanAttributes .EMPTY , "" , traceFlags ));
418+ }
419+
354420 private static Map <String , Object > tags (Object ... keyValues ) {
355421 Map <String , Object > map = new HashMap <>();
356422 for (int i = 0 ; i < keyValues .length ; i += 2 ) {
@@ -441,6 +507,16 @@ static Stream<Arguments> cases() {
441507 "attr.linked" ,
442508 0 ,
443509 SpanAttributes .builder ().put ("link.source" , "test" ).build ()))),
510+ Arguments .of (
511+ "span link with tracestate — Link.trace_state field written" ,
512+ asList (
513+ span ("anchor.op" , "anchor.op" , "web" ),
514+ linkedSpanWithTracestate ("tracestate.linked" , 0 , "vendor=abc;p=123" ))),
515+ Arguments .of (
516+ "span link with non-default flags — extra flag bit preserved alongside SAMPLED" ,
517+ asList (
518+ span ("anchor.op" , "anchor.op" , "web" ),
519+ linkedSpanWithFlags ("flags.linked" , 0 , (byte ) 0x02 ))),
444520
445521 // ── metadata paths ────────────────────────────────────────────────────
446522 Arguments .of (
@@ -449,6 +525,9 @@ static Stream<Arguments> cases() {
449525 Arguments .of (
450526 "span with http status code — http.status_code written via setHttpStatusCode" ,
451527 asList (span ("GET /resource" , "servlet.request" , "web" ).httpStatusCode (404 ))),
528+ Arguments .of (
529+ "span with origin — _dd.origin attribute written" ,
530+ asList (span ("GET /api" , "servlet.request" , "web" ).origin ("rum" ))),
452531 Arguments .of (
453532 "span with 128-bit trace ID — high-order trace_id bytes non-zero" ,
454533 asList (span ("GET /api" , "servlet.request" , "web" ).use128BitTraceId ())),
@@ -560,6 +639,17 @@ private static List<DDSpan> buildSpans(List<SpanSpec> specs) {
560639 PropagationTags .factory ().empty (),
561640 TracePropagationStyle .DATADOG );
562641 agentSpan = TRACER .startSpan ("test" , spec .operationName , parent128 , spec .startMicros );
642+ } else if (spec .origin != null ) {
643+ ExtractedContext parentWithOrigin =
644+ new ExtractedContext (
645+ DDTraceId .from (1L ),
646+ 0L ,
647+ PrioritySampling .UNSET ,
648+ spec .origin ,
649+ PropagationTags .factory ().empty (),
650+ TracePropagationStyle .DATADOG );
651+ agentSpan =
652+ TRACER .startSpan ("test" , spec .operationName , parentWithOrigin , spec .startMicros );
563653 } else if (spec .parentIndex >= 0 ) {
564654 agentSpan =
565655 TRACER .startSpan (
@@ -606,13 +696,11 @@ private static List<DDSpan> buildSpans(List<SpanSpec> specs) {
606696
607697 for (LinkSpec link : spec .links ) {
608698 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 ));
699+ SpanLink .from (
700+ spans .get (link .targetIndex ).context (),
701+ link .traceFlags ,
702+ link .traceState ,
703+ link .attributes ));
616704 }
617705
618706 agentSpan .finish (spec .finishMicros );
@@ -859,6 +947,11 @@ private static void verifySpan(CodedInputStream sp, DDSpan span, SpanSpec spec,
859947 + caseName
860948 + "]" );
861949 }
950+ if (spec .origin != null ) {
951+ assertTrue (
952+ attrKeys .contains ("_dd.origin" ),
953+ "attributes must include '_dd.origin' when origin is set [" + caseName + "]" );
954+ }
862955
863956 // ── status (field 15) ─────────────────────────────────────────────────────
864957 if (spec .error ) {
@@ -892,7 +985,9 @@ private static void verifyLink(CodedInputStream link, LinkSpec linkSpec, String
892985 throws IOException {
893986 byte [] traceId = null ;
894987 byte [] spanId = null ;
988+ String parsedTraceState = null ;
895989 Set <String > linkAttrKeys = new HashSet <>();
990+ int parsedFlags = 0 ;
896991 while (!link .isAtEnd ()) {
897992 int tag = link .readTag ();
898993 switch (WireFormat .getTagFieldNumber (tag )) {
@@ -902,9 +997,15 @@ private static void verifyLink(CodedInputStream link, LinkSpec linkSpec, String
902997 case 2 :
903998 spanId = link .readBytes ().toByteArray ();
904999 break ;
1000+ case 3 :
1001+ parsedTraceState = link .readString ();
1002+ break ;
9051003 case 4 :
9061004 linkAttrKeys .add (readKeyValueKey (link .readBytes ().newCodedInput ()));
9071005 break ;
1006+ case 6 :
1007+ parsedFlags = link .readFixed32 ();
1008+ break ;
9081009 default :
9091010 link .skipField (tag );
9101011 }
@@ -913,6 +1014,14 @@ private static void verifyLink(CodedInputStream link, LinkSpec linkSpec, String
9131014 assertEquals (16 , traceId .length , "Link.trace_id must be 16 bytes [" + caseName + "]" );
9141015 assertNotNull (spanId , "Link.span_id must be present [" + caseName + "]" );
9151016 assertEquals (8 , spanId .length , "Link.span_id must be 8 bytes [" + caseName + "]" );
1017+ if (!linkSpec .traceState .isEmpty ()) {
1018+ assertEquals (
1019+ linkSpec .traceState , parsedTraceState , "Link.trace_state mismatch [" + caseName + "]" );
1020+ }
1021+ // SpanLink.from() ORs in the SAMPLED_FLAG (0x01) when the target context has positive
1022+ // sampling priority, which all test anchor spans have via the default tracer sampler.
1023+ int expectedFlags = Byte .toUnsignedInt ((byte ) (linkSpec .traceFlags | 0x01 ));
1024+ assertEquals (expectedFlags , parsedFlags , "Link.flags mismatch [" + caseName + "]" );
9161025 for (String expectedKey : linkSpec .attributes .asMap ().keySet ()) {
9171026 assertTrue (
9181027 linkAttrKeys .contains (expectedKey ),
0 commit comments