Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 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
c385b63
Decode v1 span events in the msgpack test-agent decoder.
AlexeyKuznetsov-DD Oct 14, 2026
a2e239d
Merge branch 'master' into alexeyk/fix-v1-span-events-json-encoding-v…
AlexeyKuznetsov-DD Oct 19, 2026
cc5be54
Merge branch 'master' into alexeyk/fix-v1-span-events-json-encoding-v…
AlexeyKuznetsov-DD Oct 20, 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 @@ -120,7 +120,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 @@ -5,14 +5,17 @@
import static java.util.Collections.unmodifiableList;
import static java.util.Collections.unmodifiableMap;

import com.squareup.moshi.JsonWriter;
import datadog.trace.test.agent.decoder.DecodedSpan;
import datadog.trace.test.agent.decoder.DecodedSpanLink;
import datadog.trace.test.agent.decoder.DecodedSpanLinks;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import okio.Buffer;
import org.msgpack.core.MessageFormat;
import org.msgpack.core.MessagePack;
import org.msgpack.core.MessageUnpacker;
Expand Down Expand Up @@ -59,6 +62,14 @@ public class SpanV1 implements DecodedSpan {
static final int LINK_FIELD_TRACE_STATE = 4;
static final int LINK_FIELD_TRACE_FLAGS = 5;

// Span event field IDs (from TraceMapperV1.encodeSpanEvents)
static final int EVENT_FIELD_TIME = 1;
static final int EVENT_FIELD_NAME = 2;
static final int EVENT_FIELD_ATTRIBUTES = 3;

// The meta tag the tracer encodes span events into for protocols without native events
static final String SPAN_EVENTS_TAG = "events";

// Attribute value types
static final int STRING_VALUE_TYPE = 1;
static final int BOOL_VALUE_TYPE = 2;
Expand Down Expand Up @@ -172,10 +183,10 @@ static SpanV1 unpack(MessageUnpacker unpacker, List<String> stringTable, long tr
links = unpackSpanLinks(unpacker, stringTable);
break;
case SPAN_FIELD_SPAN_EVENTS:
// TODO Span events are not decoded yet. This blind skip also consumes any streaming
// string the events introduced without registering it, desynchronizing the string
// table for the rest of the payload (see unpackSpanLinks for the table-aware pattern).
unpacker.skipValue();
String events = unpackSpanEvents(unpacker, stringTable);
if (events != null) {
meta.put(SPAN_EVENTS_TAG, events);
}
break;
case SPAN_FIELD_ENV:
env = unpackStreamingString(unpacker, stringTable);
Expand Down Expand Up @@ -396,6 +407,107 @@ static List<DecodedSpanLink> unpackSpanLinks(MessageUnpacker unpacker, List<Stri
return unmodifiableList(links);
}

/**
* Unpacks the structured span events of field {@value #SPAN_FIELD_SPAN_EVENTS}, as {@code
* TraceMapperV1.encodeSpanEvents} writes them, into the JSON value of the {@value
* #SPAN_EVENTS_TAG} meta tag, in the shape {@code DDSpanContext.spanEventsToTag} writes for
* protocols without native events: {@code time_unix_nano}, {@code name}, then {@code attributes}
* only when there are some.
*
* <p>Event names and attributes are streaming strings, so they are read through the table-aware
* helpers to keep every later string index in the payload in sync.
*
* @param unpacker The message unpacker.
* @param stringTable The shared string table for streaming string decoding.
* @return The JSON tag value, {@code null} if there are no events.
* @throws IOException If unpacking fails.
*/
static String unpackSpanEvents(MessageUnpacker unpacker, List<String> stringTable)
throws IOException {
int eventCount = unpacker.unpackArrayHeader();
if (eventCount == 0) {
return null;
}
List<Map<String, Object>> events = new ArrayList<>(eventCount);
for (int i = 0; i < eventCount; i++) {
int fieldCount = unpacker.unpackMapHeader();
long time = 0;
String name = "";
Map<String, Object> attributes = new LinkedHashMap<>();
for (int field = 0; field < fieldCount; field++) {
switch (unpacker.unpackInt()) {
case EVENT_FIELD_TIME:
time = unpacker.unpackLong();
break;
case EVENT_FIELD_NAME:
name = unpackStreamingString(unpacker, stringTable);
break;
case EVENT_FIELD_ATTRIBUTES:
forEachAttribute(
unpacker,
stringTable,
(attributeUnpacker, table, key, valueType) ->
attributes.put(
key, unpackEventAttributeValue(attributeUnpacker, table, valueType)));
break;
default:
// Unknown event field; numeric or binary, so it cannot hold a streaming string.
unpacker.skipValue();
break;
}
}
Map<String, Object> event = new LinkedHashMap<>();
event.put("time_unix_nano", time);
event.put("name", name);
if (!attributes.isEmpty()) {
event.put("attributes", attributes);
}
events.add(event);
}
Buffer buffer = new Buffer();
JsonWriter writer = JsonWriter.of(buffer);
// Unsupported attribute values are written as null and non-finite doubles as bare literals,
// as the tracer's events tag does.
writer.setSerializeNulls(true);
writer.setLenient(true);
writer.jsonValue(events);
writer.flush();
return buffer.readUtf8();
}

/**
* Unpacks an event attribute value: a string, boolean, double, long, or a list of them. Value
* types the tracer never writes for events decode to {@code null}.
*/
private static Object unpackEventAttributeValue(
MessageUnpacker unpacker, List<String> stringTable, int valueType) throws IOException {
switch (valueType) {
case STRING_VALUE_TYPE:
return unpackStreamingString(unpacker, stringTable);
case BOOL_VALUE_TYPE:
return unpacker.unpackBoolean();
case FLOAT_VALUE_TYPE:
return unpacker.unpackDouble();
case INT_VALUE_TYPE:
return unpacker.unpackLong();
case ARRAY_VALUE_TYPE:
int arraySize = unpacker.unpackArrayHeader();
// Array contains pairs (type, value), so size must be even
if (arraySize % 2 != 0) {
throw new IllegalArgumentException(
"Event attribute array size must be even, got: " + arraySize);
}
List<Object> items = new ArrayList<>(arraySize / 2);
for (int i = 0; i < arraySize / 2; i++) {
items.add(unpackEventAttributeValue(unpacker, stringTable, unpacker.unpackInt()));
}
return items;
default:
unpacker.skipValue();
return null;
}
}

/**
* Unpacks an identifier the writer emitted as an unsigned 64-bit value.
*
Expand Down
Loading
Loading