-
Notifications
You must be signed in to change notification settings - Fork 347
Add APM test agent trace decoding #12089
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: bbujon/smoke-tests-decoded-spans
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,61 @@ | ||
| package datadog.trace.test.agent.decoder.json.raw; | ||
|
|
||
| import static java.util.Collections.emptyList; | ||
|
|
||
| import com.squareup.moshi.JsonAdapter; | ||
| import com.squareup.moshi.JsonDataException; | ||
| import com.squareup.moshi.Moshi; | ||
| import com.squareup.moshi.Types; | ||
| import datadog.trace.test.agent.decoder.DecodedMessage; | ||
| import datadog.trace.test.agent.decoder.DecodedSpan; | ||
| import datadog.trace.test.agent.decoder.DecodedTrace; | ||
| import java.io.IOException; | ||
| import java.lang.reflect.Type; | ||
| import java.util.ArrayList; | ||
| import java.util.List; | ||
|
|
||
| /** | ||
| * MessageJson decodes a JSON trace payload — a JSON array of traces, each a JSON array of spans in | ||
| * the v0.4 shape, as exposed by the dd-apm-test-agent — into the shared {@link DecodedMessage} | ||
| * model. Unlike the msgpack formats there is no message envelope, so the payload maps directly to | ||
| * the list of traces. | ||
| */ | ||
| public final class MessageJson implements DecodedMessage { | ||
| private static final Type LIST_OF_TRACES = | ||
| Types.newParameterizedType( | ||
| List.class, Types.newParameterizedType(List.class, SpanJson.class)); | ||
| private static final JsonAdapter<List<List<SpanJson>>> ADAPTER = | ||
| new Moshi.Builder().build().adapter(LIST_OF_TRACES); | ||
|
|
||
| private final List<DecodedTrace> traces; | ||
|
|
||
| private MessageJson(List<DecodedTrace> traces) { | ||
| this.traces = traces; | ||
| } | ||
|
|
||
| /** Decodes a JSON trace payload into a {@link MessageJson}. */ | ||
| public static MessageJson fromJson(String json) { | ||
| List<List<SpanJson>> rawTraces; | ||
| try { | ||
| // IOException covers malformed JSON (JsonEncodingException); JsonDataException (unchecked) | ||
| // covers a well-formed body of the wrong shape (e.g. an object where a trace array is | ||
| // expected). Wrap both so callers always get the offending body for diagnosis. | ||
| rawTraces = ADAPTER.fromJson(json); | ||
| } catch (IOException | JsonDataException e) { | ||
| throw new IllegalStateException("Failed to parse JSON traces: " + json, e); | ||
| } | ||
| List<DecodedTrace> traces = new ArrayList<>(); | ||
| if (rawTraces != null) { | ||
| for (List<SpanJson> spans : rawTraces) { | ||
| List<DecodedSpan> decodedSpans = spans == null ? emptyList() : new ArrayList<>(spans); | ||
| traces.add(new TraceJson(decodedSpans)); | ||
| } | ||
| } | ||
| return new MessageJson(traces); | ||
| } | ||
|
|
||
| @Override | ||
| public List<DecodedTrace> getTraces() { | ||
| return this.traces; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,145 @@ | ||
| package datadog.trace.test.agent.decoder.json.raw; | ||
|
|
||
| import static java.util.Collections.emptyMap; | ||
|
|
||
| import com.squareup.moshi.Json; | ||
| import datadog.trace.test.agent.decoder.DecodedSpan; | ||
| import java.util.HashMap; | ||
| import java.util.Map; | ||
|
|
||
| /** | ||
| * SpanJson decodes spans from the JSON trace format the dd-apm-test-agent exposes (e.g. from its | ||
| * {@code /test/traces} endpoint), which serializes spans in the standard v0.4 shape. Field names | ||
| * mirror that wire shape: service/name/resource/type, trace_id/span_id/parent_id, | ||
| * start/duration/error, meta, metrics, and meta_struct. | ||
| */ | ||
| public final class SpanJson implements DecodedSpan { | ||
| String service; | ||
| String name; | ||
| String resource; | ||
| String type; | ||
|
|
||
| // IDs are unsigned 64-bit; read as decimal strings and parsed with Long.parseUnsignedLong, since | ||
| // Moshi's long adapter rejects values above Long.MAX_VALUE (the agent emits them as JSON numbers, | ||
| // which Moshi coerces to their string form). | ||
| @Json(name = "trace_id") | ||
| String traceId; | ||
|
|
||
| @Json(name = "span_id") | ||
| String spanId; | ||
|
|
||
| @Json(name = "parent_id") | ||
| String parentId; | ||
|
|
||
| long start; | ||
| long duration; | ||
| int error; | ||
| Map<String, String> meta; | ||
|
|
||
| @Json(name = "meta_struct") | ||
| Map<String, Object> metaStruct; | ||
|
|
||
| Map<String, Double> metrics; | ||
|
|
||
| @Override | ||
| public String getService() { | ||
| return this.service; | ||
| } | ||
|
|
||
| @Override | ||
| public String getName() { | ||
| return this.name; | ||
| } | ||
|
|
||
| @Override | ||
| public String getResource() { | ||
| return this.resource; | ||
| } | ||
|
|
||
| @Override | ||
| public long getTraceId() { | ||
| return this.traceId == null ? 0L : Long.parseUnsignedLong(this.traceId); | ||
| } | ||
|
|
||
| @Override | ||
| public long getSpanId() { | ||
| return this.spanId == null ? 0L : Long.parseUnsignedLong(this.spanId); | ||
| } | ||
|
|
||
| @Override | ||
| public long getParentId() { | ||
| return this.parentId == null ? 0L : Long.parseUnsignedLong(this.parentId); | ||
| } | ||
|
|
||
| @Override | ||
| public long getStart() { | ||
| return this.start; | ||
| } | ||
|
|
||
| @Override | ||
| public long getDuration() { | ||
| return this.duration; | ||
| } | ||
|
|
||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Smoke tests inspecting AppSec or other structured span metadata will see the wrong type and can fail or silently miss the metadata they are intended to validate. Assertion details
Was this helpful? React 👍 or 👎
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| @Override | ||
| public int getError() { | ||
| return this.error; | ||
| } | ||
|
|
||
| @Override | ||
| public Map<String, String> getMeta() { | ||
| return this.meta == null ? emptyMap() : this.meta; | ||
| } | ||
|
|
||
| @Override | ||
| public Map<String, Object> getMetaStruct() { | ||
| return this.metaStruct == null ? emptyMap() : this.metaStruct; | ||
| } | ||
|
|
||
| @Override | ||
| public Map<String, Number> getMetrics() { | ||
| // Moshi deserializes JSON numbers as Double; expose them as the interface's Number type. | ||
| return this.metrics == null ? emptyMap() : new HashMap<>(this.metrics); | ||
| } | ||
|
|
||
| @Override | ||
| public String getType() { | ||
| return this.type; | ||
| } | ||
|
|
||
| @Override | ||
| public String toString() { | ||
| return "SpanJson{" | ||
| + "service='" | ||
| + this.service | ||
| + '\'' | ||
| + ", name='" | ||
| + this.name | ||
| + '\'' | ||
| + ", resource='" | ||
| + this.resource | ||
| + '\'' | ||
| + ", type='" | ||
| + this.type | ||
| + '\'' | ||
| + ", traceId=" | ||
| + this.traceId | ||
| + ", spanId=" | ||
| + this.spanId | ||
| + ", parentId=" | ||
| + this.parentId | ||
| + ", start=" | ||
| + this.start | ||
| + ", duration=" | ||
| + this.duration | ||
| + ", error=" | ||
| + this.error | ||
| + ", meta=" | ||
| + this.meta | ||
| + ", metaStruct=" | ||
| + this.metaStruct | ||
| + ", metrics=" | ||
| + this.metrics | ||
| + '}'; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| package datadog.trace.test.agent.decoder.json.raw; | ||
|
|
||
| import static java.util.Collections.unmodifiableList; | ||
|
|
||
| import datadog.trace.test.agent.decoder.DecodedSpan; | ||
| import datadog.trace.test.agent.decoder.DecodedTrace; | ||
| import java.util.List; | ||
|
|
||
| /** TraceJson is a single trace (an ordered list of {@link SpanJson}) from the JSON trace format. */ | ||
| public final class TraceJson implements DecodedTrace { | ||
| private final List<DecodedSpan> spans; | ||
|
|
||
| TraceJson(List<DecodedSpan> spans) { | ||
| this.spans = unmodifiableList(spans); | ||
| } | ||
|
|
||
| @Override | ||
| public List<DecodedSpan> getSpans() { | ||
| return this.spans; | ||
| } | ||
|
|
||
| @Override | ||
| public String toString() { | ||
| return this.spans.toString(); | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When a trace contains
meta_struct, each v0.4 entry is a MessagePack-encoded binary payload (seeTraceMapperV0_4.MetaStructWriter), which/test/tracesrepresents as a base64 JSON string. Deserializing directly intoMap<String, Object>therefore exposes strings instead of the structured objects returned by the existing v0.4 decoder, so AppSec/LLM smoke tests that inspect or cast these values will fail or evaluate the wrong data. Decode each base64 value and unpack its MessagePack payload; the current synthetic test should also use the wire representation rather than an already-decoded object.Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It does not apply here. From Claude: