Skip to content

Commit 0bfd4d7

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

7 files changed

Lines changed: 386 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: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package datadog.trace.test.agent.decoder.json.raw;
2+
3+
import static java.util.Collections.emptyList;
4+
5+
import com.squareup.moshi.JsonAdapter;
6+
import com.squareup.moshi.JsonDataException;
7+
import com.squareup.moshi.Moshi;
8+
import com.squareup.moshi.Types;
9+
import datadog.trace.test.agent.decoder.DecodedMessage;
10+
import datadog.trace.test.agent.decoder.DecodedSpan;
11+
import datadog.trace.test.agent.decoder.DecodedTrace;
12+
import java.io.IOException;
13+
import java.lang.reflect.Type;
14+
import java.util.ArrayList;
15+
import java.util.List;
16+
17+
/**
18+
* MessageJson decodes a JSON trace payload — a JSON array of traces, each a JSON array of spans in
19+
* the v0.4 shape, as exposed by the dd-apm-test-agent — into the shared {@link DecodedMessage}
20+
* model. Unlike the msgpack formats there is no message envelope, so the payload maps directly to
21+
* the list of traces.
22+
*/
23+
public final class MessageJson implements DecodedMessage {
24+
private static final Type LIST_OF_TRACES =
25+
Types.newParameterizedType(
26+
List.class, Types.newParameterizedType(List.class, SpanJson.class));
27+
private static final JsonAdapter<List<List<SpanJson>>> ADAPTER =
28+
new Moshi.Builder().build().adapter(LIST_OF_TRACES);
29+
30+
private final List<DecodedTrace> traces;
31+
32+
private MessageJson(List<DecodedTrace> traces) {
33+
this.traces = traces;
34+
}
35+
36+
/** Decodes a JSON trace payload into a {@link MessageJson}. */
37+
public static MessageJson fromJson(String json) {
38+
List<List<SpanJson>> rawTraces;
39+
try {
40+
// IOException covers malformed JSON (JsonEncodingException); JsonDataException (unchecked)
41+
// covers a well-formed body of the wrong shape (e.g. an object where a trace array is
42+
// expected). Wrap both so callers always get the offending body for diagnosis.
43+
rawTraces = ADAPTER.fromJson(json);
44+
} catch (IOException | JsonDataException e) {
45+
throw new IllegalStateException("Failed to parse JSON traces: " + json, e);
46+
}
47+
List<DecodedTrace> traces = new ArrayList<>();
48+
if (rawTraces != null) {
49+
for (List<SpanJson> spans : rawTraces) {
50+
List<DecodedSpan> decodedSpans = spans == null ? emptyList() : new ArrayList<>(spans);
51+
traces.add(new TraceJson(decodedSpans));
52+
}
53+
}
54+
return new MessageJson(traces);
55+
}
56+
57+
@Override
58+
public List<DecodedTrace> getTraces() {
59+
return this.traces;
60+
}
61+
}
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
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+
@Json(name = "trace_id")
23+
long traceId;
24+
25+
@Json(name = "span_id")
26+
long spanId;
27+
28+
@Json(name = "parent_id")
29+
long parentId;
30+
31+
long start;
32+
long duration;
33+
int error;
34+
Map<String, String> meta;
35+
36+
@Json(name = "meta_struct")
37+
Map<String, Object> metaStruct;
38+
39+
Map<String, Double> metrics;
40+
41+
@Override
42+
public String getService() {
43+
return this.service;
44+
}
45+
46+
@Override
47+
public String getName() {
48+
return this.name;
49+
}
50+
51+
@Override
52+
public String getResource() {
53+
return this.resource;
54+
}
55+
56+
@Override
57+
public long getTraceId() {
58+
return this.traceId;
59+
}
60+
61+
@Override
62+
public long getSpanId() {
63+
return this.spanId;
64+
}
65+
66+
@Override
67+
public long getParentId() {
68+
return this.parentId;
69+
}
70+
71+
@Override
72+
public long getStart() {
73+
return this.start;
74+
}
75+
76+
@Override
77+
public long getDuration() {
78+
return this.duration;
79+
}
80+
81+
@Override
82+
public int getError() {
83+
return this.error;
84+
}
85+
86+
@Override
87+
public Map<String, String> getMeta() {
88+
return this.meta == null ? emptyMap() : this.meta;
89+
}
90+
91+
@Override
92+
public Map<String, Object> getMetaStruct() {
93+
return this.metaStruct == null ? emptyMap() : this.metaStruct;
94+
}
95+
96+
@Override
97+
public Map<String, Number> getMetrics() {
98+
// Moshi deserializes JSON numbers as Double; expose them as the interface's Number type.
99+
return this.metrics == null ? emptyMap() : new HashMap<>(this.metrics);
100+
}
101+
102+
@Override
103+
public String getType() {
104+
return this.type;
105+
}
106+
107+
@Override
108+
public String toString() {
109+
return "SpanJson{"
110+
+ "service='"
111+
+ this.service
112+
+ '\''
113+
+ ", name='"
114+
+ this.name
115+
+ '\''
116+
+ ", resource='"
117+
+ this.resource
118+
+ '\''
119+
+ ", type='"
120+
+ this.type
121+
+ '\''
122+
+ ", traceId="
123+
+ this.traceId
124+
+ ", spanId="
125+
+ this.spanId
126+
+ ", parentId="
127+
+ this.parentId
128+
+ ", start="
129+
+ this.start
130+
+ ", duration="
131+
+ this.duration
132+
+ ", error="
133+
+ this.error
134+
+ ", meta="
135+
+ this.meta
136+
+ ", metaStruct="
137+
+ this.metaStruct
138+
+ ", metrics="
139+
+ this.metrics
140+
+ '}';
141+
}
142+
}
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)