99import static java .util .Collections .emptyList ;
1010import static org .junit .jupiter .api .Assertions .assertEquals ;
1111import static org .junit .jupiter .api .Assertions .assertFalse ;
12+ import static org .junit .jupiter .api .Assertions .assertNotEquals ;
1213import static org .junit .jupiter .api .Assertions .assertNotNull ;
14+ import static org .junit .jupiter .api .Assertions .assertNull ;
1315import static org .junit .jupiter .api .Assertions .assertTrue ;
1416
1517import com .google .protobuf .CodedInputStream ;
1618import com .google .protobuf .WireFormat ;
19+ import datadog .trace .api .DD128bTraceId ;
20+ import datadog .trace .api .TracePropagationStyle ;
1721import datadog .trace .api .sampling .PrioritySampling ;
1822import datadog .trace .api .sampling .SamplingMechanism ;
1923import datadog .trace .bootstrap .instrumentation .api .AgentSpan ;
24+ import datadog .trace .bootstrap .instrumentation .api .SpanAttributes ;
2025import datadog .trace .bootstrap .instrumentation .api .SpanLink ;
2126import datadog .trace .core .CoreTracer ;
2227import datadog .trace .core .DDSpan ;
2328import datadog .trace .core .otlp .common .OtlpPayload ;
29+ import datadog .trace .core .propagation .ExtractedContext ;
30+ import datadog .trace .core .propagation .PropagationTags ;
2431import java .io .ByteArrayOutputStream ;
2532import java .io .IOException ;
2633import java .util .ArrayList ;
@@ -106,10 +113,22 @@ static final class SpanSpec {
106113 final int parentIndex ;
107114
108115 /**
109- * Indices into the already-built span list to link to (one {@link SpanLink} per entry). Each
110- * index must refer to a span that precedes this one in the list. An empty array means no links.
116+ * Links to add to this span (one {@link SpanLink} per entry). Each link targets a span that
117+ * precedes this one in the list. An empty array means no links.
111118 */
112- final int [] linkTargets ;
119+ final LinkSpec [] links ;
120+
121+ /** If true, the span is measured (sets the {@code _dd.measured} attribute). */
122+ boolean measured ;
123+
124+ /** Non-zero HTTP status code to set via {@code setHttpStatusCode}; 0 = not set. */
125+ int httpStatusCode ;
126+
127+ /**
128+ * If true, starts the span under a synthetic {@link ExtractedContext} carrying a known 128-bit
129+ * trace ID, exercising the high-order bytes of {@code writeTraceId}.
130+ */
131+ boolean use128BitTraceId ;
113132
114133 SpanSpec (
115134 String resourceName ,
@@ -124,7 +143,7 @@ static final class SpanSpec {
124143 String serviceName ,
125144 Map <String , Object > extraTags ,
126145 int parentIndex ,
127- int ... linkTargets ) {
146+ LinkSpec ... links ) {
128147 this .resourceName = resourceName ;
129148 this .operationName = operationName ;
130149 this .spanType = spanType ;
@@ -137,7 +156,37 @@ static final class SpanSpec {
137156 this .serviceName = serviceName ;
138157 this .extraTags = extraTags ;
139158 this .parentIndex = parentIndex ;
140- this .linkTargets = linkTargets ;
159+ this .links = links ;
160+ }
161+
162+ SpanSpec measured () {
163+ this .measured = true ;
164+ return this ;
165+ }
166+
167+ SpanSpec httpStatusCode (int code ) {
168+ this .httpStatusCode = code ;
169+ return this ;
170+ }
171+
172+ SpanSpec use128BitTraceId () {
173+ this .use128BitTraceId = true ;
174+ return this ;
175+ }
176+ }
177+
178+ /** Descriptor for a single span link: the index of the target span and optional attributes. */
179+ static final class LinkSpec {
180+ final int targetIndex ;
181+ final SpanAttributes attributes ;
182+
183+ LinkSpec (int targetIndex ) {
184+ this (targetIndex , SpanAttributes .EMPTY );
185+ }
186+
187+ LinkSpec (int targetIndex , SpanAttributes attributes ) {
188+ this .targetIndex = targetIndex ;
189+ this .attributes = attributes ;
141190 }
142191 }
143192
@@ -258,10 +307,34 @@ private static SpanSpec serviceSpan(String resourceName, String serviceName) {
258307 -1 );
259308 }
260309
310+ /** A span with {@link SpanLink}s pointing to the spans at the given {@code targetIndices}. */
311+ private static SpanSpec linkedSpan (String resourceName , int ... targetIndices ) {
312+ LinkSpec [] links = new LinkSpec [targetIndices .length ];
313+ for (int i = 0 ; i < targetIndices .length ; i ++) {
314+ links [i ] = new LinkSpec (targetIndices [i ]);
315+ }
316+ return new SpanSpec (
317+ resourceName ,
318+ "op.linked" ,
319+ "web" ,
320+ null ,
321+ BASE_MICROS ,
322+ BASE_MICROS + DURATION_MICROS ,
323+ false ,
324+ null ,
325+ 0 ,
326+ null ,
327+ new HashMap <>(),
328+ -1 ,
329+ links );
330+ }
331+
261332 /**
262- * A span with {@link SpanLink}s pointing to the spans at the given {@code linkTargets} indices.
333+ * A span with one {@link SpanLink} pointing to the span at {@code targetIndex}, carrying the
334+ * given {@link SpanAttributes}.
263335 */
264- private static SpanSpec linkedSpan (String resourceName , int ... linkTargets ) {
336+ private static SpanSpec linkedSpanWithAttrs (
337+ String resourceName , int targetIndex , SpanAttributes attributes ) {
265338 return new SpanSpec (
266339 resourceName ,
267340 "op.linked" ,
@@ -275,10 +348,9 @@ private static SpanSpec linkedSpan(String resourceName, int... linkTargets) {
275348 null ,
276349 new HashMap <>(),
277350 -1 ,
278- linkTargets );
351+ new LinkSpec ( targetIndex , attributes ) );
279352 }
280353
281- @ SuppressWarnings ("unchecked" )
282354 private static Map <String , Object > tags (Object ... keyValues ) {
283355 Map <String , Object > map = new HashMap <>();
284356 for (int i = 0 ; i < keyValues .length ; i += 2 ) {
@@ -361,6 +433,25 @@ static Stream<Arguments> cases() {
361433 span ("target.a" , "op.a" , "web" ),
362434 span ("target.b" , "op.b" , "web" ),
363435 linkedSpan ("multi.linked" , 0 , 1 ))),
436+ Arguments .of (
437+ "span link with attributes — link attributes written to proto" ,
438+ asList (
439+ span ("anchor.op" , "anchor.op" , "web" ),
440+ linkedSpanWithAttrs (
441+ "attr.linked" ,
442+ 0 ,
443+ SpanAttributes .builder ().put ("link.source" , "test" ).build ()))),
444+
445+ // ── metadata paths ────────────────────────────────────────────────────
446+ Arguments .of (
447+ "measured span — _dd.measured attribute written" ,
448+ asList (span ("measured.op" , "op.measured" , "web" ).measured ())),
449+ Arguments .of (
450+ "span with http status code — http.status_code written via setHttpStatusCode" ,
451+ asList (span ("GET /resource" , "servlet.request" , "web" ).httpStatusCode (404 ))),
452+ Arguments .of (
453+ "span with 128-bit trace ID — high-order trace_id bytes non-zero" ,
454+ asList (span ("GET /api" , "servlet.request" , "web" ).use128BitTraceId ())),
364455
365456 // ── multiple spans in one payload ─────────────────────────────────────
366457 Arguments .of (
@@ -447,12 +538,29 @@ void testCollectSpans(String caseName, List<SpanSpec> specs) throws IOException
447538
448539 // ── span construction ─────────────────────────────────────────────────────
449540
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+
450548 /** Builds {@link DDSpan} instances from the given specs, collecting them in order. */
451549 private static List <DDSpan > buildSpans (List <SpanSpec > specs ) {
452550 List <DDSpan > spans = new ArrayList <>(specs .size ());
453551 for (SpanSpec spec : specs ) {
454552 AgentSpan agentSpan ;
455- if (spec .parentIndex >= 0 ) {
553+ if (spec .use128BitTraceId ) {
554+ ExtractedContext parent128 =
555+ new ExtractedContext (
556+ TRACE_ID_128BIT ,
557+ 0L ,
558+ PrioritySampling .UNSET ,
559+ null ,
560+ PropagationTags .factory ().empty (),
561+ TracePropagationStyle .DATADOG );
562+ agentSpan = TRACER .startSpan ("test" , spec .operationName , parent128 , spec .startMicros );
563+ } else if (spec .parentIndex >= 0 ) {
456564 agentSpan =
457565 TRACER .startSpan (
458566 "test" ,
@@ -481,6 +589,12 @@ private static List<DDSpan> buildSpans(List<SpanSpec> specs) {
481589 agentSpan .setErrorMessage (spec .errorMessage );
482590 }
483591 }
592+ if (spec .measured ) {
593+ agentSpan .setMeasured (true );
594+ }
595+ if (spec .httpStatusCode != 0 ) {
596+ agentSpan .setHttpStatusCode (spec .httpStatusCode );
597+ }
484598
485599 spec .extraTags .forEach (
486600 (key , value ) -> {
@@ -490,8 +604,15 @@ private static List<DDSpan> buildSpans(List<SpanSpec> specs) {
490604 else if (value instanceof Double ) agentSpan .setTag (key , (double ) (Double ) value );
491605 });
492606
493- for (int linkTarget : spec .linkTargets ) {
494- agentSpan .addLink (SpanLink .from (spans .get (linkTarget ).context ()));
607+ for (LinkSpec link : spec .links ) {
608+ 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 ));
495616 }
496617
497618 agentSpan .finish (spec .finishMicros );
@@ -604,7 +725,7 @@ private static void verifySpan(CodedInputStream sp, DDSpan span, SpanSpec spec,
604725 attrKeys .add (readKeyValueKey (sp .readBytes ().newCodedInput ()));
605726 break ;
606727 case 13 :
607- verifyLink (sp .readBytes ().newCodedInput (), caseName );
728+ verifyLink (sp .readBytes ().newCodedInput (), spec . links [ linkCount ], caseName );
608729 linkCount ++;
609730 break ;
610731 case 15 :
@@ -637,6 +758,15 @@ private static void verifySpan(CodedInputStream sp, DDSpan span, SpanSpec spec,
637758 // ── trace_id (field 1): 16 bytes ─────────────────────────────────────────
638759 assertNotNull (parsedTraceId , "trace_id must be present [" + caseName + "]" );
639760 assertEquals (16 , parsedTraceId .length , "trace_id must be 16 bytes [" + caseName + "]" );
761+ if (spec .use128BitTraceId ) {
762+ // 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 ));
765+ assertNotEquals (
766+ 0L ,
767+ highOrderBytes ,
768+ "128-bit trace_id high-order bytes must be non-zero [" + caseName + "]" );
769+ }
640770
641771 // ── span_id (field 2): 8 bytes, encodes span.getSpanId() ─────────────────
642772 assertNotNull (parsedSpanId , "span_id must be present [" + caseName + "]" );
@@ -717,6 +847,19 @@ private static void verifySpan(CodedInputStream sp, DDSpan span, SpanSpec spec,
717847 "attributes must include extra tag '" + key + "' [" + caseName + "]" );
718848 }
719849
850+ if (spec .measured ) {
851+ assertTrue (
852+ attrKeys .contains ("_dd.measured" ),
853+ "attributes must include '_dd.measured' for measured spans [" + caseName + "]" );
854+ }
855+ if (spec .httpStatusCode != 0 ) {
856+ assertTrue (
857+ attrKeys .contains ("http.status_code" ),
858+ "attributes must include 'http.status_code' when set via setHttpStatusCode ["
859+ + caseName
860+ + "]" );
861+ }
862+
720863 // ── status (field 15) ─────────────────────────────────────────────────────
721864 if (spec .error ) {
722865 assertTrue (statusFound , "status must be present for error span [" + caseName + "]" );
@@ -725,30 +868,31 @@ private static void verifySpan(CodedInputStream sp, DDSpan span, SpanSpec spec,
725868 assertEquals (
726869 spec .errorMessage , parsedStatusMessage , "status.message mismatch [" + caseName + "]" );
727870 } else {
728- assertEquals (
729- null ,
730- parsedStatusMessage ,
731- "status.message must be absent when not set [" + caseName + "]" );
871+ assertNull (
872+ parsedStatusMessage , "status.message must be absent when not set [" + caseName + "]" );
732873 }
733874 } else {
734875 assertFalse (statusFound , "status must be absent for non-error span [" + caseName + "]" );
735876 }
736877
737878 // ── links (field 13) ──────────────────────────────────────────────────────
738- assertEquals (spec .linkTargets .length , linkCount , "link count mismatch [" + caseName + "]" );
879+ assertEquals (spec .links .length , linkCount , "link count mismatch [" + caseName + "]" );
739880 }
740881
741882 /**
742- * Parses a {@code Span.Link} message body and verifies trace_id and span_id are present.
883+ * Parses a {@code Span.Link} message body and verifies trace_id, span_id, and (if expected) link
884+ * attributes are present.
743885 *
744886 * <pre>
745887 * Link { bytes trace_id = 1; bytes span_id = 2; string trace_state = 3;
746888 * KeyValue attributes = 4; fixed32 flags = 6; }
747889 * </pre>
748890 */
749- private static void verifyLink (CodedInputStream link , String caseName ) throws IOException {
891+ private static void verifyLink (CodedInputStream link , LinkSpec linkSpec , String caseName )
892+ throws IOException {
750893 byte [] traceId = null ;
751894 byte [] spanId = null ;
895+ Set <String > linkAttrKeys = new HashSet <>();
752896 while (!link .isAtEnd ()) {
753897 int tag = link .readTag ();
754898 switch (WireFormat .getTagFieldNumber (tag )) {
@@ -758,6 +902,9 @@ private static void verifyLink(CodedInputStream link, String caseName) throws IO
758902 case 2 :
759903 spanId = link .readBytes ().toByteArray ();
760904 break ;
905+ case 4 :
906+ linkAttrKeys .add (readKeyValueKey (link .readBytes ().newCodedInput ()));
907+ break ;
761908 default :
762909 link .skipField (tag );
763910 }
@@ -766,6 +913,11 @@ private static void verifyLink(CodedInputStream link, String caseName) throws IO
766913 assertEquals (16 , traceId .length , "Link.trace_id must be 16 bytes [" + caseName + "]" );
767914 assertNotNull (spanId , "Link.span_id must be present [" + caseName + "]" );
768915 assertEquals (8 , spanId .length , "Link.span_id must be 8 bytes [" + caseName + "]" );
916+ for (String expectedKey : linkSpec .attributes .asMap ().keySet ()) {
917+ assertTrue (
918+ linkAttrKeys .contains (expectedKey ),
919+ "Link attributes must include '" + expectedKey + "' [" + caseName + "]" );
920+ }
769921 }
770922
771923 // ── proto parsing helpers ─────────────────────────────────────────────────
0 commit comments