diff --git a/communication/src/main/java/datadog/communication/ddagent/DDAgentFeaturesDiscovery.java b/communication/src/main/java/datadog/communication/ddagent/DDAgentFeaturesDiscovery.java index 16be2e84b98..398435a2589 100644 --- a/communication/src/main/java/datadog/communication/ddagent/DDAgentFeaturesDiscovery.java +++ b/communication/src/main/java/datadog/communication/ddagent/DDAgentFeaturesDiscovery.java @@ -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; @@ -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(); } diff --git a/communication/src/test/groovy/datadog/communication/ddagent/DDAgentFeaturesDiscoveryTest.groovy b/communication/src/test/groovy/datadog/communication/ddagent/DDAgentFeaturesDiscoveryTest.groovy index 8bcda6eb811..6fae9ca5f40 100644 --- a/communication/src/test/groovy/datadog/communication/ddagent/DDAgentFeaturesDiscoveryTest.groovy +++ b/communication/src/test/groovy/datadog/communication/ddagent/DDAgentFeaturesDiscoveryTest.groovy @@ -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 = @@ -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 * _ } diff --git a/dd-trace-api/src/main/java/datadog/trace/api/ConfigDefaults.java b/dd-trace-api/src/main/java/datadog/trace/api/ConfigDefaults.java index bf5bab30d05..44b4acd1b7c 100644 --- a/dd-trace-api/src/main/java/datadog/trace/api/ConfigDefaults.java +++ b/dd-trace-api/src/main/java/datadog/trace/api/ConfigDefaults.java @@ -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; diff --git a/utils/test-agent-utils/decoder/src/main/java/datadog/trace/test/agent/decoder/json/raw/MessageJson.java b/utils/test-agent-utils/decoder/src/main/java/datadog/trace/test/agent/decoder/json/raw/MessageJson.java index 42e8971cf06..dae87d04c92 100644 --- a/utils/test-agent-utils/decoder/src/main/java/datadog/trace/test/agent/decoder/json/raw/MessageJson.java +++ b/utils/test-agent-utils/decoder/src/main/java/datadog/trace/test/agent/decoder/json/raw/MessageJson.java @@ -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; @@ -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 @@ -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); @@ -74,6 +80,7 @@ public static MessageJson fromJson(String json) { + span); } span.resolveLinks(); + span.resolveEvents(); decodedSpans.add(span); } traces.add(new TraceJson(decodedSpans)); @@ -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 getTraces() { return this.traces; diff --git a/utils/test-agent-utils/decoder/src/main/java/datadog/trace/test/agent/decoder/json/raw/SpanJson.java b/utils/test-agent-utils/decoder/src/main/java/datadog/trace/test/agent/decoder/json/raw/SpanJson.java index 8c59fd6835b..7cee1cf20cf 100644 --- a/utils/test-agent-utils/decoder/src/main/java/datadog/trace/test/agent/decoder/json/raw/SpanJson.java +++ b/utils/test-agent-utils/decoder/src/main/java/datadog/trace/test/agent/decoder/json/raw/SpanJson.java @@ -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; @@ -53,6 +54,12 @@ public final class SpanJson implements DecodedSpan { @Json(name = "span_links") List 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 links; @@ -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{" diff --git a/utils/test-agent-utils/decoder/src/main/java/datadog/trace/test/agent/decoder/v1/raw/SpanV1.java b/utils/test-agent-utils/decoder/src/main/java/datadog/trace/test/agent/decoder/v1/raw/SpanV1.java index c4ec1e4b6b9..7baeaf2572b 100644 --- a/utils/test-agent-utils/decoder/src/main/java/datadog/trace/test/agent/decoder/v1/raw/SpanV1.java +++ b/utils/test-agent-utils/decoder/src/main/java/datadog/trace/test/agent/decoder/v1/raw/SpanV1.java @@ -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; @@ -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; @@ -172,10 +183,10 @@ static SpanV1 unpack(MessageUnpacker unpacker, List 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); @@ -396,6 +407,107 @@ static List unpackSpanLinks(MessageUnpacker unpacker, ListEvent 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 stringTable) + throws IOException { + int eventCount = unpacker.unpackArrayHeader(); + if (eventCount == 0) { + return null; + } + List> events = new ArrayList<>(eventCount); + for (int i = 0; i < eventCount; i++) { + int fieldCount = unpacker.unpackMapHeader(); + long time = 0; + String name = ""; + Map 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 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 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 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. * diff --git a/utils/test-agent-utils/decoder/src/test/java/datadog/trace/test/agent/decoder/JsonDecoderTest.java b/utils/test-agent-utils/decoder/src/test/java/datadog/trace/test/agent/decoder/JsonDecoderTest.java index 78d60bf8e0f..20a7d55b59a 100644 --- a/utils/test-agent-utils/decoder/src/test/java/datadog/trace/test/agent/decoder/JsonDecoderTest.java +++ b/utils/test-agent-utils/decoder/src/test/java/datadog/trace/test/agent/decoder/JsonDecoderTest.java @@ -26,6 +26,70 @@ 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 nativeSpanEventsReplaceLegacySpanEventsTag() { + DecodedSpan span = + decodeSpanWithEvents( + "\"meta\":{\"events\":\"legacy\"},\"span_events\":[{\"name\":\"native\"}]"); + + assertEquals("[{\"name\":\"native\"}]", span.getMeta().get("events")); + } + + @Test + void emptyNativeSpanEventsDecodeToAnEmptyArray() { + assertEquals("[]", decodeSpanWithEvents("\"span_events\":[]").getMeta().get("events")); + } + + @Test + void nullOrAbsentNativeSpanEventsLeaveNoEventsTag() { + 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 traces = Decoder.decodeJson(TWO_TRACES).getTraces(); diff --git a/utils/test-agent-utils/decoder/src/test/java/datadog/trace/test/agent/decoder/v1/raw/SpanV1Test.java b/utils/test-agent-utils/decoder/src/test/java/datadog/trace/test/agent/decoder/v1/raw/SpanV1Test.java index 03e4ece9d6e..1c9f6d50ced 100644 --- a/utils/test-agent-utils/decoder/src/test/java/datadog/trace/test/agent/decoder/v1/raw/SpanV1Test.java +++ b/utils/test-agent-utils/decoder/src/test/java/datadog/trace/test/agent/decoder/v1/raw/SpanV1Test.java @@ -1,7 +1,9 @@ package datadog.trace.test.agent.decoder.v1.raw; +import static java.util.Arrays.asList; import static java.util.Collections.emptyList; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import datadog.trace.test.agent.decoder.DecodedSpanLink; import java.io.IOException; @@ -14,9 +16,9 @@ import org.msgpack.core.MessageUnpacker; /** - * Tests the {@code v1.0} span decoder against identifiers whose high bit is set. {@code - * TraceMapperV1} writes span identifiers with {@code writeUnsignedLong}, so those arrive as msgpack - * {@code UINT64} — a shape the {@code sample_v1.msgpack} fixture never produces. + * Tests the {@code v1.0} span decoder against shapes the {@code sample_v1.msgpack} fixture never + * produces: identifiers whose high bit is set ({@code TraceMapperV1} writes span identifiers with + * {@code writeUnsignedLong}, so those arrive as msgpack {@code UINT64}) and span events. */ class SpanV1Test { // 0xfedcba9876543210: an unsigned 64-bit identifier above Long.MAX_VALUE. @@ -71,6 +73,80 @@ void noSpanLinksDecodeToAnEmptyList() throws IOException { assertEquals(emptyList(), SpanV1.unpackSpanLinks(unpacker(packer), new ArrayList<>())); } + @Test + void decodesSpanEventsIntoTheEventsTag() throws IOException { + MessageBufferPacker packer = MessagePack.newDefaultBufferPacker(); + packer.packArrayHeader(2); + // First event: every attribute value type, introducing new streaming strings. + packer.packMapHeader(3); + packer.packInt(SpanV1.EVENT_FIELD_TIME); + packer.packLong(1791902357478000001L); + packer.packInt(SpanV1.EVENT_FIELD_NAME); + packer.packString("configuration-loaded"); // string table index 0 + packer.packInt(SpanV1.EVENT_FIELD_ATTRIBUTES); + packer.packArrayHeader(5 * 3); + packer.packString("source").packInt(SpanV1.STRING_VALUE_TYPE).packString("file \"local\""); + packer.packString("count").packInt(SpanV1.INT_VALUE_TYPE).packLong(3); + packer.packString("ratio").packInt(SpanV1.FLOAT_VALUE_TYPE).packDouble(2.5); + packer.packString("cached").packInt(SpanV1.BOOL_VALUE_TYPE).packBoolean(false); + packer.packString("tags").packInt(SpanV1.ARRAY_VALUE_TYPE).packArrayHeader(2 * 2); + packer.packInt(SpanV1.STRING_VALUE_TYPE).packInt(0); // reference to "configuration-loaded" + packer.packInt(SpanV1.INT_VALUE_TYPE).packLong(1); + // Second event: name by reference, no attributes. + packer.packMapHeader(3); + packer.packInt(SpanV1.EVENT_FIELD_TIME); + packer.packLong(2); + packer.packInt(SpanV1.EVENT_FIELD_NAME); + packer.packInt(0); + packer.packInt(SpanV1.EVENT_FIELD_ATTRIBUTES); + packer.packArrayHeader(0); + List stringTable = new ArrayList<>(); + + String events = SpanV1.unpackSpanEvents(unpacker(packer), stringTable); + + assertEquals( + "[{\"time_unix_nano\":1791902357478000001,\"name\":\"configuration-loaded\"," + + "\"attributes\":{\"source\":\"file \\\"local\\\"\",\"count\":3,\"ratio\":2.5," + + "\"cached\":false,\"tags\":[\"configuration-loaded\",1]}}," + + "{\"time_unix_nano\":2,\"name\":\"configuration-loaded\"}]", + events); + assertEquals( + asList( + "configuration-loaded", "source", "file \"local\"", "count", "ratio", "cached", "tags"), + stringTable, + "event strings registered in the string table"); + } + + @Test + void spanEventsKeepTheStringTableInSync() throws IOException { + MessageBufferPacker packer = MessagePack.newDefaultBufferPacker(); + packer.packMapHeader(2); + packer.packInt(SpanV1.SPAN_FIELD_SPAN_EVENTS); + packer.packArrayHeader(1); + packer.packMapHeader(1); + packer.packInt(SpanV1.EVENT_FIELD_NAME); + packer.packString("event-name"); // string table index 0 + packer.packInt(SpanV1.SPAN_FIELD_SERVICE); + packer.packInt(0); // reference to the string the event introduced + + SpanV1 span = SpanV1.unpack(unpacker(packer), new ArrayList<>()); + + assertEquals("[{\"time_unix_nano\":0,\"name\":\"event-name\"}]", span.getMeta().get("events")); + assertEquals("event-name", span.getService()); + } + + @Test + void noSpanEventsLeaveNoEventsTag() throws IOException { + MessageBufferPacker packer = MessagePack.newDefaultBufferPacker(); + packer.packMapHeader(1); + packer.packInt(SpanV1.SPAN_FIELD_SPAN_EVENTS); + packer.packArrayHeader(0); + + SpanV1 span = SpanV1.unpack(unpacker(packer), new ArrayList<>()); + + assertFalse(span.getMeta().containsKey("events")); + } + private static long spanIdOf(BigInteger id) throws IOException { MessageBufferPacker packer = MessagePack.newDefaultBufferPacker(); packer.packMapHeader(1);