From 9a21f3b492b5d2c998dce41612ab63eb36e5e97a Mon Sep 17 00:00:00 2001 From: Alexey Kuznetsov Date: Thu, 2 Jul 2026 19:26:01 -0400 Subject: [PATCH] Refactored span events to avoid not needed JSON deserialization for v1 protocol. --- .../shim/trace/OtelConventions.java | 6 +- .../opentelemetry/shim/trace/OtelSpan.java | 4 +- .../shim/trace/OtelSpanEvent.java | 86 ++++-- .../agent/test/TrackingSpanDecorator.groovy | 5 + .../common/writer/ddagent/TraceMapperV1.java | 73 +---- .../java/datadog/trace/core/CoreSpan.java | 6 +- .../main/java/datadog/trace/core/DDSpan.java | 35 ++- .../datadog/trace/core/DDSpanContext.java | 50 +++- .../java/datadog/trace/core/Metadata.java | 10 +- .../ddagent/TraceMapperV1PayloadTest.groovy | 121 ++++++-- .../FileBasedPayloadDispatcherTest.java | 1 + .../trace/common/writer/TraceGenerator.java | 44 ++- .../trace/core/DDSpanSerializationTest.java | 275 ++++++++++-------- .../groovy/TraceGenerator.groovy | 2 +- .../instrumentation/api/AgentSpan.java | 3 + .../instrumentation/api/AgentSpanEvent.java | 50 ++++ .../instrumentation/api/ImmutableSpan.java | 4 + 17 files changed, 520 insertions(+), 255 deletions(-) create mode 100644 internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/AgentSpanEvent.java diff --git a/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/trace/OtelConventions.java b/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/trace/OtelConventions.java index a328a63140f..1c8dc7852e2 100644 --- a/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/trace/OtelConventions.java +++ b/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/trace/OtelConventions.java @@ -7,7 +7,6 @@ import static datadog.trace.api.DDTags.ERROR_MSG; import static datadog.trace.api.DDTags.ERROR_STACK; import static datadog.trace.api.DDTags.ERROR_TYPE; -import static datadog.trace.api.DDTags.SPAN_EVENTS; import static datadog.trace.bootstrap.instrumentation.api.Tags.SPAN_KIND; import static datadog.trace.bootstrap.instrumentation.api.Tags.SPAN_KIND_CLIENT; import static datadog.trace.bootstrap.instrumentation.api.Tags.SPAN_KIND_CONSUMER; @@ -136,11 +135,12 @@ public static void applyNamingConvention(AgentSpan span) { } } - public static void setEventsAsTag(AgentSpan span, List events) { + public static void recordSpanEvents(AgentSpan span, List events) { if (events == null || events.isEmpty()) { return; } - span.setTag(SPAN_EVENTS, OtelSpanEvent.toTag(events)); + + span.addSpanEvents(events); } public static void applySpanEventExceptionAttributesAsTags( diff --git a/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/trace/OtelSpan.java b/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/trace/OtelSpan.java index 34bf7f2a486..459be506fd7 100644 --- a/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/trace/OtelSpan.java +++ b/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/trace/OtelSpan.java @@ -3,7 +3,7 @@ import static datadog.opentelemetry.shim.trace.OtelConventions.applyNamingConvention; import static datadog.opentelemetry.shim.trace.OtelConventions.applyReservedAttribute; import static datadog.opentelemetry.shim.trace.OtelConventions.applySpanEventExceptionAttributesAsTags; -import static datadog.opentelemetry.shim.trace.OtelConventions.setEventsAsTag; +import static datadog.opentelemetry.shim.trace.OtelConventions.recordSpanEvents; import static datadog.opentelemetry.shim.trace.OtelSpanEvent.EXCEPTION_SPAN_EVENT_NAME; import static datadog.opentelemetry.shim.trace.OtelSpanEvent.initializeExceptionAttributes; import static datadog.trace.bootstrap.instrumentation.api.AgentTracer.activateSpan; @@ -179,7 +179,7 @@ public AgentSpan asAgentSpan() { @Override public void onSpanFinished() { applyNamingConvention(this.delegate); - setEventsAsTag(this.delegate, this.events); + recordSpanEvents(this.delegate, this.events); } private static class NoopSpan implements Span { diff --git a/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/trace/OtelSpanEvent.java b/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/trace/OtelSpanEvent.java index a49cab4b877..8ed4e7abda9 100644 --- a/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/trace/OtelSpanEvent.java +++ b/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/trace/OtelSpanEvent.java @@ -2,18 +2,20 @@ import datadog.trace.api.time.SystemTimeSource; import datadog.trace.api.time.TimeSource; +import datadog.trace.bootstrap.instrumentation.api.AgentSpanEvent; import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.api.common.Attributes; import io.opentelemetry.api.common.AttributesBuilder; import java.io.PrintWriter; import java.io.StringWriter; +import java.util.Collections; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.TimeUnit; -import javax.annotation.Nonnull; -public class OtelSpanEvent { +public class OtelSpanEvent implements AgentSpanEvent { public static final String EXCEPTION_SPAN_EVENT_NAME = "exception"; public static final AttributeKey EXCEPTION_MESSAGE_ATTRIBUTE_KEY = AttributeKey.stringKey("exception.message"); @@ -26,33 +28,69 @@ public class OtelSpanEvent { private static TimeSource timeSource = SystemTimeSource.INSTANCE; private final String name; - private final String attributes; + private final Attributes attributes; /** Event timestamp in nanoseconds. */ private final long timestamp; public OtelSpanEvent(String name, Attributes attributes) { - this.name = name; - this.attributes = AttributesJsonParser.toJson(attributes); - this.timestamp = OtelSpanEvent.timeSource.getCurrentTimeNanos(); + this(name, attributes, OtelSpanEvent.timeSource.getCurrentTimeNanos()); } public OtelSpanEvent(String name, Attributes attributes, long timestamp, TimeUnit unit) { + this(name, attributes, unit.toNanos(timestamp)); + } + + private OtelSpanEvent(String name, Attributes attributes, long timestampNanos) { this.name = name; - this.attributes = AttributesJsonParser.toJson(attributes); - this.timestamp = unit.toNanos(timestamp); + this.attributes = attributes; + this.timestamp = timestampNanos; } - @Nonnull - public static String toTag(List events) { - StringBuilder builder = new StringBuilder("["); - for (OtelSpanEvent event : events) { - if (builder.length() > 1) { - builder.append(','); - } - builder.append(event.toJson()); + @Override + public String name() { + return this.name; + } + + @Override + public long timeNanos() { + return this.timestamp; + } + + /** + * Exposes the event attributes as typed values for native (V1) encoding. OpenTelemetry attribute + * values are already {@link String}, {@link Boolean}, {@link Long}, {@link Double} or a {@link + * List} of those, so they are passed through unchanged. + */ + @Override + public Map attributes() { + if (this.attributes == null || this.attributes.isEmpty()) { + return Collections.emptyMap(); } - return builder.append(']').toString(); + Map map = new LinkedHashMap<>(this.attributes.size()); + this.attributes.forEach((key, value) -> map.put(key.getKey(), value)); + return map; + } + + @Override + public CharSequence toJson() { + StringBuilder builder = new StringBuilder(128); + builder + .append('{') + .append("\"time_unix_nano\":") + .append(this.timestamp) + .append(',') + .append("\"name\":") + .append('"') + .append(this.name) + .append('"'); + + String attributesJson = AttributesJsonParser.toJson(this.attributes); + if (!attributesJson.isEmpty()) { + builder.append(",\"attributes\":").append(attributesJson); + } + + return builder.append('}').toString(); } /** @@ -170,24 +208,14 @@ public static void setTimeSource(TimeSource newTimeSource) { timeSource = newTimeSource; } - public String toJson() { - StringBuilder builder = - new StringBuilder( - "{\"time_unix_nano\":" + this.timestamp + ",\"name\":\"" + this.name + "\""); - if (!this.attributes.isEmpty()) { - builder.append(",\"attributes\":").append(this.attributes); - } - return builder.append('}').toString(); - } - @Override public String toString() { return "OtelSpanEvent{timestamp=" + this.timestamp + ", name='" + this.name - + "', attributes='" + + "', attributes=" + this.attributes - + "'}"; + + '}'; } } diff --git a/dd-java-agent/instrumentation-testing/src/main/groovy/datadog/trace/agent/test/TrackingSpanDecorator.groovy b/dd-java-agent/instrumentation-testing/src/main/groovy/datadog/trace/agent/test/TrackingSpanDecorator.groovy index b9de4adad0e..a56406a4629 100644 --- a/dd-java-agent/instrumentation-testing/src/main/groovy/datadog/trace/agent/test/TrackingSpanDecorator.groovy +++ b/dd-java-agent/instrumentation-testing/src/main/groovy/datadog/trace/agent/test/TrackingSpanDecorator.groovy @@ -9,6 +9,7 @@ import datadog.trace.api.interceptor.MutableSpan import datadog.trace.bootstrap.CallDepthThreadLocalMap import datadog.trace.bootstrap.instrumentation.api.AgentSpan import datadog.trace.bootstrap.instrumentation.api.AgentSpanContext +import datadog.trace.bootstrap.instrumentation.api.AgentSpanEvent import datadog.trace.bootstrap.instrumentation.api.AgentSpanLink import datadog.trace.core.DDSpan @@ -405,6 +406,10 @@ class TrackingSpanDecorator implements AgentSpan { delegate.addLink(link) } + void addSpanEvents(List events) { + delegate.addSpanEvents(events) + } + @Override AgentSpan setMetaStruct(String field, Object value) { return delegate.setMetaStruct(field, value) diff --git a/dd-trace-core/src/main/java/datadog/trace/common/writer/ddagent/TraceMapperV1.java b/dd-trace-core/src/main/java/datadog/trace/common/writer/ddagent/TraceMapperV1.java index 819ff2021be..5583aaea610 100644 --- a/dd-trace-core/src/main/java/datadog/trace/common/writer/ddagent/TraceMapperV1.java +++ b/dd-trace-core/src/main/java/datadog/trace/common/writer/ddagent/TraceMapperV1.java @@ -17,6 +17,7 @@ import datadog.trace.api.ProcessTags; import datadog.trace.api.TagMap; import datadog.trace.api.sampling.SamplingMechanism; +import datadog.trace.bootstrap.instrumentation.api.AgentSpanEvent; import datadog.trace.bootstrap.instrumentation.api.AgentSpanLink; import datadog.trace.bootstrap.instrumentation.api.InstrumentationTags; import datadog.trace.bootstrap.instrumentation.api.Tags; @@ -89,7 +90,7 @@ public void map(List> trace, Writable writable) { } CoreSpan firstSpan = trace.get(0); - firstSpan.processTagsAndBaggageWithStructuredLinks(spanMetadata); + firstSpan.processTagsAndBaggageWithStructuredLinksAndEvents(spanMetadata); Metadata firstSpanMeta = spanMetadata.metadata; // encoded fields: 1..7, but skipping #5, as not required by tracers and set by the agent. @@ -128,7 +129,7 @@ private void encodeSpans(Writable writable, int fieldId, List span : spans) { if (meta == null) { - span.processTagsAndBaggageWithStructuredLinks(spanMetadata); + span.processTagsAndBaggageWithStructuredLinksAndEvents(spanMetadata); meta = spanMetadata.metadata; } TagMap tags = meta.getTags(); @@ -162,7 +163,7 @@ private void encodeSpans(Writable writable, int fieldId, List events) { writable.writeInt(fieldId); - if (!(eventsObject instanceof List) || ((List) eventsObject).isEmpty()) { + if (events == null || events.isEmpty()) { writable.startArray(0); return; } - List events = (List) eventsObject; - int encodableCount = 0; - for (Object event : events) { - if (isEncodableSpanEvent(event)) { - encodableCount++; - } - } - writable.startArray(encodableCount); - for (Object event : events) { - if (!(event instanceof Map)) { - continue; - } - Map eventMap = (Map) event; - Long timeUnixNano = asLong(eventMap.get("time_unix_nano")); - Object nameObject = eventMap.get("name"); - if (timeUnixNano == null || nameObject == null) { - continue; - } - - Map attributes = - eventMap.get("attributes") instanceof Map ? (Map) eventMap.get("attributes") : null; - + writable.startArray(events.size()); + for (AgentSpanEvent event : events) { writable.startMap(3); - encodeLong(writable, 1, timeUnixNano); - encodeString(writable, 2, String.valueOf(nameObject)); - encodeEventAttributes(writable, 3, attributes); - } - } - - private boolean isEncodableSpanEvent(Object event) { - if (!(event instanceof Map)) { - return false; + encodeLong(writable, 1, event.timeNanos()); + encodeString(writable, 2, event.name()); + encodeEventAttributes(writable, 3, event.attributes()); } - Map eventMap = (Map) event; - return eventMap.get("name") != null && asLong(eventMap.get("time_unix_nano")) != null; } private void encodeEventAttributes(Writable writable, int fieldId, Map attrs) { @@ -340,20 +314,6 @@ private boolean isIntegralNumber(Number number) { return !(number instanceof Float || number instanceof Double); } - private Long asLong(Object value) { - if (value instanceof Number) { - return ((Number) value).longValue(); - } - if (value instanceof CharSequence) { - try { - return Long.parseLong(value.toString()); - } catch (NumberFormatException ignored) { - return null; - } - } - return null; - } - private void encodeSpanAttributes( Writable writable, int fieldId, Metadata meta, Map metaStruct) { TagMap tags = meta.getTags(); @@ -364,9 +324,7 @@ private void encodeSpanAttributes( boolean writeTopLevel = meta.topLevel(); int tagCount = 0; for (TagMap.EntryReader entry : tags) { - if (!DDTags.SPAN_EVENTS.equals(entry.tag())) { - tagCount += getFlatAttributeCount(entry); - } + tagCount += getFlatAttributeCount(entry); } writable.writeInt(fieldId); @@ -387,9 +345,6 @@ private void encodeSpanAttributes( } for (TagMap.EntryReader entry : tags) { - if (DDTags.SPAN_EVENTS.equals(entry.tag())) { - continue; - } writeFlattenedTagAttribute(writable, entry); } if (writeHttpStatus) { @@ -487,7 +442,7 @@ private void writeAttribute(Writable writable, String key, Object value) { return; } - if (!(value instanceof String) && value != null) { + if (!(value instanceof CharSequence) && value != null) { log.debug("Not a string value for key: {}, value: {}", key, value); } writable.writeInt(VALUE_TYPE_STRING); diff --git a/dd-trace-core/src/main/java/datadog/trace/core/CoreSpan.java b/dd-trace-core/src/main/java/datadog/trace/core/CoreSpan.java index da163c2f871..64b8d0de76e 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/CoreSpan.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/CoreSpan.java @@ -100,13 +100,13 @@ default String getSpanKindString() { /** * Variant of {@link #processTagsAndBaggage(MetadataConsumer)} for protocols that serialize span - * links as first-class structured data rather than tags. Baggage tag injection still follows the - * tracer configuration. + * links and span events as first-class structured data rather than flattening them into tags. + * Baggage tag injection still follows the tracer configuration. * *

To simplify tests, by default delegating to {@link * #processTagsAndBaggage(MetadataConsumer)}. */ - default void processTagsAndBaggageWithStructuredLinks(MetadataConsumer consumer) { + default void processTagsAndBaggageWithStructuredLinksAndEvents(MetadataConsumer consumer) { processTagsAndBaggage(consumer); } diff --git a/dd-trace-core/src/main/java/datadog/trace/core/DDSpan.java b/dd-trace-core/src/main/java/datadog/trace/core/DDSpan.java index a288c405e6f..01daea4735c 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/DDSpan.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/DDSpan.java @@ -27,6 +27,7 @@ import datadog.trace.api.sampling.SamplingMechanism; import datadog.trace.bootstrap.debugger.DebuggerContext; import datadog.trace.bootstrap.instrumentation.api.AgentSpan; +import datadog.trace.bootstrap.instrumentation.api.AgentSpanEvent; import datadog.trace.bootstrap.instrumentation.api.AgentSpanLink; import datadog.trace.bootstrap.instrumentation.api.AttachableWrapper; import datadog.trace.bootstrap.instrumentation.api.ErrorPriorities; @@ -120,6 +121,9 @@ static DDSpan create( private static final List EMPTY = Collections.emptyList(); protected volatile List links; + private static final List NO_EVENTS = Collections.emptyList(); + protected volatile List spanEvents = NO_EVENTS; + /** * Spans should be constructed using the builder, not by calling the constructor directly. * @@ -781,8 +785,8 @@ public void processTagsAndBaggage(final MetadataConsumer consumer) { } @Override - public void processTagsAndBaggageWithStructuredLinks(final MetadataConsumer consumer) { - context.processTagsAndBaggageWithStructuredLinks(consumer, longRunningVersion, this); + public void processTagsAndBaggageWithStructuredLinksAndEvents(final MetadataConsumer consumer) { + context.processTagsAndBaggageWithStructuredLinksAndEvents(consumer, longRunningVersion, this); } @Override @@ -937,6 +941,33 @@ public void addLink(AgentSpanLink link) { } } + public List getSpanEvents() { + return this.spanEvents; + } + + @Override + public void addSpanEvents(List newEvents) { + if (newEvents == null || newEvents.isEmpty()) { + return; + } + + // Mirrors `addLink()` method thread safety model. + List events = this.spanEvents; + if (events != NO_EVENTS) { + events.addAll(newEvents); + return; + } + + synchronized (this) { + events = this.spanEvents; + if (events != NO_EVENTS) { + events.addAll(newEvents); + } else { + this.spanEvents = new CopyOnWriteArrayList<>(newEvents); + } + } + } + // to be accessible in Spock spies, which the field wouldn't otherwise be public long getStartTimeNano() { return startTimeNano; diff --git a/dd-trace-core/src/main/java/datadog/trace/core/DDSpanContext.java b/dd-trace-core/src/main/java/datadog/trace/core/DDSpanContext.java index 6120502ec09..c96535ac186 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/DDSpanContext.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/DDSpanContext.java @@ -1,6 +1,7 @@ package datadog.trace.core; import static datadog.trace.api.DDTags.PARENT_ID; +import static datadog.trace.api.DDTags.SPAN_EVENTS; import static datadog.trace.api.DDTags.SPAN_LINKS; import static datadog.trace.api.cache.RadixTreeCache.HTTP_STATUSES; import static datadog.trace.bootstrap.instrumentation.api.ErrorPriorities.UNSET; @@ -24,6 +25,7 @@ import datadog.trace.api.sampling.PrioritySampling; import datadog.trace.api.sampling.SamplingMechanism; import datadog.trace.bootstrap.instrumentation.api.AgentSpanContext; +import datadog.trace.bootstrap.instrumentation.api.AgentSpanEvent; import datadog.trace.bootstrap.instrumentation.api.AppendableSpanLinks; import datadog.trace.bootstrap.instrumentation.api.Baggage; import datadog.trace.bootstrap.instrumentation.api.ClientIpAddressData; @@ -1196,21 +1198,23 @@ void earlyProcessTags(AppendableSpanLinks links) { void processTagsAndBaggage( final MetadataConsumer consumer, int longRunningVersion, DDSpan restrictedSpan) { processTagsAndBaggage( - consumer, longRunningVersion, restrictedSpan, injectLinksAsTags, injectBaggageAsTags); + consumer, longRunningVersion, restrictedSpan, injectLinksAsTags, injectBaggageAsTags, true); } /** - * Serialize span links as first-class structured data rather than tags. While baggage tag - * injection keeps following the tracer configuration. + * Serialize span links and span events as first-class structured data rather than flattening them + * into tags. Baggage tag injection keeps following the tracer configuration. */ - void processTagsAndBaggageWithStructuredLinks( + void processTagsAndBaggageWithStructuredLinksAndEvents( final MetadataConsumer consumer, int longRunningVersion, DDSpan restrictedSpan) { processTagsAndBaggage( consumer, longRunningVersion, restrictedSpan, false, // injectLinksAsTags - injectBaggageAsTags); + injectBaggageAsTags, + false // injectEventsAsTags + ); } void processTagsAndBaggage( @@ -1218,9 +1222,10 @@ void processTagsAndBaggage( int longRunningVersion, DDSpan restrictedSpan, boolean injectLinksAsTags, - boolean injectBaggageAsTags) { + boolean injectBaggageAsTags, + boolean injectEventsAsTags) { // NOTE: The span is passed for the sole purpose of allowing updating & reading of the span - // links + // links and events // This is a compromise to avoid... // - creating an extra wrapper object that would create significant allocation // - implementing an interface to read the spans that require making the read method public @@ -1236,6 +1241,15 @@ void processTagsAndBaggage( } } + // Events: flatten the structured span events into the legacy JSON `events` tag for protocols + // that don't encode them natively (V1 reads Metadata.getSpanEvents() instead). + if (injectEventsAsTags) { + String eventsTag = spanEventsToTag(restrictedSpan.getSpanEvents()); + if (eventsTag != null) { + unsafeTags.set(SPAN_EVENTS, eventsTag); + } + } + // Baggage Map baggageItemsWithPropagationTags; if (injectBaggageAsTags) { @@ -1262,8 +1276,28 @@ void processTagsAndBaggage( getOrigin(), longRunningVersion, ProcessTags.getTagsForSerialization(), - restrictedSpan.getLinks())); + restrictedSpan.getLinks(), + restrictedSpan.getSpanEvents())); + } + } + + /** + * Assembles the legacy v0.x {@code events} tag: a JSON array of the per-event objects produced by + * {@link AgentSpanEvent#toJson()}. Returns {@code null} when there are no events so the tag is + * not emitted. + */ + private static String spanEventsToTag(List events) { + if (events == null || events.isEmpty()) { + return null; + } + StringBuilder builder = new StringBuilder("["); + for (AgentSpanEvent event : events) { + if (builder.length() > 1) { + builder.append(','); + } + builder.append(event.toJson()); } + return builder.append(']').toString(); } void injectW3CBaggageTags(Map baggageItemsWithPropagationTags) { diff --git a/dd-trace-core/src/main/java/datadog/trace/core/Metadata.java b/dd-trace-core/src/main/java/datadog/trace/core/Metadata.java index b7f9a6b2cc2..aab99c0a0f6 100644 --- a/dd-trace-core/src/main/java/datadog/trace/core/Metadata.java +++ b/dd-trace-core/src/main/java/datadog/trace/core/Metadata.java @@ -4,6 +4,7 @@ import static java.util.Collections.emptyList; import datadog.trace.api.TagMap; +import datadog.trace.bootstrap.instrumentation.api.AgentSpanEvent; import datadog.trace.bootstrap.instrumentation.api.AgentSpanLink; import datadog.trace.bootstrap.instrumentation.api.UTF8BytesString; import java.util.List; @@ -23,6 +24,7 @@ public final class Metadata { private final int longRunningVersion; private final UTF8BytesString processTags; private final List spanLinks; + private final List spanEvents; public Metadata( long threadId, @@ -36,7 +38,8 @@ public Metadata( CharSequence origin, int longRunningVersion, UTF8BytesString processTags, - List spanLinks) { + List spanLinks, + List spanEvents) { this.threadId = threadId; this.threadName = threadName; this.httpStatusCode = httpStatusCode; @@ -49,6 +52,7 @@ public Metadata( this.longRunningVersion = longRunningVersion; this.processTags = processTags; this.spanLinks = spanLinks == null ? emptyList() : spanLinks; + this.spanEvents = spanEvents == null ? emptyList() : spanEvents; } public UTF8BytesString getHttpStatusCode() { @@ -102,4 +106,8 @@ public UTF8BytesString processTags() { public List getSpanLinks() { return spanLinks; } + + public List getSpanEvents() { + return spanEvents; + } } diff --git a/dd-trace-core/src/test/groovy/datadog/trace/common/writer/ddagent/TraceMapperV1PayloadTest.groovy b/dd-trace-core/src/test/groovy/datadog/trace/common/writer/ddagent/TraceMapperV1PayloadTest.groovy index b9975dd3038..51881ce93b4 100644 --- a/dd-trace-core/src/test/groovy/datadog/trace/common/writer/ddagent/TraceMapperV1PayloadTest.groovy +++ b/dd-trace-core/src/test/groovy/datadog/trace/common/writer/ddagent/TraceMapperV1PayloadTest.groovy @@ -22,6 +22,7 @@ import datadog.trace.api.DDSpanId import datadog.trace.api.ProcessTags import datadog.trace.api.sampling.PrioritySampling import datadog.trace.api.sampling.SamplingMechanism +import datadog.trace.bootstrap.instrumentation.api.AgentSpanEvent import datadog.trace.bootstrap.instrumentation.api.InstrumentationTags import datadog.trace.bootstrap.instrumentation.api.SpanAttributes import datadog.trace.bootstrap.instrumentation.api.SpanLink @@ -446,24 +447,17 @@ class TraceMapperV1PayloadTest extends DDSpecification { assertTrue(links.isEmpty()) } - def "test span events are encoded from events tag"() { + def "test span events are encoded from structured span events"() { setup: - List> eventPayload = [ - [ - time_unix_nano: 1234567890L, - name : "event.one", - attributes : [ - str : "v", - int : 42L, - double: 12.5d, - bool : true, - arr : ["x", 7L, 2.5d, false] - ] - ], - [ - time_unix_nano: 1234567891L, - name : "event.two" - ] + List spanEvents = [ + spanEvent(1234567890L, "event.one", [ + str : "v", + int : 42L, + double: 12.5d, + bool : true, + arr : ["x", 7L, 2.5d, false] + ]), + spanEvent(1234567891L, "event.two", [:]) ] def span = new TraceGenerator.PojoSpan( "service-a", @@ -476,12 +470,14 @@ class TraceMapperV1PayloadTest extends DDSpecification { 2000L, 0, [:], - ["events": eventPayload], + [:], "web", false, PrioritySampling.SAMPLER_KEEP, 200, - null) + null, + [], + spanEvents) TraceMapperV1 mapper = new TraceMapperV1() byte[] encoded = serializeMappedPayload(mapper, [[span]]) @@ -490,7 +486,7 @@ class TraceMapperV1PayloadTest extends DDSpecification { stringTable.add("") when: - List> events = readFirstSpan(unpacker, stringTable).events + def events = readFirstSpan(unpacker, stringTable).events then: assertEquals(2, events.size()) @@ -507,7 +503,7 @@ class TraceMapperV1PayloadTest extends DDSpecification { assertEquals([:], events[1].attributes) } - def "test malformed span events fall back to empty events"() { + def "test missing span events encode empty events"() { setup: def span = new TraceGenerator.PojoSpan( "service-a", @@ -520,7 +516,7 @@ class TraceMapperV1PayloadTest extends DDSpecification { 2000L, 0, [:], - ["events": [foo: "bar"]], + [:], "web", false, PrioritySampling.SAMPLER_KEEP, @@ -534,12 +530,64 @@ class TraceMapperV1PayloadTest extends DDSpecification { stringTable.add("") when: - List> events = readFirstSpan(unpacker, stringTable).events + def events = readFirstSpan(unpacker, stringTable).events then: assertTrue(events.isEmpty()) } + def "test unsupported span event attribute values are skipped"() { + setup: + Map attributes = new LinkedHashMap<>() + attributes.put("keep.str", "v") + attributes.put("keep.bool", true) + attributes.put("drop.null", null) + attributes.put("drop.map", [nested: "x"]) + attributes.put("arr.mixed", ["ok", null, [nested: "x"], 3L]) + List spanEvents = [spanEvent(5L, "event", attributes)] + def span = new TraceGenerator.PojoSpan( + "service-a", + "operation-a", + "resource-a", + DDTraceId.ONE, + 123L, + 0L, + 1000L, + 2000L, + 0, + [:], + [:], + "web", + false, + PrioritySampling.SAMPLER_KEEP, + 200, + null, + [], + spanEvents) + + TraceMapperV1 mapper = new TraceMapperV1() + byte[] encoded = serializeMappedPayload(mapper, [[span]]) + MessageUnpacker unpacker = MessagePack.newDefaultUnpacker(encoded) + List stringTable = new ArrayList<>() + stringTable.add("") + + when: + def events = readFirstSpan(unpacker, stringTable).events + + then: + assertEquals(1, events.size()) + assertEquals(5L, events[0].timeUnixNano) + assertEquals("event", events[0].name) + // null and Map values are not encodable and are dropped + assertEquals(3, events[0].attributes.size()) + assertEquals("v", events[0].attributes["keep.str"]) + assertEquals(true, events[0].attributes["keep.bool"]) + assertTrue(!events[0].attributes.containsKey("drop.null")) + assertTrue(!events[0].attributes.containsKey("drop.map")) + // unsupported array items (null, Map) are dropped, encodable ones kept in order + assertEquals(["ok", 3L], events[0].attributes["arr.mixed"]) + } + def "test meta struct is encoded as bytes attribute"() { setup: def span = new TraceGenerator.PojoSpan( @@ -1013,9 +1061,6 @@ class TraceMapperV1PayloadTest extends DDSpecification { expectedAttributes.put(DDTags.THREAD_ID, expectedSpan.getTag(DDTags.THREAD_ID)) expectedAttributes.put(DDTags.THREAD_NAME, expectedSpan.getTag(DDTags.THREAD_NAME)) for (Map.Entry entry : expectedSpan.getTags().entrySet()) { - if (DDTags.SPAN_EVENTS == entry.getKey()) { - continue - } addFlattenedExpectedAttribute(expectedAttributes, entry.getKey(), entry.getValue()) } if (shouldContainHttpStatus) { @@ -1199,6 +1244,30 @@ class TraceMapperV1PayloadTest extends DDSpecification { } } + private static AgentSpanEvent spanEvent(long timeNanos, String name, Map attributes) { + return new AgentSpanEvent() { + @Override + long timeNanos() { + return timeNanos + } + + @Override + String name() { + return name + } + + @Override + Map attributes() { + return attributes + } + + @Override + CharSequence toJson() { + return "" + } + } + } + private static void assertEqualsWithNullAsEmpty(CharSequence expected, CharSequence actual) { if (expected == null) { assertEquals("", actual) diff --git a/dd-trace-core/src/test/java/datadog/trace/common/writer/FileBasedPayloadDispatcherTest.java b/dd-trace-core/src/test/java/datadog/trace/common/writer/FileBasedPayloadDispatcherTest.java index 2040f685fdc..761ae52bd1d 100644 --- a/dd-trace-core/src/test/java/datadog/trace/common/writer/FileBasedPayloadDispatcherTest.java +++ b/dd-trace-core/src/test/java/datadog/trace/common/writer/FileBasedPayloadDispatcherTest.java @@ -313,6 +313,7 @@ private static CoreSpan mockSpan(CharSequence type, Map tags) null, 0, null, + null, null); doAnswer( inv -> { diff --git a/dd-trace-core/src/test/java/datadog/trace/common/writer/TraceGenerator.java b/dd-trace-core/src/test/java/datadog/trace/common/writer/TraceGenerator.java index c8fad4ab0ee..5f64da41660 100644 --- a/dd-trace-core/src/test/java/datadog/trace/common/writer/TraceGenerator.java +++ b/dd-trace-core/src/test/java/datadog/trace/common/writer/TraceGenerator.java @@ -10,6 +10,7 @@ import datadog.trace.api.ProcessTags; import datadog.trace.api.TagMap; import datadog.trace.api.sampling.PrioritySampling; +import datadog.trace.bootstrap.instrumentation.api.AgentSpanEvent; import datadog.trace.bootstrap.instrumentation.api.AgentSpanLink; import datadog.trace.bootstrap.instrumentation.api.Tags; import datadog.trace.bootstrap.instrumentation.api.UTF8BytesString; @@ -216,6 +217,46 @@ public PojoSpan( int statusCode, CharSequence origin, List spanLinks) { + this( + serviceName, + operationName, + resourceName, + traceId, + spanId, + parentId, + start, + duration, + error, + baggage, + tags, + type, + measured, + samplingPriority, + statusCode, + origin, + spanLinks, + emptyList()); + } + + public PojoSpan( + String serviceName, + String operationName, + CharSequence resourceName, + DDTraceId traceId, + long spanId, + long parentId, + long start, + long duration, + int error, + Map baggage, + Map tags, + CharSequence type, + boolean measured, + int samplingPriority, + int statusCode, + CharSequence origin, + List spanLinks, + List spanEvents) { this.serviceName = UTF8BytesString.create(serviceName); this.operationName = UTF8BytesString.create(operationName); this.resourceName = UTF8BytesString.create(resourceName); @@ -242,7 +283,8 @@ public PojoSpan( origin, 0, ProcessTags.getTagsForSerialization(), - spanLinks); + spanLinks, + spanEvents); } @Override diff --git a/dd-trace-core/src/test/java/datadog/trace/core/DDSpanSerializationTest.java b/dd-trace-core/src/test/java/datadog/trace/core/DDSpanSerializationTest.java index f6f8f774ecf..871433834fb 100644 --- a/dd-trace-core/src/test/java/datadog/trace/core/DDSpanSerializationTest.java +++ b/dd-trace-core/src/test/java/datadog/trace/core/DDSpanSerializationTest.java @@ -1,5 +1,6 @@ package datadog.trace.core; +import static datadog.trace.api.DDTags.SPAN_EVENTS; import static datadog.trace.api.DDTags.SPAN_LINKS; import static datadog.trace.api.config.GeneralConfig.EXPERIMENTAL_PROPAGATE_PROCESS_TAGS_ENABLED; import static datadog.trace.api.config.TracerConfig.TRACE_BAGGAGE_TAG_KEYS; @@ -18,6 +19,7 @@ import datadog.trace.api.ProcessTags; import datadog.trace.api.datastreams.NoopPathwayContext; import datadog.trace.api.sampling.PrioritySampling; +import datadog.trace.bootstrap.instrumentation.api.AgentSpanEvent; import datadog.trace.bootstrap.instrumentation.api.Baggage; import datadog.trace.bootstrap.instrumentation.api.ProfilingContextIntegration; import datadog.trace.bootstrap.instrumentation.api.SpanAttributes; @@ -33,8 +35,10 @@ import java.io.IOException; import java.nio.ByteBuffer; import java.nio.channels.Channels; +import java.util.Arrays; import java.util.Collections; import java.util.HashMap; +import java.util.List; import java.util.Map; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; @@ -48,6 +52,9 @@ @WithConfig(key = EXPERIMENTAL_PROPAGATE_PROCESS_TAGS_ENABLED, value = "false") public class DDSpanSerializationTest extends DDCoreJavaSpecification { + private static final String EXPECTED_SPAN_EVENTS_TAG = + "[{\"time_unix_nano\":1000,\"name\":\"event.one\"}," + + "{\"time_unix_nano\":2000,\"name\":\"event.two\",\"attributes\":{\"k\":\"v\"}}]"; @BeforeAll static void beforeAll() { @@ -192,29 +199,7 @@ void serializeTraceWithBaggageAndTagsCorrectlyV04( throws Exception { ListWriter writer = new ListWriter(); CoreTracer tracer = tracerBuilder().writer(writer).build(); - DDSpanContext context = - new DDSpanContext( - DDTraceId.ONE, - 1, - DDSpanId.ZERO, - null, - "fakeService", - "fakeOperation", - "fakeResource", - PrioritySampling.UNSET, - null, - baggage, - false, - null, - tags.size(), - tracer.createTraceCollector(DDTraceId.ONE), - null, - null, - NoopPathwayContext.INSTANCE, - false, - null, - injectBaggage, - true); + DDSpanContext context = createSpanContext(tracer, baggage, tags.size(), injectBaggage); context.setAllTags(tags); DDSpan span = DDSpan.create("test", 0, context, null); CaptureBuffer capture = new CaptureBuffer(); @@ -269,29 +254,7 @@ void serializeTraceWithBaggageAndTagsCorrectlyV05( throws Exception { ListWriter writer = new ListWriter(); CoreTracer tracer = tracerBuilder().writer(writer).build(); - DDSpanContext context = - new DDSpanContext( - DDTraceId.ONE, - 1, - DDSpanId.ZERO, - null, - "fakeService", - "fakeOperation", - "fakeResource", - PrioritySampling.UNSET, - null, - baggage, - false, - null, - tags.size(), - tracer.createTraceCollector(DDTraceId.ONE), - null, - null, - NoopPathwayContext.INSTANCE, - false, - null, - injectBaggage, - true); + DDSpanContext context = createSpanContext(tracer, baggage, tags.size(), injectBaggage); context.setAllTags(tags); DDSpan span = DDSpan.create("test", 0, context, null); CaptureBuffer capture = new CaptureBuffer(); @@ -342,7 +305,7 @@ void serializeTraceWithConfiguredBaggageAndTagsCorrectlyV1(String scenario, bool w3cBaggageItems.put("ignored", "ignored-value"); DDSpanContext context = - createSpanContext(tracer, baggage, Baggage.create(w3cBaggageItems), injectBaggage, true, 1); + createSpanContext(tracer, baggage, Baggage.create(w3cBaggageItems), injectBaggage, 1); context.setTag("span-tag", "span-value"); DDSpan span = DDSpan.create("test", 0, context, null); @@ -366,7 +329,7 @@ void serializeTraceWithConfiguredBaggageAndTagsCorrectlyV1(String scenario, bool @Test void serializeTraceWithSpanLinksAsStructuredLinksOnlyV1() throws Exception { CoreTracer tracer = tracerBuilder().writer(new ListWriter()).build(); - DDSpanContext context = createSpanContext(tracer, Collections.emptyMap(), null, true, true, 0); + DDSpanContext context = createSpanContext(tracer, Collections.emptyMap(), null, true, 0); DDSpan span = DDSpan.create("test", 0, context, null); Map linkAttributes = new HashMap<>(); @@ -388,7 +351,7 @@ void serializeTraceWithSpanLinksAsStructuredLinksOnlyV1() throws Exception { assertArrayEquals(V1PayloadReader.traceIdBytes(link.traceId()), actualLink.getTraceId()); assertEquals(link.spanId(), actualLink.getSpanId()); assertEquals(link.traceState(), actualLink.getTraceState()); - assertEquals((long) (link.traceFlags() & 0xFF), actualLink.getTraceFlags()); + assertEquals(link.traceFlags() & 0xFF, actualLink.getTraceFlags()); assertEquals("unit-test", actualLink.getAttributes().get("link.source")); tracer.close(); } @@ -396,27 +359,7 @@ void serializeTraceWithSpanLinksAsStructuredLinksOnlyV1() throws Exception { @Test void serializeTraceWithFlatMapTagV04() throws Exception { CoreTracer tracer = tracerBuilder().writer(new ListWriter()).build(); - DDSpanContext context = - new DDSpanContext( - DDTraceId.ONE, - 1, - DDSpanId.ZERO, - null, - "fakeService", - "fakeOperation", - "fakeResource", - PrioritySampling.UNSET, - null, - null, - false, - null, - 0, - tracer.createTraceCollector(DDTraceId.ONE), - null, - null, - NoopPathwayContext.INSTANCE, - false, - null); + DDSpanContext context = createSpanContext(tracer); context.setTag("key1", "value1"); Map nested = new HashMap<>(); nested.put("sub1", "v1"); @@ -463,27 +406,7 @@ void serializeTraceWithFlatMapTagV04() throws Exception { @Test void serializeTraceWithFlatMapTagV05() throws Exception { CoreTracer tracer = tracerBuilder().writer(new ListWriter()).build(); - DDSpanContext context = - new DDSpanContext( - DDTraceId.ONE, - 1, - DDSpanId.ZERO, - null, - "fakeService", - "fakeOperation", - "fakeResource", - PrioritySampling.UNSET, - null, - null, - false, - null, - 0, - tracer.createTraceCollector(DDTraceId.ONE), - null, - null, - NoopPathwayContext.INSTANCE, - false, - null); + DDSpanContext context = createSpanContext(tracer); context.setTag("key1", "value1"); Map nested = new HashMap<>(); nested.put("sub1", "v1"); @@ -526,6 +449,114 @@ void serializeTraceWithFlatMapTagV05() throws Exception { tracer.close(); } + @Test + void serializeTraceWithSpanEventsAsEventsTagV04() throws Exception { + CoreTracer tracer = tracerBuilder().writer(new ListWriter()).build(); + DDSpanContext context = createSpanContext(tracer); + DDSpan span = DDSpan.create("test", 0, context, null); + span.addSpanEvents(spanEvents()); + + CaptureBuffer capture = new CaptureBuffer(); + MsgPackWriter packer = new MsgPackWriter(new FlushingBuffer(1024, capture)); + packer.format(Collections.singletonList(span), new TraceMapperV0_4()); + packer.flush(); + MessageUnpacker unpacker = MessagePack.newDefaultUnpacker(new ArrayBufferInput(capture.bytes)); + int traceCount = capture.messageCount; + int spanCount = unpacker.unpackArrayHeader(); + int size = unpacker.unpackMapHeader(); + + assertEquals(1, traceCount); + assertEquals(1, spanCount); + Map unpackedMeta = null; + for (int i = 0; i < size; i++) { + String key = unpacker.unpackString(); + if ("meta".equals(key)) { + int packedSize = unpacker.unpackMapHeader(); + unpackedMeta = new HashMap<>(); + for (int j = 0; j < packedSize; j++) { + unpackedMeta.put(unpacker.unpackString(), unpacker.unpackString()); + } + } else { + unpacker.unpackValue(); + } + } + + assertNotNull(unpackedMeta); + assertEquals(EXPECTED_SPAN_EVENTS_TAG, unpackedMeta.get(SPAN_EVENTS)); + tracer.close(); + } + + @Test + void serializeTraceWithSpanEventsAsEventsTagV05() throws Exception { + CoreTracer tracer = tracerBuilder().writer(new ListWriter()).build(); + DDSpanContext context = createSpanContext(tracer); + DDSpan span = DDSpan.create("test", 0, context, null); + span.addSpanEvents(spanEvents()); + + CaptureBuffer capture = new CaptureBuffer(); + MsgPackWriter packer = new MsgPackWriter(new FlushingBuffer(1024, capture)); + TraceMapperV0_5 mapper = new TraceMapperV0_5(); + packer.format(Collections.singletonList(span), mapper); + packer.flush(); + MessageUnpacker unpacker = MessagePack.newDefaultUnpacker(new ArrayBufferInput(capture.bytes)); + int traceCount = capture.messageCount; + int spanCount = unpacker.unpackArrayHeader(); + int size = unpacker.unpackArrayHeader(); + String[] dictionary = buildDictionary(mapper); + + assertEquals(1, traceCount); + assertEquals(1, spanCount); + assertEquals(12, size); + for (int i = 0; i < 9; ++i) { + unpacker.skipValue(); + } + int packedSize = unpacker.unpackMapHeader(); + Map unpackedMeta = new HashMap<>(); + for (int j = 0; j < packedSize; j++) { + String k = dictionary[unpacker.unpackInt()]; + String v = dictionary[unpacker.unpackInt()]; + unpackedMeta.put(k, v); + } + + assertEquals(EXPECTED_SPAN_EVENTS_TAG, unpackedMeta.get(SPAN_EVENTS)); + tracer.close(); + } + + private static List spanEvents() { + return Arrays.asList( + spanEvent("{\"time_unix_nano\":1000,\"name\":\"event.one\"}"), + spanEvent("{\"time_unix_nano\":2000,\"name\":\"event.two\",\"attributes\":{\"k\":\"v\"}}")); + } + + /** + * Minimal {@link AgentSpanEvent} whose {@link AgentSpanEvent#toJson()} returns a fixed string, so + * the assembled legacy {@code events} tag is deterministic and exercises {@code + * DDSpanContext.spanEventsToTag} (the v0.x compatibility path). + */ + private static AgentSpanEvent spanEvent(String json) { + return new AgentSpanEvent() { + @Override + public long timeNanos() { + return 0; + } + + @Override + public String name() { + return "test"; + } + + @Override + public Map attributes() { + return Collections.emptyMap(); + } + + @Override + public CharSequence toJson() { + return json; + } + }; + } + private static class CaptureBuffer implements ByteBufferConsumer { private byte[] bytes; int messageCount; @@ -543,26 +574,7 @@ private DDSpanContext createSpanContext( Map baggage = new HashMap<>(); baggage.put("a-baggage", "value"); DDSpanContext ctx = - new DDSpanContext( - traceId, - spanId, - DDSpanId.ZERO, - null, - "fakeService", - "fakeOperation", - "fakeResource", - PrioritySampling.UNSET, - null, - baggage, - false, - spanType, - 1, - tracer.createTraceCollector(DDTraceId.ONE), - null, - null, - NoopPathwayContext.INSTANCE, - false, - null); + createSpanContext(tracer, traceId, spanId, spanType, baggage, null, true, 1); Map tags = new HashMap<>(); tags.put("k1", "v1"); ctx.setAllTags(tags); @@ -574,11 +586,33 @@ private DDSpanContext createSpanContext( Map baggage, Baggage w3cBaggage, boolean injectBaggage, - boolean injectLinks, + int tagsSize) { + return createSpanContext( + tracer, DDTraceId.ONE, 1, "fakeType", baggage, w3cBaggage, injectBaggage, tagsSize); + } + + private DDSpanContext createSpanContext(CoreTracer tracer) { + return createSpanContext(tracer, null, 0, true); + } + + private DDSpanContext createSpanContext( + CoreTracer tracer, Map baggage, int tagsSize, boolean injectBaggage) { + return createSpanContext( + tracer, DDTraceId.ONE, 1, null, baggage, null, injectBaggage, tagsSize); + } + + private DDSpanContext createSpanContext( + CoreTracer tracer, + DDTraceId traceId, + long spanId, + String spanType, + Map baggage, + Baggage w3cBaggage, + boolean injectBaggage, int tagsSize) { return new DDSpanContext( - DDTraceId.ONE, - 1, + traceId, + spanId, DDSpanId.ZERO, null, null, @@ -590,7 +624,7 @@ private DDSpanContext createSpanContext( baggage, w3cBaggage, false, - "fakeType", + spanType, tagsSize, tracer.createTraceCollector(DDTraceId.ONE), null, @@ -601,10 +635,10 @@ private DDSpanContext createSpanContext( null, ProfilingContextIntegration.NoOp.INSTANCE, injectBaggage, - injectLinks); + true); } - private static byte[] serializeV1Payload(DDSpan span) throws Exception { + private static byte[] serializeV1Payload(DDSpan span) { TraceMapperV1 mapper = new TraceMapperV1(); CapturePayloadBuffer capture = new CapturePayloadBuffer(mapper); MsgPackWriter packer = new MsgPackWriter(new FlushingBuffer(1024, capture)); @@ -619,9 +653,10 @@ private static String[] buildDictionary(TraceMapperV0_5 mapper) throws Exception Map encoding = TraceMapperTestBridge.getEncoding(mapper); String[] dictionary = new String[encoding.size()]; - MessageUnpacker unpacker = MessagePack.newDefaultUnpacker(dictionaryBuffer.slice()); - for (int i = 0; i < dictionary.length; ++i) { - dictionary[i] = unpacker.unpackString(); + try (MessageUnpacker unpacker = MessagePack.newDefaultUnpacker(dictionaryBuffer.slice())) { + for (int i = 0; i < dictionary.length; ++i) { + dictionary[i] = unpacker.unpackString(); + } } return dictionary; } diff --git a/dd-trace-core/src/traceAgentTest/groovy/TraceGenerator.groovy b/dd-trace-core/src/traceAgentTest/groovy/TraceGenerator.groovy index a0c81cbd0cb..b888b42a421 100644 --- a/dd-trace-core/src/traceAgentTest/groovy/TraceGenerator.groovy +++ b/dd-trace-core/src/traceAgentTest/groovy/TraceGenerator.groovy @@ -162,7 +162,7 @@ class TraceGenerator { this.measured = measured this.metadata = new Metadata(currentThread().getId(), UTF8BytesString.create(currentThread().getName()), fromMap(tags), baggage, UNSET, measured, topLevel, null, null, 0, - tagsForSerialization, emptyList()) + tagsForSerialization, emptyList(), emptyList()) } @Override diff --git a/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/AgentSpan.java b/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/AgentSpan.java index 0eb67f3ad52..59a3bcb4499 100644 --- a/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/AgentSpan.java +++ b/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/AgentSpan.java @@ -13,6 +13,7 @@ import datadog.trace.api.gateway.IGSpanInfo; import datadog.trace.api.gateway.RequestContext; import datadog.trace.api.interceptor.MutableSpan; +import java.util.List; import java.util.Map; import javax.annotation.Nonnull; import javax.annotation.Nullable; @@ -205,6 +206,8 @@ default boolean isValid() { void addLink(AgentSpanLink link); + void addSpanEvents(List events); + AgentSpan setMetaStruct(final String field, final Object value); boolean isOutbound(); diff --git a/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/AgentSpanEvent.java b/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/AgentSpanEvent.java new file mode 100644 index 00000000000..befb0359218 --- /dev/null +++ b/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/AgentSpanEvent.java @@ -0,0 +1,50 @@ +package datadog.trace.bootstrap.instrumentation.api; + +import java.util.Map; + +/** + * Describes an event that occurred during a span's lifetime (the OpenTelemetry span-event concept). + * + *

Span events are kept as structured data until serialization so the different trace payloads + * can encode them in their own representation without a JSON round-trip: + * + *

    + *
  • the V1 payload encodes them natively (span field {@code 12}) from {@link #timeNanos()}, + * {@link #name()} and {@link #attributes()}; + *
  • the legacy v0.x payloads carry them as the JSON string {@code events} tag, assembled from + * each event's {@link #toJson()}. + *
+ * + * @see AgentSpanLink for the analogous structured representation of span links. + */ +public interface AgentSpanEvent { + /** + * Gets the event timestamp. + * + * @return The event timestamp, in nanoseconds since the Unix epoch. + */ + long timeNanos(); + + /** + * Gets the event name. + * + * @return The event name. + */ + String name(); + + /** + * Gets the event attributes as typed values. Values are {@link String}, {@link Boolean}, {@link + * Long}, {@link Double}, or a {@link java.util.List} of those, mirroring the OpenTelemetry + * attribute types. + * + * @return The event attributes. + */ + Map attributes(); + + /** + * Serializes the event as a JSON string. + * + * @return The event as a JSON string. + */ + CharSequence toJson(); +} diff --git a/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/ImmutableSpan.java b/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/ImmutableSpan.java index c71f8b59023..5483769947f 100644 --- a/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/ImmutableSpan.java +++ b/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/ImmutableSpan.java @@ -3,6 +3,7 @@ import datadog.trace.api.TagMap; import datadog.trace.api.gateway.Flow.Action.RequestBlockingAction; import datadog.trace.api.interceptor.MutableSpan; +import java.util.List; import java.util.Map; /** @@ -201,6 +202,9 @@ public AgentSpan setSamplingPriority(int newPriority, int samplingMechanism) { @Override public void addLink(AgentSpanLink link) {} + @Override + public void addSpanEvents(List events) {} + @Override public AgentSpan setMetaStruct(String field, Object value) { return this;