Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
6680c2b
Fix JSON span event encoding in v1 payloads
AlexeyKuznetsov-DD Jul 29, 2026
ffe6112
Enable `v1` by default to run the tests on CI.
AlexeyKuznetsov-DD Jul 29, 2026
e875817
Merge branch 'master' into alexeyk/fix-v1-span-events-json-encoding-v…
AlexeyKuznetsov-DD Jul 30, 2026
0f351ca
Debug SSI system-tests
AlexeyKuznetsov-DD Jul 30, 2026
707372a
Debug SSI system-tests
AlexeyKuznetsov-DD Jul 30, 2026
f707cb8
Debug SSI system-tests: actual fix
AlexeyKuznetsov-DD Jul 30, 2026
7d2c4af
Merge branch 'master' into alexeyk/fix-v1-span-events-json-encoding
AlexeyKuznetsov-DD Jul 30, 2026
4ffbbd0
Merge branch 'alexeyk/fix-v1-span-events-json-encoding' into alexeyk/…
AlexeyKuznetsov-DD Jul 30, 2026
912f05b
Merge branch 'master' into alexeyk/fix-v1-span-events-json-encoding-v…
AlexeyKuznetsov-DD Jul 31, 2026
7760dee
Test with systemt-test from master.
AlexeyKuznetsov-DD Jul 31, 2026
314a2c6
Test the latest fix in system-tests.
AlexeyKuznetsov-DD Aug 1, 2026
ed1e432
Test the latest fix in system-tests.
AlexeyKuznetsov-DD Aug 1, 2026
66fadfb
Merge branch 'master' into alexeyk/fix-v1-span-events-json-encoding-v…
AlexeyKuznetsov-DD Aug 3, 2026
a69117d
Test v1 on master.
AlexeyKuznetsov-DD Aug 3, 2026
26e1302
Merge branch 'master' into alexeyk/fix-v1-span-events-json-encoding-v…
AlexeyKuznetsov-DD Aug 4, 2026
445fb36
Merge branch 'master' into alexeyk/fix-v1-span-events-json-encoding-v…
AlexeyKuznetsov-DD Aug 4, 2026
367fa34
Merge branch 'master' into alexeyk/fix-v1-span-events-json-encoding-v…
AlexeyKuznetsov-DD Aug 7, 2026
753c24e
Merge branch 'master' into alexeyk/fix-v1-span-events-json-encoding-v…
AlexeyKuznetsov-DD Aug 10, 2026
8bd717e
Merge branch 'master' into alexeyk/fix-v1-span-events-json-encoding-v…
AlexeyKuznetsov-DD Aug 12, 2026
1c88331
Merge branch 'master' into alexeyk/fix-v1-span-events-json-encoding-v…
AlexeyKuznetsov-DD Aug 17, 2026
0e4447c
Merge branch 'master' into alexeyk/fix-v1-span-events-json-encoding-v…
AlexeyKuznetsov-DD Aug 31, 2026
287648c
Merge branch 'master' into alexeyk/fix-v1-span-events-json-encoding-v…
AlexeyKuznetsov-DD Sep 4, 2026
0793a4e
Merge branch 'master' into alexeyk/fix-v1-span-events-json-encoding-v…
AlexeyKuznetsov-DD Sep 9, 2026
59f75c5
Merge branch 'master' into alexeyk/fix-v1-span-events-json-encoding-v…
AlexeyKuznetsov-DD Sep 16, 2026
8db3d9d
Merge branch 'master' into alexeyk/fix-v1-span-events-json-encoding-v…
AlexeyKuznetsov-DD Sep 18, 2026
5eb2359
Merge branch 'master' into alexeyk/fix-v1-span-events-json-encoding-v…
AlexeyKuznetsov-DD Sep 25, 2026
4c85fa0
Merge branch 'master' into alexeyk/fix-v1-span-events-json-encoding-v…
AlexeyKuznetsov-DD Oct 5, 2026
9132527
Merge branch 'master' into alexeyk/fix-v1-span-events-json-encoding-v…
AlexeyKuznetsov-DD Oct 7, 2026
b67375c
Merge branch 'master' into alexeyk/fix-v1-span-events-json-encoding-v…
AlexeyKuznetsov-DD Oct 9, 2026
dac6e8d
Merge branch 'master' into alexeyk/fix-v1-span-events-json-encoding-v…
AlexeyKuznetsov-DD Oct 13, 2026
5b0b88c
Merge branch 'master' into alexeyk/fix-v1-span-events-json-encoding-v…
AlexeyKuznetsov-DD Oct 14, 2026
aabb490
Preserve v1 span events in test-agent JSON decoding.
AlexeyKuznetsov-DD Oct 14, 2026
c4326c9
Merge branch 'master' into alexeyk/fix-v1-span-events-json-encoding-v…
AlexeyKuznetsov-DD Oct 14, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import static datadog.communication.http.OkHttpUtils.msgpackRequestBodyOf;
import static datadog.communication.http.OkHttpUtils.prepareRequest;
import static datadog.communication.serialization.msgpack.MsgPackWriter.FIXARRAY;
import static datadog.trace.api.ProtocolVersion.V0_4;
import static datadog.trace.api.ProtocolVersion.V1_0;
import static java.util.Collections.emptyMap;
import static java.util.Collections.emptySet;
import static java.util.Collections.singletonList;
Expand Down Expand Up @@ -118,7 +118,7 @@ public DDAgentFeaturesDiscovery(
this.agentBaseUrl = agentUrl;
this.metricsEnabled = metricsEnabled;
this.ignoreAgentVersionForStats = ignoreAgentVersionForStats;
this.protocolVersion = protocolVersion != null ? protocolVersion : V0_4;
this.protocolVersion = protocolVersion != null ? protocolVersion : V1_0;
this.discoveryTimer = monitoring.newTimer("trace.agent.discovery.time");
this.discoveryState = new State();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ class DDAgentFeaturesDiscoveryTest extends DDSpecification {
V1_0 | V1_ENDPOINT
}

def "null protocol version falls back to v0.4 trace endpoints"() {
def "null protocol version falls back to v1.0 trace endpoints"() {
setup:
OkHttpClient client = Mock(OkHttpClient)
DDAgentFeaturesDiscovery features =
Expand All @@ -98,9 +98,10 @@ class DDAgentFeaturesDiscoveryTest extends DDSpecification {

then:
1 * client.newCall({ Request request -> request.url().toString() == "http://localhost:8125/info" }) >> { Request request -> infoResponse(request, "{}") }
1 * client.newCall({ Request request -> request.url().toString() == "http://localhost:8125/v1.0/traces" }) >> { Request request -> success(request) }
0 * client.newCall({ Request request -> request.url().toString() == "http://localhost:8125/v0.5/traces" }) >> { Request request -> success(request) }
1 * client.newCall({ Request request -> request.url().toString() == "http://localhost:8125/v0.4/traces" }) >> { Request request -> success(request) }
features.getTraceEndpoint() == V04_ENDPOINT
0 * client.newCall({ Request request -> request.url().toString() == "http://localhost:8125/v0.4/traces" }) >> { Request request -> success(request) }
features.getTraceEndpoint() == V1_ENDPOINT
0 * _
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ public final class ConfigDefaults {
Arrays.asList("user.id", "session.id", "account.id");
static final boolean DEFAULT_JMX_FETCH_ENABLED = true;

static final String DEFAULT_TRACE_AGENT_PROTOCOL_VERSION = ProtocolVersion.V0_4.asConfigValue();
static final String DEFAULT_TRACE_AGENT_PROTOCOL_VERSION = ProtocolVersion.V1_0.asConfigValue();

static final boolean DEFAULT_CLIENT_IP_ENABLED = false;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
package datadog.trace.test.agent.decoder.json.raw;

import static java.lang.annotation.RetentionPolicy.RUNTIME;
import static java.util.Collections.unmodifiableList;

import com.squareup.moshi.FromJson;
import com.squareup.moshi.JsonAdapter;
import com.squareup.moshi.JsonDataException;
import com.squareup.moshi.JsonQualifier;
import com.squareup.moshi.JsonReader;
import com.squareup.moshi.JsonWriter;
import com.squareup.moshi.Moshi;
Expand All @@ -14,12 +16,15 @@
import datadog.trace.test.agent.decoder.DecodedSpan;
import datadog.trace.test.agent.decoder.DecodedTrace;
import java.io.IOException;
import java.lang.annotation.Retention;
import java.lang.reflect.Type;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import okio.Buffer;
import okio.BufferedSink;

/**
* MessageJson decodes a JSON trace payload — a JSON array of traces, each a JSON array of spans in
Expand All @@ -35,6 +40,7 @@ public final class MessageJson implements DecodedMessage {
new Moshi.Builder()
.add(new MetricNumberAdapter())
.add(new MetaStructObjectAdapter())
.add(new RawJsonAdapter())
.build()
.adapter(LIST_OF_TRACES);

Expand Down Expand Up @@ -74,6 +80,7 @@ public static MessageJson fromJson(String json) {
+ span);
}
span.resolveLinks();
span.resolveEvents();
decodedSpans.add(span);
}
traces.add(new TraceJson(decodedSpans));
Expand Down Expand Up @@ -188,6 +195,86 @@ private static Number parseNumber(String literal) {
}
}

/** Marks a {@code String} field that holds a JSON value as compact JSON text. */
@Retention(RUNTIME)
@JsonQualifier
@interface RawJson {}

/**
* Decodes a {@link RawJson} field by copying the JSON value token by token into compact JSON
* text. Number literals are copied verbatim rather than materialized as {@code Double}, so values
* above 2^53 or beyond {@code long} range, and fractional values, keep their exact textual form.
* A JSON {@code null} decodes to a {@code null} field.
*/
static final class RawJsonAdapter {
@FromJson
@RawJson
String fromJson(JsonReader reader) throws IOException {
if (reader.peek() == JsonReader.Token.NULL) {
return reader.nextNull();
}
Buffer buffer = new Buffer();
JsonWriter writer = JsonWriter.of(buffer);
// Keep null members ("key":null) rather than dropping them.
writer.setSerializeNulls(true);
copyValue(reader, writer);
writer.flush();
return buffer.readUtf8();
}

@ToJson
void toJson(JsonWriter writer, @RawJson String value) throws IOException {
if (value == null) {
writer.nullValue();
return;
}
try (BufferedSink sink = writer.valueSink()) {
sink.writeUtf8(value);
}
}

private static void copyValue(JsonReader reader, JsonWriter writer) throws IOException {
switch (reader.peek()) {
case BEGIN_OBJECT:
reader.beginObject();
writer.beginObject();
while (reader.hasNext()) {
writer.name(reader.nextName());
copyValue(reader, writer);
}
reader.endObject();
writer.endObject();
break;
case BEGIN_ARRAY:
reader.beginArray();
writer.beginArray();
while (reader.hasNext()) {
copyValue(reader, writer);
}
reader.endArray();
writer.endArray();
break;
case STRING:
writer.value(reader.nextString());
break;
case NUMBER:
try (BufferedSink sink = writer.valueSink()) {
sink.writeUtf8(reader.nextString());
}
break;
case BOOLEAN:
writer.value(reader.nextBoolean());
break;
case NULL:
reader.nextNull();
writer.nullValue();
break;
default:
throw new JsonDataException("Unexpected JSON token: " + reader.peek());
}
}
}

@Override
public List<DecodedTrace> getTraces() {
return this.traces;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import datadog.trace.test.agent.decoder.DecodedSpanLink;
import datadog.trace.test.agent.decoder.DecodedSpanLinks;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

Expand Down Expand Up @@ -53,6 +54,12 @@ public final class SpanJson implements DecodedSpan {
@Json(name = "span_links")
List<SpanLinkJson> spanLinks;

// Only present in V1 payload. Kept as compact JSON text (number literals verbatim, see
// MessageJson.RawJsonAdapter) and exposed as the events meta tag by #resolveEvents
@Json(name = "span_events")
@MessageJson.RawJson
String spanEvents;

// Decoded from span links meta tag or #spanLinks
// transient so Moshi does not try to decode it
transient List<DecodedSpanLink> links;
Expand Down Expand Up @@ -146,6 +153,21 @@ void resolveLinks() {
this.links = unmodifiableList(decoded);
}

/**
* Exposes the {@code span_events} field, when the agent filled it (the tracer submitted a {@code
* v1.0} payload), as the {@code events} meta tag, replacing any such tag. The value is the
* agent's JSON re-emitted compactly; its member order and attribute shape are the agent's, not
* those of the tag the tracer writes for older protocols.
*/
void resolveEvents() {
if (this.spanEvents != null) {
if (this.meta == null) {
this.meta = new HashMap<>();
}
this.meta.put("events", this.spanEvents);
}
}

@Override
public String toString() {
return "SpanJson{"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,59 @@ class JsonDecoderTest {
// highlighting).
private static final String TWO_TRACES = loadJson("/two-traces.json");

@Test
void exposesNativeSpanEventsWithNumberLiteralsVerbatim() {
String events =
"[{\"name\":\"configuration-loaded\",\"time_unix_nano\":1791902357478000001,"
+ "\"attributes\":{\"source\":\"file\",\"count\":3,\"cached\":false,"
+ "\"ratio\":0.123456789012,\"huge\":18446744073709551616,\"missing\":null,"
+ "\"list\":[1,2.5,\"three\"]}}]";
DecodedSpan span = decodeSpanWithEvents("\"span_events\":" + events);

assertEquals(events, span.getMeta().get("events"));
}

@Test
void compactsNativeSpanEvents() {
DecodedSpan span =
decodeSpanWithEvents(
"\"span_events\": [ {\"name\": \"a \\\"quoted\\\" name\", \"time_unix_nano\": 1e3} ]");

assertEquals(
"[{\"name\":\"a \\\"quoted\\\" name\",\"time_unix_nano\":1e3}]",
span.getMeta().get("events"));
}

@Test
void preservesLegacySpanEventsTag() {
String events = "[{\"name\":\"configuration-loaded\"}]";
DecodedSpan span =
decodeSpanWithEvents(
"\"meta\":{\"events\":\"[{\\\"name\\\":\\\"configuration-loaded\\\"}]\"}");

assertEquals(events, span.getMeta().get("events"));
}

@Test
void nativeSpanEventsTakePrecedenceAndEmptyEventsStayEmpty() {
DecodedSpan span = decodeSpanWithEvents("\"meta\":{\"events\":\"legacy\"},\"span_events\":[]");

assertEquals("[]", span.getMeta().get("events"));
assertNull(decodeSpanWithEvents("\"span_events\":null").getMeta().get("events"));
assertNull(decodeSpanWithEvents("\"meta\":{}").getMeta().get("events"));
}

private static DecodedSpan decodeSpanWithEvents(String fields) {
return Decoder.decodeJson(
"[[{\"name\":\"test\",\"trace_id\":1,\"span_id\":2,\"start\":3,\"duration\":4,"
+ fields
+ "}]]")
.getTraces()
.get(0)
.getSpans()
.get(0);
}

@Test
void decodesTraceAndSpanStructure() {
List<DecodedTrace> traces = Decoder.decodeJson(TWO_TRACES).getTraces();
Expand Down
Loading