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 6c6bfa1bc8c..3742f913880 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 @@ -1,6 +1,13 @@ package datadog.opentelemetry.shim.trace; +import static datadog.opentelemetry.shim.trace.OtelSpanEvent.EXCEPTION_MESSAGE_ATTRIBUTE_KEY; +import static datadog.opentelemetry.shim.trace.OtelSpanEvent.EXCEPTION_STACK_TRACE_ATTRIBUTE_KEY; +import static datadog.opentelemetry.shim.trace.OtelSpanEvent.EXCEPTION_TYPE_ATTRIBUTE_KEY; import static datadog.trace.api.DDTags.ANALYTICS_SAMPLE_RATE; +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; @@ -125,6 +132,20 @@ public static void applyNamingConvention(AgentSpan span) { } } + public static void setEventsAsTag(AgentSpan span, List events) { + if (events == null || events.isEmpty()) { + return; + } + span.setTag(SPAN_EVENTS, OtelSpanEvent.toTag(events)); + } + + public static void applySpanEventExceptionAttributesAsTags( + AgentSpan span, Attributes exceptionAttributes) { + span.setTag(ERROR_MSG, exceptionAttributes.get(EXCEPTION_MESSAGE_ATTRIBUTE_KEY)); + span.setTag(ERROR_TYPE, exceptionAttributes.get(EXCEPTION_TYPE_ATTRIBUTE_KEY)); + span.setTag(ERROR_STACK, exceptionAttributes.get(EXCEPTION_STACK_TRACE_ATTRIBUTE_KEY)); + } + private static String computeOperationName(AgentSpan span) { Object spanKingTag = span.getTag(SPAN_KIND); SpanKind spanKind = 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 64fb9f9a2cb..5e112d63b2b 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 @@ -2,6 +2,10 @@ 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.OtelSpanEvent.EXCEPTION_SPAN_EVENT_NAME; +import static datadog.opentelemetry.shim.trace.OtelSpanEvent.initializeExceptionAttributes; import static datadog.trace.bootstrap.instrumentation.api.AgentTracer.activateSpan; import static io.opentelemetry.api.trace.StatusCode.ERROR; import static io.opentelemetry.api.trace.StatusCode.OK; @@ -10,7 +14,6 @@ import datadog.trace.bootstrap.instrumentation.api.AgentScope; import datadog.trace.bootstrap.instrumentation.api.AgentSpan; import datadog.trace.bootstrap.instrumentation.api.AttachableWrapper; -import datadog.trace.bootstrap.instrumentation.api.ErrorPriorities; import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.api.common.Attributes; import io.opentelemetry.api.trace.Span; @@ -18,6 +21,7 @@ import io.opentelemetry.api.trace.StatusCode; import io.opentelemetry.api.trace.TraceFlags; import io.opentelemetry.api.trace.TraceState; +import java.util.ArrayList; import java.util.List; import java.util.concurrent.TimeUnit; import javax.annotation.ParametersAreNonnullByDefault; @@ -27,6 +31,8 @@ public class OtelSpan implements Span { private final AgentSpan delegate; private StatusCode statusCode; private boolean recording; + /** Span events ({@code null} until an event is added). */ + private List events; public OtelSpan(AgentSpan delegate) { this.delegate = delegate; @@ -71,13 +77,23 @@ public Span setAttribute(AttributeKey key, T value) { @Override public Span addEvent(String name, Attributes attributes) { - // Not supported + if (this.recording) { + if (this.events == null) { + this.events = new ArrayList<>(); + } + this.events.add(new OtelSpanEvent(name, attributes)); + } return this; } @Override public Span addEvent(String name, Attributes attributes, long timestamp, TimeUnit unit) { - // Not supported + if (this.recording) { + if (this.events == null) { + this.events = new ArrayList<>(); + } + this.events.add(new OtelSpanEvent(name, attributes, timestamp, unit)); + } return this; } @@ -100,8 +116,12 @@ public Span setStatus(StatusCode statusCode, String description) { @Override public Span recordException(Throwable exception, Attributes additionalAttributes) { if (this.recording) { - // Store exception as span tags as span events are not supported yet - this.delegate.addThrowable(exception, ErrorPriorities.UNSET); + if (this.events == null) { + this.events = new ArrayList<>(); + } + additionalAttributes = initializeExceptionAttributes(exception, additionalAttributes); + applySpanEventExceptionAttributesAsTags(this.delegate, additionalAttributes); + this.events.add(new OtelSpanEvent(EXCEPTION_SPAN_EVENT_NAME, additionalAttributes)); } return this; } @@ -118,6 +138,7 @@ public Span updateName(String name) { public void end() { this.recording = false; applyNamingConvention(this.delegate); + setEventsAsTag(this.delegate, this.events); this.delegate.finish(); } @@ -125,6 +146,7 @@ public void end() { public void end(long timestamp, TimeUnit unit) { this.recording = false; applyNamingConvention(this.delegate); + setEventsAsTag(this.delegate, this.events); this.delegate.finish(unit.toMicros(timestamp)); } 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 new file mode 100644 index 00000000000..0f2f3f82805 --- /dev/null +++ b/dd-java-agent/agent-otel/otel-shim/src/main/java/datadog/opentelemetry/shim/trace/OtelSpanEvent.java @@ -0,0 +1,191 @@ +package datadog.opentelemetry.shim.trace; + +import datadog.trace.api.time.SystemTimeSource; +import datadog.trace.api.time.TimeSource; +import edu.umd.cs.findbugs.annotations.NonNull; +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.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.TimeUnit; + +public class OtelSpanEvent { + public static final String EXCEPTION_SPAN_EVENT_NAME = "exception"; + public static final AttributeKey EXCEPTION_MESSAGE_ATTRIBUTE_KEY = + AttributeKey.stringKey("exception.message"); + public static final AttributeKey EXCEPTION_TYPE_ATTRIBUTE_KEY = + AttributeKey.stringKey("exception.type"); + public static final AttributeKey EXCEPTION_STACK_TRACE_ATTRIBUTE_KEY = + AttributeKey.stringKey("exception.stacktrace"); + + // TODO TimeSource instance is not retrieved from CoreTracer + private static TimeSource timeSource = SystemTimeSource.INSTANCE; + + private final String name; + private final String 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(); + } + + public OtelSpanEvent(String name, Attributes attributes, long timestamp, TimeUnit unit) { + this.name = name; + this.attributes = AttributesJsonParser.toJson(attributes); + this.timestamp = unit.toNanos(timestamp); + } + + @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()); + } + return builder.append(']').toString(); + } + + /** + * Make sure exception related attributes are presents and generates them if needed. + * + *

All exception span events get the following reserved attributes: {@link + * #EXCEPTION_MESSAGE_ATTRIBUTE_KEY}, {@link #EXCEPTION_TYPE_ATTRIBUTE_KEY} and {@link + * #EXCEPTION_STACK_TRACE_ATTRIBUTE_KEY}. If additionalAttributes contains a reserved key, the + * value in additionalAttributes is used. Else, the value is determined from the provided + * Throwable. + * + * @param exception The Throwable from which to build reserved attributes + * @param additionalAttributes The user-provided attributes + * @return An {@link Attributes} collection with exception attributes. + */ + static Attributes initializeExceptionAttributes( + Throwable exception, Attributes additionalAttributes) { + // Create an AttributesBuilder with the additionalAttributes provided + AttributesBuilder builder = additionalAttributes.toBuilder(); + // Handle exception message + String value = additionalAttributes.get(EXCEPTION_MESSAGE_ATTRIBUTE_KEY); + if (value == null) { + value = exception.getMessage(); + builder.put(EXCEPTION_MESSAGE_ATTRIBUTE_KEY, value); + } + // Handle exception type + value = additionalAttributes.get(EXCEPTION_TYPE_ATTRIBUTE_KEY); + if (value == null) { + value = exception.getClass().getName(); + builder.put(EXCEPTION_TYPE_ATTRIBUTE_KEY, value); + } + // Handle exception stacktrace + value = additionalAttributes.get(EXCEPTION_STACK_TRACE_ATTRIBUTE_KEY); + if (value == null) { + value = stringifyErrorStack(exception); + builder.put(EXCEPTION_STACK_TRACE_ATTRIBUTE_KEY, value); + } + return builder.build(); + } + + static String stringifyErrorStack(Throwable error) { + final StringWriter errorString = new StringWriter(); + error.printStackTrace(new PrintWriter(errorString)); + return errorString.toString(); + } + + /** Helper class for JSON-encoding {@link OtelSpanEvent} {@link #attributes}. */ + public static class AttributesJsonParser { + public static String toJson(Attributes attributes) { + if (attributes == null || attributes.isEmpty()) { + return ""; + } + StringBuilder jsonBuilder = new StringBuilder(); + jsonBuilder.append('{'); + + Set, Object>> entrySet = attributes.asMap().entrySet(); + + for (Map.Entry, Object> entry : entrySet) { + if (jsonBuilder.length() > 1) { + jsonBuilder.append(','); + } + // AttributeKey type has method `getKey()` that "stringifies" the key + String key = entry.getKey().getKey(); + Object value = entry.getValue(); + // Escape key and append it + jsonBuilder.append('"').append(escapeJson(key)).append("\":"); + // Append value to jsonBuilder + appendValue(value, jsonBuilder); + } + jsonBuilder.append('}'); + return jsonBuilder.toString(); + } + /** + * Recursively adds the value of an {@link Attributes} to the active StringBuilder in JSON + * format, depending on the value's type. + * + * @param value The value to append + * @param jsonBuilder The active {@link StringBuilder} + */ + private static void appendValue(Object value, StringBuilder jsonBuilder) { + // Append value based on its type + if (value instanceof String) { + jsonBuilder.append('"').append(escapeJson((String) value)).append('"'); + } else if (value instanceof List) { + jsonBuilder.append('['); + List valArray = (List) value; + for (int i = 0; i < valArray.size(); i++) { + if (i > 0) { + jsonBuilder.append(','); + } + appendValue(valArray.get(i), jsonBuilder); + } + jsonBuilder.append(']'); + } else if (value instanceof Number || value instanceof Boolean) { + jsonBuilder.append(value); + } else { + jsonBuilder.append("null"); // null for unsupported types + } + } + + private static String escapeJson(String value) { + return value + .replace("\\", "\\\\") + .replace("\"", "\\\"") + .replace("\b", "\\b") + .replace("\f", "\\f") + .replace("\n", "\\n") + .replace("\r", "\\r") + .replace("\t", "\\t"); + } + } + + 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='" + + this.attributes + + "'}"; + } +} diff --git a/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.4/src/main/java/datadog/trace/instrumentation/opentelemetry14/OpenTelemetryInstrumentation.java b/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.4/src/main/java/datadog/trace/instrumentation/opentelemetry14/OpenTelemetryInstrumentation.java index fa049fb6bc2..63883a317f5 100644 --- a/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.4/src/main/java/datadog/trace/instrumentation/opentelemetry14/OpenTelemetryInstrumentation.java +++ b/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.4/src/main/java/datadog/trace/instrumentation/opentelemetry14/OpenTelemetryInstrumentation.java @@ -73,10 +73,12 @@ public String[] helperClassNames() { "datadog.opentelemetry.shim.trace.OtelSpanBuilder", "datadog.opentelemetry.shim.trace.OtelSpanBuilder$1", "datadog.opentelemetry.shim.trace.OtelSpanContext", + "datadog.opentelemetry.shim.trace.OtelSpanEvent$AttributesJsonParser", "datadog.opentelemetry.shim.trace.OtelSpanLink", "datadog.opentelemetry.shim.trace.OtelTracer", "datadog.opentelemetry.shim.trace.OtelTracerBuilder", "datadog.opentelemetry.shim.trace.OtelTracerProvider", + "datadog.opentelemetry.shim.trace.OtelSpanEvent", }; } diff --git a/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.4/src/test/groovy/OpenTelemetry14Test.groovy b/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.4/src/test/groovy/OpenTelemetry14Test.groovy index 7a0c4d7ee91..532b2b44fc7 100644 --- a/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.4/src/test/groovy/OpenTelemetry14Test.groovy +++ b/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.4/src/test/groovy/OpenTelemetry14Test.groovy @@ -1,7 +1,9 @@ +import datadog.opentelemetry.shim.trace.OtelSpanEvent import datadog.trace.agent.test.AgentTestRunner import datadog.trace.api.DDSpanId import datadog.trace.api.DDTags import datadog.trace.api.DDTraceId +import datadog.trace.api.time.ControllableTimeSource import io.opentelemetry.api.GlobalOpenTelemetry import io.opentelemetry.api.common.AttributeKey import io.opentelemetry.api.common.Attributes @@ -14,15 +16,16 @@ import opentelemetry14.context.propagation.TextMap import org.skyscreamer.jsonassert.JSONAssert import spock.lang.Subject -import java.security.InvalidParameterException - +import static datadog.opentelemetry.shim.trace.OtelConventions.SPAN_KIND_INTERNAL 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 import static datadog.trace.bootstrap.instrumentation.api.Tags.SPAN_KIND_PRODUCER import static datadog.trace.bootstrap.instrumentation.api.Tags.SPAN_KIND_SERVER -import static datadog.opentelemetry.shim.trace.OtelConventions.SPAN_KIND_INTERNAL import static io.opentelemetry.api.trace.SpanKind.CLIENT import static io.opentelemetry.api.trace.SpanKind.CONSUMER import static io.opentelemetry.api.trace.SpanKind.INTERNAL @@ -31,8 +34,13 @@ import static io.opentelemetry.api.trace.SpanKind.SERVER import static io.opentelemetry.api.trace.StatusCode.ERROR import static io.opentelemetry.api.trace.StatusCode.OK import static io.opentelemetry.api.trace.StatusCode.UNSET +import static java.util.concurrent.TimeUnit.MILLISECONDS +import static java.util.concurrent.TimeUnit.NANOSECONDS class OpenTelemetry14Test extends AgentTestRunner { + static final TIME_MILLIS = 1723220824705 + static final TIME_NANO = TIME_MILLIS * 1_000_000L + @Subject def tracer = GlobalOpenTelemetry.get().tracerProvider.get("some-instrumentation") @@ -175,25 +183,105 @@ class OpenTelemetry14Test extends AgentTestRunner { } } - def "test non-supported features do not crash"() { + def "test add event"() { setup: def builder = tracer.spanBuilder("some-name") - def anotherSpan = tracer.spanBuilder("another-name").startSpan() - anotherSpan.end() + def timeSource = new ControllableTimeSource() + timeSource.set(1000) + OtelSpanEvent.setTimeSource(timeSource) when: - // Adding event is not supported def result = builder.startSpan() - result.addEvent("some-event") + result.addEvent("event") result.end() then: - assertTraces(2) { + def expectedEventTag = """ + [ + { "time_unix_nano": ${timeSource.getCurrentTimeNanos()}, + "name": "event" + } + ]""" + assertTraces(1) { trace(1) { - span {} + span { + tags { + defaultTags() + "$SPAN_KIND" "$SPAN_KIND_INTERNAL" + tag("$SPAN_EVENTS", { JSONAssert.assertEquals(expectedEventTag, it as String, false); return true }) + } + } } + } + } + + def "test add single event"() { + setup: + def builder = tracer.spanBuilder("some-name") + def expectedEventTag = """ + [ + { "time_unix_nano": ${unit.toNanos(timestamp)}, + "name": "${name}" + ${expectedAttributes == null ? "" : ", attributes: " + expectedAttributes} + } + ]""" + + when: + def result = builder.startSpan() + result.addEvent(name, attributes, timestamp, unit) + result.end() + + then: + + assertTraces(1) { trace(1) { - span {} + span { + tags { + defaultTags() + "$SPAN_KIND" "$SPAN_KIND_INTERNAL" + tag("$SPAN_EVENTS", { JSONAssert.assertEquals(expectedEventTag, it as String, false); return true }) + } + } + } + } + + where: + name | timestamp | unit | attributes | expectedAttributes + "event1" | TIME_MILLIS | MILLISECONDS | Attributes.empty() | null + "event2" | TIME_NANO | NANOSECONDS | Attributes.builder().put("string-key", "string-value").put("long-key", 123456789L).put("double-key", 1234.5678).put("boolean-key-true", true).put("boolean-key-false", false).build() | '{"string-key": "string-value", "long-key": 123456789, "double-key": 1234.5678, "boolean-key-true": true, "boolean-key-false": false }' + "event3" | TIME_NANO | NANOSECONDS | Attributes.builder().put("string-key-array", "string-value1", "string-value2", "string-value3").put("long-key-array", 123456L, 1234567L, 12345678L).put("double-key-array", 1234.5D, 1234.56D, 1234.567D).put("boolean-key-array", true, false, true).build() | '{"string-key-array": [ "string-value1", "string-value2", "string-value3" ], "long-key-array": [ 123456, 1234567, 12345678 ], "double-key-array": [ 1234.5, 1234.56, 1234.567], "boolean-key-array": [true, false, true] }' + } + + def "test add multiple span events"() { + setup: + def builder = tracer.spanBuilder("some-name") + + when: + def result = builder.startSpan() + result.addEvent("event1", null, TIME_NANO, NANOSECONDS) + result.addEvent("event2", Attributes.builder().put("string-key", "string-value").build(), TIME_NANO, NANOSECONDS) + result.end() + + then: + def expectedEventTag = """ + [ + { "time_unix_nano": ${TIME_NANO}, + "name": "event1" + }, + { "time_unix_nano": ${TIME_NANO}, + "name": "event2", + "attributes": {"string-key": "string-value"} + } + ]""" + assertTraces(1) { + trace(1) { + span { + tags { + defaultTags() + "$SPAN_KIND" "$SPAN_KIND_INTERNAL" + tag("$SPAN_EVENTS", { JSONAssert.assertEquals(expectedEventTag, it as String, false); return true }) + } + } } } } @@ -304,7 +392,7 @@ class OpenTelemetry14Test extends AgentTestRunner { where: attributes | expectedAttributes Attributes.empty() | null - Attributes.builder().put("string-key", "string-value").put("long-key", 123456789L).put("double-key", 1234.5678D).put("boolean-key-true", true).put("boolean-key-false", false).build() | '{ string-key: "string-value", long-key: "123456789", double-key: "1234.5678", boolean-key-true: "true", boolean-key-false: "false" }' + Attributes.builder().put("string-key", "string-value").put("long-key", 123456789L).put("double-key", 1234.5678).put("boolean-key-true", true).put("boolean-key-false", false).build() | '{ string-key: "string-value", long-key: "123456789", double-key: "1234.5678", boolean-key-true: "true", boolean-key-false: "false" }' Attributes.builder().put("string-key-array", "string-value1", "string-value2", "string-value3").put("long-key-array", 123456L, 1234567L, 12345678L).put("double-key-array", 1234.5D, 1234.56D, 1234.567D).put("boolean-key-array", true, false, true).build() | '{ string-key-array.0: "string-value1", string-key-array.1: "string-value2", string-key-array.2: "string-value3", long-key-array.0: "123456", long-key-array.1: "1234567", long-key-array.2: "12345678", double-key-array.0: "1234.5", double-key-array.1: "1234.56", double-key-array.2: "1234.567", boolean-key-array.0: "true", boolean-key-array.1: "false", boolean-key-array.2: "true" }' } @@ -566,28 +654,33 @@ class OpenTelemetry14Test extends AgentTestRunner { def "test span record exception"() { setup: def result = tracer.spanBuilder("some-name").startSpan() - def message = "input can't be null" - def exception = new InvalidParameterException(message) - - expect: - result.delegate.getTag(ERROR_MSG) == null - result.delegate.getTag(DDTags.ERROR_TYPE) == null - result.delegate.getTag(DDTags.ERROR_STACK) == null - !result.delegate.isError() - - when: - result.recordException(exception) - - then: - result.delegate.getTag(ERROR_MSG) == message - result.delegate.getTag(DDTags.ERROR_TYPE) == InvalidParameterException.name - result.delegate.getTag(DDTags.ERROR_STACK) != null - !result.delegate.isError() + def timeSource = new ControllableTimeSource() + timeSource.set(1000) + OtelSpanEvent.setTimeSource(timeSource) + def errorMessage = overridenMessage?:exception.getMessage() + def errorType = overridenType?:exception.getClass().getName() + def errorStackTrace = overridenStacktrace?:OtelSpanEvent.stringifyErrorStack(exception) + def expectedAttributes = + """{ + "exception.message": "${errorMessage}", + "exception.type": "${errorType}", + "exception.stacktrace": "${errorStackTrace}" + ${extraJson?:''} + }""" when: + result.recordException(exception, attributes) result.end() then: + def expectedEventTag = """ + [ + { "time_unix_nano": ${timeSource.getCurrentTimeNanos()}, + "name": "exception", + "attributes": ${expectedAttributes} + } + ]""" + assertTraces(1) { trace(1) { span { @@ -598,11 +691,42 @@ class OpenTelemetry14Test extends AgentTestRunner { tags { defaultTags() "$SPAN_KIND" "$SPAN_KIND_INTERNAL" - errorTags(exception) + tag("events", { JSONAssert.assertEquals(expectedEventTag, it as String, false); return true }) + tag(ERROR_MSG, errorMessage) + tag(ERROR_TYPE, errorType) + tag(ERROR_STACK, errorStackTrace) } } } } + + where: + exception | attributes | overridenMessage | overridenType | overridenStacktrace | extraJson + new NullPointerException("Null pointer") | Attributes.empty() | null | null | null | null + new NumberFormatException("Number format exception") | Attributes.builder().put("exception.message", "something-else").build() | "something-else" | null | null | null + new NullPointerException("Null pointer") | Attributes.builder().put("exception.type", "CustomType").build() | null | "CustomType" | null | null + new NullPointerException("Null pointer") | Attributes.builder().put("exception.stacktrace", "CustomTrace").build() | null | null | "CustomTrace" | null + new NullPointerException("Null pointer") | Attributes.builder().put("key", "value").build() | null | null | null | ', "key": "value"' + } + + def "test span error meta on record multiple exceptions"() { + // Span's "error" tags should reflect the last recorded exception + setup: + def result = tracer.spanBuilder("some-name").startSpan() + def exception1 = new NullPointerException("Null pointer") + def exception2 = new NumberFormatException("Number format exception") + def expectedStackTrace = OtelSpanEvent.stringifyErrorStack(exception2) + + when: + result.recordException(exception1) + result.recordException(exception2) + result.end() + + then: + result.delegate.getTag(ERROR_MSG) == exception2.getMessage() + result.delegate.getTag(ERROR_TYPE) == exception2.getClass().getName() + result.delegate.getTag(ERROR_STACK) == expectedStackTrace + !result.delegate.isError() } def "test span name update"() { @@ -648,6 +772,8 @@ class OpenTelemetry14Test extends AgentTestRunner { result.updateName("other-name") result.setAttribute("string", "other-value") result.setStatus(OK) + result.addEvent("event") + result.recordException(new Throwable()) then: assertTraces(1) { diff --git a/dd-trace-api/src/main/java/datadog/trace/api/DDTags.java b/dd-trace-api/src/main/java/datadog/trace/api/DDTags.java index f5b9d2f2ad7..78e295ef0cc 100644 --- a/dd-trace-api/src/main/java/datadog/trace/api/DDTags.java +++ b/dd-trace-api/src/main/java/datadog/trace/api/DDTags.java @@ -64,6 +64,7 @@ public class DDTags { public static final String LANGUAGE_TAG_VALUE = "jvm"; public static final String ORIGIN_KEY = "_dd.origin"; public static final String SPAN_LINKS = "_dd.span_links"; + public static final String SPAN_EVENTS = "events"; public static final String LIBRARY_VERSION_TAG_KEY = "library_version"; public static final String CI_ENV_VARS = "_dd.ci.env_vars"; public static final String CI_ITR_TESTS_SKIPPED = "_dd.ci.itr.tests_skipped";