Skip to content

Commit 8f198b7

Browse files
committed
Extend test to check 128 trace-ids and span link attributes
1 parent a731c48 commit 8f198b7

File tree

1 file changed

+172
-20
lines changed

1 file changed

+172
-20
lines changed

dd-trace-core/src/test/java/datadog/trace/core/otlp/trace/OtlpTraceProtoTest.java

Lines changed: 172 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,25 @@
99
import static java.util.Collections.emptyList;
1010
import static org.junit.jupiter.api.Assertions.assertEquals;
1111
import static org.junit.jupiter.api.Assertions.assertFalse;
12+
import static org.junit.jupiter.api.Assertions.assertNotEquals;
1213
import static org.junit.jupiter.api.Assertions.assertNotNull;
14+
import static org.junit.jupiter.api.Assertions.assertNull;
1315
import static org.junit.jupiter.api.Assertions.assertTrue;
1416

1517
import com.google.protobuf.CodedInputStream;
1618
import com.google.protobuf.WireFormat;
19+
import datadog.trace.api.DD128bTraceId;
20+
import datadog.trace.api.TracePropagationStyle;
1721
import datadog.trace.api.sampling.PrioritySampling;
1822
import datadog.trace.api.sampling.SamplingMechanism;
1923
import datadog.trace.bootstrap.instrumentation.api.AgentSpan;
24+
import datadog.trace.bootstrap.instrumentation.api.SpanAttributes;
2025
import datadog.trace.bootstrap.instrumentation.api.SpanLink;
2126
import datadog.trace.core.CoreTracer;
2227
import datadog.trace.core.DDSpan;
2328
import datadog.trace.core.otlp.common.OtlpPayload;
29+
import datadog.trace.core.propagation.ExtractedContext;
30+
import datadog.trace.core.propagation.PropagationTags;
2431
import java.io.ByteArrayOutputStream;
2532
import java.io.IOException;
2633
import 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

Comments
 (0)