Skip to content

Commit d14912e

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

7 files changed

Lines changed: 410 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: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
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, Double> 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+
// Moshi deserializes JSON numbers as Double; expose them as the interface's Number type.
102+
return this.metrics == null ? emptyMap() : new HashMap<>(this.metrics);
103+
}
104+
105+
@Override
106+
public String getType() {
107+
return this.type;
108+
}
109+
110+
@Override
111+
public String toString() {
112+
return "SpanJson{"
113+
+ "service='"
114+
+ this.service
115+
+ '\''
116+
+ ", name='"
117+
+ this.name
118+
+ '\''
119+
+ ", resource='"
120+
+ this.resource
121+
+ '\''
122+
+ ", type='"
123+
+ this.type
124+
+ '\''
125+
+ ", traceId="
126+
+ this.traceId
127+
+ ", spanId="
128+
+ this.spanId
129+
+ ", parentId="
130+
+ this.parentId
131+
+ ", start="
132+
+ this.start
133+
+ ", duration="
134+
+ this.duration
135+
+ ", error="
136+
+ this.error
137+
+ ", meta="
138+
+ this.meta
139+
+ ", metaStruct="
140+
+ this.metaStruct
141+
+ ", metrics="
142+
+ this.metrics
143+
+ '}';
144+
}
145+
}
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)