Skip to content

Commit fa90299

Browse files
committed
feat(test-agent): Add APM test agent trace decoding
1 parent d1a2872 commit fa90299

7 files changed

Lines changed: 507 additions & 1 deletion

File tree

utils/test-agent-utils/decoder/build.gradle.kts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,12 @@ extra["excludedClassesCoverage"] = listOf(
88
"datadog.trace.test.agent.decoder.v04.raw.*",
99
"datadog.trace.test.agent.decoder.v05.raw.*",
1010
"datadog.trace.test.agent.decoder.v1.raw.*",
11+
"datadog.trace.test.agent.decoder.json.raw.*",
1112
)
1213

1314
dependencies {
1415
implementation(group = "org.msgpack", name = "msgpack-core", version = "0.8.24")
16+
implementation(libs.moshi)
1517

1618
testImplementation(libs.bundles.junit5)
1719
testImplementation(project(":utils:test-utils"))

utils/test-agent-utils/decoder/gradle.lockfile

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ com.github.stephenc.jcip:jcip-annotations:1.0-1=spotbugs
1212
com.google.code.findbugs:jsr305:3.0.2=compileClasspath,spotbugs,testCompileClasspath,testRuntimeClasspath
1313
com.google.code.gson:gson:2.13.2=spotbugs
1414
com.google.errorprone:error_prone_annotations:2.41.0=spotbugs
15+
com.squareup.moshi:moshi:1.11.0=compileClasspath,testCompileClasspath,testRuntimeClasspath
16+
com.squareup.okio:okio:1.17.5=compileClasspath,testCompileClasspath,testRuntimeClasspath
1517
com.thoughtworks.qdox:qdox:1.12.1=codenarc
1618
commons-fileupload:commons-fileupload:1.5=testCompileClasspath,testRuntimeClasspath
1719
commons-io:commons-io:2.11.0=testCompileClasspath,testRuntimeClasspath
@@ -29,7 +31,7 @@ org.apache.commons:commons-lang3:3.19.0=spotbugs
2931
org.apache.commons:commons-text:1.14.0=spotbugs
3032
org.apache.logging.log4j:log4j-api:2.25.2=spotbugs
3133
org.apache.logging.log4j:log4j-core:2.25.2=spotbugs
32-
org.apiguardian:apiguardian-api:1.1.2=testCompileClasspath
34+
org.apiguardian:apiguardian-api:1.1.2=testCompileClasspath,testRuntimeClasspath
3335
org.codehaus.groovy:groovy-ant:3.0.23=codenarc
3436
org.codehaus.groovy:groovy-docgenerator:3.0.23=codenarc
3537
org.codehaus.groovy:groovy-groovydoc:3.0.23=codenarc

utils/test-agent-utils/decoder/src/main/java/datadog/trace/test/agent/decoder/Decoder.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package datadog.trace.test.agent.decoder;
22

3+
import datadog.trace.test.agent.decoder.json.raw.MessageJson;
34
import datadog.trace.test.agent.decoder.v04.raw.MessageV04;
45
import datadog.trace.test.agent.decoder.v05.raw.MessageV05;
56
import datadog.trace.test.agent.decoder.v1.raw.MessageV1;
@@ -12,6 +13,11 @@ public static DecodedMessage decodeV1(byte[] buffer) {
1213
return MessageV1.unpack(buffer);
1314
}
1415

16+
/** Decodes the JSON trace format exposed by the dd-apm-test-agent. */
17+
public static DecodedMessage decodeJson(String json) {
18+
return MessageJson.fromJson(json);
19+
}
20+
1521
public static DecodedMessage decodeV05(byte[] buffer) {
1622
return MessageV05.unpack(buffer);
1723
}
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
package datadog.trace.test.agent.decoder.json.raw;
2+
3+
import static java.util.Collections.emptyList;
4+
5+
import com.squareup.moshi.FromJson;
6+
import com.squareup.moshi.JsonAdapter;
7+
import com.squareup.moshi.JsonDataException;
8+
import com.squareup.moshi.JsonReader;
9+
import com.squareup.moshi.JsonWriter;
10+
import com.squareup.moshi.Moshi;
11+
import com.squareup.moshi.ToJson;
12+
import com.squareup.moshi.Types;
13+
import datadog.trace.test.agent.decoder.DecodedMessage;
14+
import datadog.trace.test.agent.decoder.DecodedSpan;
15+
import datadog.trace.test.agent.decoder.DecodedTrace;
16+
import java.io.IOException;
17+
import java.lang.reflect.Type;
18+
import java.util.ArrayList;
19+
import java.util.List;
20+
21+
/**
22+
* MessageJson decodes a JSON trace payload — a JSON array of traces, each a JSON array of spans in
23+
* the v0.4 shape, as exposed by the dd-apm-test-agent — into the shared {@link DecodedMessage}
24+
* model. Unlike the msgpack formats there is no message envelope, so the payload maps directly to
25+
* the list of traces.
26+
*/
27+
public final class MessageJson implements DecodedMessage {
28+
private static final Type LIST_OF_TRACES =
29+
Types.newParameterizedType(
30+
List.class, Types.newParameterizedType(List.class, SpanJson.class));
31+
private static final JsonAdapter<List<List<SpanJson>>> ADAPTER =
32+
new Moshi.Builder().add(new MetricNumberAdapter()).build().adapter(LIST_OF_TRACES);
33+
34+
private final List<DecodedTrace> traces;
35+
36+
private MessageJson(List<DecodedTrace> traces) {
37+
this.traces = traces;
38+
}
39+
40+
/** Decodes a JSON trace payload into a {@link MessageJson}. */
41+
public static MessageJson fromJson(String json) {
42+
List<List<SpanJson>> rawTraces;
43+
try {
44+
// IOException covers malformed JSON (JsonEncodingException); JsonDataException (unchecked)
45+
// covers a well-formed body of the wrong shape (e.g. an object where a trace array is
46+
// expected). Wrap both so callers always get the offending body for diagnosis.
47+
rawTraces = ADAPTER.fromJson(json);
48+
} catch (IOException | JsonDataException e) {
49+
throw new IllegalStateException("Failed to parse JSON traces: " + json, e);
50+
}
51+
List<DecodedTrace> traces = new ArrayList<>();
52+
if (rawTraces != null) {
53+
for (List<SpanJson> spans : rawTraces) {
54+
List<DecodedSpan> decodedSpans;
55+
if (spans == null) {
56+
decodedSpans = emptyList();
57+
} else {
58+
decodedSpans = new ArrayList<>(spans.size());
59+
for (SpanJson span : spans) {
60+
if (span == null) {
61+
throw new IllegalStateException("Malformed JSON trace with a null span: " + json);
62+
}
63+
if (span.traceId == null || span.spanId == null) {
64+
throw new IllegalStateException(
65+
"JSON span missing required identifier (trace_id and span_id): " + span);
66+
}
67+
decodedSpans.add(span);
68+
}
69+
}
70+
traces.add(new TraceJson(decodedSpans));
71+
}
72+
}
73+
return new MessageJson(traces);
74+
}
75+
76+
/**
77+
* Decodes the numeric values of the {@code metrics} map. Moshi coerces every JSON number to
78+
* {@code Double}; this adapter instead reads the number from its literal form and preserves the
79+
* integral vs. fractional distinction the msgpack decoders produce (see {@code
80+
* SpanV04.unpackNumber}): integral values become {@code Integer} (or {@code Long} when they
81+
* overflow {@code int}), fractional values become {@code Double}. Reading the literal rather than
82+
* coercing through {@code double} also keeps integral values above 2^53 exact.
83+
*/
84+
static final class MetricNumberAdapter {
85+
@FromJson
86+
Number fromJson(JsonReader reader) throws IOException {
87+
String literal = reader.nextString();
88+
boolean fractional =
89+
literal.indexOf('.') >= 0 || literal.indexOf('e') >= 0 || literal.indexOf('E') >= 0;
90+
if (!fractional) {
91+
try {
92+
return Integer.valueOf(literal);
93+
} catch (NumberFormatException overflowsInt) {
94+
try {
95+
return Long.valueOf(literal);
96+
} catch (NumberFormatException overflowsLong) {
97+
// Integral magnitude beyond long range: fall through to Double.
98+
}
99+
}
100+
}
101+
return Double.valueOf(literal);
102+
}
103+
104+
@ToJson
105+
void toJson(JsonWriter writer, Number value) throws IOException {
106+
writer.value(value);
107+
}
108+
}
109+
110+
@Override
111+
public List<DecodedTrace> getTraces() {
112+
return this.traces;
113+
}
114+
}
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
package datadog.trace.test.agent.decoder.json.raw;
2+
3+
import static java.util.Collections.emptyMap;
4+
5+
import com.squareup.moshi.Json;
6+
import datadog.trace.test.agent.decoder.DecodedSpan;
7+
import java.util.HashMap;
8+
import java.util.Map;
9+
10+
/**
11+
* SpanJson decodes spans from the JSON trace format the dd-apm-test-agent exposes (e.g. from its
12+
* {@code /test/traces} endpoint), which serializes spans in the standard v0.4 shape. Field names
13+
* mirror that wire shape: service/name/resource/type, trace_id/span_id/parent_id,
14+
* start/duration/error, meta, metrics, and meta_struct.
15+
*/
16+
public final class SpanJson implements DecodedSpan {
17+
String service;
18+
String name;
19+
String resource;
20+
String type;
21+
22+
// IDs are unsigned 64-bit; read as decimal strings and parsed with Long.parseUnsignedLong, since
23+
// Moshi's long adapter rejects values above Long.MAX_VALUE (the agent emits them as JSON numbers,
24+
// which Moshi coerces to their string form).
25+
@Json(name = "trace_id")
26+
String traceId;
27+
28+
@Json(name = "span_id")
29+
String spanId;
30+
31+
@Json(name = "parent_id")
32+
String parentId;
33+
34+
long start;
35+
long duration;
36+
int error;
37+
Map<String, String> meta;
38+
39+
@Json(name = "meta_struct")
40+
Map<String, Object> metaStruct;
41+
42+
Map<String, Number> metrics;
43+
44+
@Override
45+
public String getService() {
46+
return this.service;
47+
}
48+
49+
@Override
50+
public String getName() {
51+
return this.name;
52+
}
53+
54+
@Override
55+
public String getResource() {
56+
return this.resource;
57+
}
58+
59+
@Override
60+
public long getTraceId() {
61+
return this.traceId == null ? 0L : Long.parseUnsignedLong(this.traceId);
62+
}
63+
64+
@Override
65+
public long getSpanId() {
66+
return this.spanId == null ? 0L : Long.parseUnsignedLong(this.spanId);
67+
}
68+
69+
@Override
70+
public long getParentId() {
71+
return this.parentId == null ? 0L : Long.parseUnsignedLong(this.parentId);
72+
}
73+
74+
@Override
75+
public long getStart() {
76+
return this.start;
77+
}
78+
79+
@Override
80+
public long getDuration() {
81+
return this.duration;
82+
}
83+
84+
@Override
85+
public int getError() {
86+
return this.error;
87+
}
88+
89+
@Override
90+
public Map<String, String> getMeta() {
91+
return this.meta == null ? emptyMap() : this.meta;
92+
}
93+
94+
@Override
95+
public Map<String, Object> getMetaStruct() {
96+
return this.metaStruct == null ? emptyMap() : this.metaStruct;
97+
}
98+
99+
@Override
100+
public Map<String, Number> getMetrics() {
101+
return this.metrics == null ? emptyMap() : new HashMap<>(this.metrics);
102+
}
103+
104+
@Override
105+
public String getType() {
106+
return this.type;
107+
}
108+
109+
@Override
110+
public String toString() {
111+
return "SpanJson{"
112+
+ "service='"
113+
+ this.service
114+
+ '\''
115+
+ ", name='"
116+
+ this.name
117+
+ '\''
118+
+ ", resource='"
119+
+ this.resource
120+
+ '\''
121+
+ ", type='"
122+
+ this.type
123+
+ '\''
124+
+ ", traceId="
125+
+ this.traceId
126+
+ ", spanId="
127+
+ this.spanId
128+
+ ", parentId="
129+
+ this.parentId
130+
+ ", start="
131+
+ this.start
132+
+ ", duration="
133+
+ this.duration
134+
+ ", error="
135+
+ this.error
136+
+ ", meta="
137+
+ this.meta
138+
+ ", metaStruct="
139+
+ this.metaStruct
140+
+ ", metrics="
141+
+ this.metrics
142+
+ '}';
143+
}
144+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package datadog.trace.test.agent.decoder.json.raw;
2+
3+
import static java.util.Collections.unmodifiableList;
4+
5+
import datadog.trace.test.agent.decoder.DecodedSpan;
6+
import datadog.trace.test.agent.decoder.DecodedTrace;
7+
import java.util.List;
8+
9+
/** TraceJson is a single trace (an ordered list of {@link SpanJson}) from the JSON trace format. */
10+
public final class TraceJson implements DecodedTrace {
11+
private final List<DecodedSpan> spans;
12+
13+
TraceJson(List<DecodedSpan> spans) {
14+
this.spans = unmodifiableList(spans);
15+
}
16+
17+
@Override
18+
public List<DecodedSpan> getSpans() {
19+
return this.spans;
20+
}
21+
22+
@Override
23+
public String toString() {
24+
return this.spans.toString();
25+
}
26+
}

0 commit comments

Comments
 (0)