From 4f318a7ce162a63630e2b60f2c97feb9018b3326 Mon Sep 17 00:00:00 2001 From: Bruce Bujon Date: Fri, 9 Oct 2026 12:09:07 +0200 Subject: [PATCH] feat(test): Improve span event testing --- .../test/assertions/SpanEventMatcher.java | 75 +++++ .../agent/test/assertions/SpanMatcher.java | 40 +++ .../agent/test/assertions/TagsMatcher.java | 2 + .../opentelemetry-1.4/build.gradle | 1 - .../opentelemetry-1.4/gradle.lockfile | 4 +- .../opentelemetry14/OpenTelemetry14Test.java | 277 +++++++----------- 6 files changed, 232 insertions(+), 167 deletions(-) create mode 100644 dd-java-agent/instrumentation-testing/src/main/java/datadog/trace/agent/test/assertions/SpanEventMatcher.java diff --git a/dd-java-agent/instrumentation-testing/src/main/java/datadog/trace/agent/test/assertions/SpanEventMatcher.java b/dd-java-agent/instrumentation-testing/src/main/java/datadog/trace/agent/test/assertions/SpanEventMatcher.java new file mode 100644 index 00000000000..5c68c425982 --- /dev/null +++ b/dd-java-agent/instrumentation-testing/src/main/java/datadog/trace/agent/test/assertions/SpanEventMatcher.java @@ -0,0 +1,75 @@ +package datadog.trace.agent.test.assertions; + +import static datadog.trace.test.junit.utils.assertions.Matchers.assertValue; +import static datadog.trace.test.junit.utils.assertions.Matchers.is; +import static datadog.trace.test.junit.utils.assertions.Matchers.validates; +import static java.util.Collections.emptyMap; + +import datadog.trace.core.DDSpan; +import datadog.trace.core.DDSpanEvent; +import datadog.trace.test.junit.utils.assertions.Matcher; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +/** + * Provides matchers for span events based on their properties such as name, time, and attributes. + */ +public final class SpanEventMatcher { + private final Matcher nameMatcher; + private Matcher timeMatcher; + private Matcher> attributesMatcher; + + private SpanEventMatcher(Matcher nameMatcher) { + this.nameMatcher = nameMatcher; + this.attributesMatcher = is(emptyMap()); + } + + /** + * Creates a {@code SpanEventMatcher} that matches a span event with the given name. By default, + * the event must have no attribute and its time must be within the span start and end times. + * + * @param name The span event name to match against. + * @return A {@code SpanEventMatcher} that matches a span event with the given name. + */ + public static SpanEventMatcher event(String name) { + return new SpanEventMatcher(is(name)); + } + + /** + * Sets the event time to match against. + * + * @param timestamp The event time to match against. + * @param unit The event time unit. + * @return The updated {@code SpanEventMatcher} instance with the new time constraint. + */ + public SpanEventMatcher time(long timestamp, TimeUnit unit) { + this.timeMatcher = is(unit.toNanos(timestamp)); + return this; + } + + /** + * Sets the event attributes to match against. + * + * @param attributes The event attributes to match against, with values being {@link String}, + * {@link Boolean}, {@link Long}, {@link Double}, or a {@link List} of them. + * @return The updated {@code SpanEventMatcher} instance with the new attributes constraint. + */ + public SpanEventMatcher attributes(Map attributes) { + this.attributesMatcher = is(attributes); + return this; + } + + void assertEvent(DDSpan span, DDSpanEvent event) { + Matcher timeMatcher = this.timeMatcher; + if (timeMatcher == null) { + long startTime = span.getStartTime(); + long endTime = startTime + span.getDurationNano(); + timeMatcher = validates(time -> startTime <= time && time <= endTime); + } + // Assert event values + assertValue(this.nameMatcher, event.name(), "Unexpected event name"); + assertValue(timeMatcher, event.timeUnixNano(), "Unexpected event time"); + assertValue(this.attributesMatcher, event.attributes(), "Unexpected event attributes"); + } +} diff --git a/dd-java-agent/instrumentation-testing/src/main/java/datadog/trace/agent/test/assertions/SpanMatcher.java b/dd-java-agent/instrumentation-testing/src/main/java/datadog/trace/agent/test/assertions/SpanMatcher.java index f74168dd7f0..38827c3524d 100644 --- a/dd-java-agent/instrumentation-testing/src/main/java/datadog/trace/agent/test/assertions/SpanMatcher.java +++ b/dd-java-agent/instrumentation-testing/src/main/java/datadog/trace/agent/test/assertions/SpanMatcher.java @@ -16,6 +16,7 @@ import datadog.trace.api.TagMap; import datadog.trace.bootstrap.instrumentation.api.AgentSpanLink; import datadog.trace.core.DDSpan; +import datadog.trace.core.DDSpanEvent; import datadog.trace.test.junit.utils.assertions.Any; import datadog.trace.test.junit.utils.assertions.IsNull; import datadog.trace.test.junit.utils.assertions.Matcher; @@ -53,6 +54,7 @@ *
  • span top-level status with {@link #topLevel()} and {@link #topLevel(boolean)} *
  • span tags with {@link #tags(TagsMatcher...)} *
  • span links with {@link #links(SpanLinkMatcher...)} + *
  • span events with {@link #events(SpanEventMatcher...)} * */ public final class SpanMatcher { @@ -70,6 +72,7 @@ public final class SpanMatcher { private Matcher topLevelMatcher; private TagsMatcher[] tagMatchers; private SpanLinkMatcher[] linkMatchers; + private SpanEventMatcher[] eventMatchers; private static final Matcher CHILD_OF_PREVIOUS_MATCHER = is(0L); @@ -372,6 +375,19 @@ public SpanMatcher links(SpanLinkMatcher... matchers) { return this; } + /** + * Checks the span events structure. + * + * @param matchers The {@link SpanEventMatcher} to verify the span events structure, one per + * event. + * @return The current {@link SpanMatcher} instance updated with the specified span event + * constraints. + */ + public SpanMatcher events(SpanEventMatcher... matchers) { + this.eventMatchers = matchers; + return this; + } + void assertSpan(List trace, int spanIndex) { DDSpan span = trace.get(spanIndex); // Apply parent span index @@ -400,6 +416,7 @@ else if (this.parentIdMatcher == CHILD_OF_PREVIOUS_MATCHER) { assertValue(this.topLevelMatcher, span.isTopLevel(), "Unexpected top-level status"); assertSpanTags(span.getTags()); assertSpanLinks(spanLinks(span)); + assertSpanEvents(span); } private void assertSpanTags(TagMap tags) { @@ -461,4 +478,27 @@ private void assertSpanLinks(List links) { linkMatcher.assertLink(link); } } + + /* + * Right now, it's expecting to have as many matchers as events, and in the same order. + */ + private void assertSpanEvents(DDSpan span) { + // Check if events should be asserted at all + if (this.eventMatchers == null) { + return; + } + List events = span.getEvents(); + int eventCount = events.size(); + int expectedEventCount = this.eventMatchers.length; + if (eventCount != expectedEventCount) { + throw assertionFailure() + .message("Unexpected span event count") + .expected(expectedEventCount) + .actual(eventCount) + .build(); + } + for (int i = 0; i < expectedEventCount; i++) { + this.eventMatchers[i].assertEvent(span, events.get(i)); + } + } } diff --git a/dd-java-agent/instrumentation-testing/src/main/java/datadog/trace/agent/test/assertions/TagsMatcher.java b/dd-java-agent/instrumentation-testing/src/main/java/datadog/trace/agent/test/assertions/TagsMatcher.java index 7fe7cd0645e..0a317be454a 100644 --- a/dd-java-agent/instrumentation-testing/src/main/java/datadog/trace/agent/test/assertions/TagsMatcher.java +++ b/dd-java-agent/instrumentation-testing/src/main/java/datadog/trace/agent/test/assertions/TagsMatcher.java @@ -15,6 +15,7 @@ import static datadog.trace.api.DDTags.REQUIRED_CODE_ORIGIN_TAGS; import static datadog.trace.api.DDTags.RUNTIME_ID_TAG; import static datadog.trace.api.DDTags.SCHEMA_VERSION_TAG_KEY; +import static datadog.trace.api.DDTags.SPAN_EVENTS; import static datadog.trace.api.DDTags.SPAN_LINKS; import static datadog.trace.api.DDTags.THREAD_ID; import static datadog.trace.api.DDTags.THREAD_NAME; @@ -56,6 +57,7 @@ public static TagsMatcher defaultTags() { tagMatchers.put(DJM_ENABLED, any()); tagMatchers.put(PARENT_ID, any()); tagMatchers.put(SPAN_LINKS, any()); // this is checked by LinksAsserter + tagMatchers.put(SPAN_EVENTS, any()); // this is checked by SpanEventMatcher tagMatchers.put(DD_INTEGRATION, any()); tagMatchers.put(TRACER_HOST, any()); diff --git a/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.4/build.gradle b/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.4/build.gradle index 88837de73ef..b2c4f465254 100644 --- a/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.4/build.gradle +++ b/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.4/build.gradle @@ -24,6 +24,5 @@ dependencies { testImplementation project(path: ':dd-java-agent:agent-otel:otel-bootstrap', configuration: 'shadow') testImplementation group: 'io.opentelemetry', name: 'opentelemetry-api', version: openTelemetryVersion - testImplementation group: 'org.skyscreamer', name: 'jsonassert', version: '1.5.1' latestDepTestImplementation group: 'io.opentelemetry', name: 'opentelemetry-api', version: '1+' } diff --git a/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.4/gradle.lockfile b/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.4/gradle.lockfile index 8517260f017..f76a8cbf39b 100644 --- a/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.4/gradle.lockfile +++ b/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.4/gradle.lockfile @@ -47,7 +47,6 @@ com.squareup.okhttp3:logging-interceptor:3.12.12=latestDepTestCompileClasspath,l com.squareup.okhttp3:okhttp:3.12.12=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath com.squareup.okio:okio:1.17.5=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath com.thoughtworks.qdox:qdox:1.12.1=codenarc -com.vaadin.external.google:android-json:0.0.20131108.vaadin1=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath commons-fileupload:commons-fileupload:1.5=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath commons-io:commons-io:2.11.0=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath commons-io:commons-io:2.21.0=spotbugs @@ -74,7 +73,7 @@ org.apache.commons:commons-lang3:3.20.0=spotbugs org.apache.commons:commons-text:1.15.0=spotbugs org.apache.logging.log4j:log4j-api:2.26.1=spotbugs org.apache.logging.log4j:log4j-core:2.26.1=spotbugs -org.apiguardian:apiguardian-api:1.1.2=latestDepTestCompileClasspath,testCompileClasspath +org.apiguardian:apiguardian-api:1.1.2=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath org.checkerframework:checker-qual:3.33.0=annotationProcessor,latestDepTestAnnotationProcessor,testAnnotationProcessor org.codehaus.groovy:groovy-ant:3.0.23=codenarc org.codehaus.groovy:groovy-docgenerator:3.0.23=codenarc @@ -114,7 +113,6 @@ org.ow2.asm:asm-tree:9.10.1=latestDepTestRuntimeClasspath,spotbugs,testRuntimeCl org.ow2.asm:asm-util:9.10.1=spotbugs org.ow2.asm:asm-util:9.7.1=latestDepTestRuntimeClasspath,testRuntimeClasspath org.ow2.asm:asm:9.10.1=buildTimeInstrumentationPlugin,compileClasspath,latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,muzzleTooling,runtimeClasspath,spotbugs,testCompileClasspath,testRuntimeClasspath -org.skyscreamer:jsonassert:1.5.1=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath org.slf4j:jcl-over-slf4j:1.7.30=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath org.slf4j:jul-to-slf4j:1.7.30=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath org.slf4j:log4j-over-slf4j:1.7.30=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath diff --git a/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.4/src/test/java/opentelemetry14/OpenTelemetry14Test.java b/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.4/src/test/java/opentelemetry14/OpenTelemetry14Test.java index 21f55c1917e..c7b91fc2da7 100644 --- a/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.4/src/test/java/opentelemetry14/OpenTelemetry14Test.java +++ b/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.4/src/test/java/opentelemetry14/OpenTelemetry14Test.java @@ -1,6 +1,8 @@ package opentelemetry14; import static datadog.opentelemetry.shim.trace.OtelConventionsTestHelper.stringifyErrorStack; +import static datadog.trace.agent.test.assertions.SpanEventMatcher.event; +import static datadog.trace.agent.test.assertions.SpanLinkMatcher.to; import static datadog.trace.agent.test.assertions.SpanMatcher.span; import static datadog.trace.agent.test.assertions.TagsMatcher.defaultTags; import static datadog.trace.agent.test.assertions.TagsMatcher.tag; @@ -8,14 +10,12 @@ 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.api.DDTags.SPAN_LINKS; +import static datadog.trace.bootstrap.instrumentation.api.AgentSpanLink.SAMPLED_FLAG; 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.trace.test.junit.utils.assertions.Matchers.any; import static datadog.trace.test.junit.utils.assertions.Matchers.is; import static io.opentelemetry.api.common.AttributeKey.booleanArrayKey; import static io.opentelemetry.api.common.AttributeKey.doubleArrayKey; @@ -30,9 +30,10 @@ 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.lang.String.join; import static java.util.Arrays.asList; import static java.util.Collections.emptyList; +import static java.util.Collections.emptyMap; +import static java.util.Collections.singletonMap; import static java.util.concurrent.TimeUnit.MILLISECONDS; import static java.util.concurrent.TimeUnit.NANOSECONDS; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -42,15 +43,14 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.params.provider.Arguments.arguments; +import datadog.trace.agent.test.assertions.SpanLinkMatcher; import datadog.trace.agent.test.assertions.TagsMatcher; import datadog.trace.api.DDSpanId; import datadog.trace.api.DDTags; import datadog.trace.api.DDTraceId; +import datadog.trace.bootstrap.instrumentation.api.SpanAttributes; import datadog.trace.bootstrap.instrumentation.api.WithAgentSpan; import datadog.trace.core.DDSpan; -import datadog.trace.core.DDSpanEvent; -import datadog.trace.test.junit.utils.assertions.Matcher; -import datadog.trace.test.junit.utils.assertions.Matchers; import io.opentelemetry.api.GlobalOpenTelemetry; import io.opentelemetry.api.common.Attributes; import io.opentelemetry.api.trace.Span; @@ -64,19 +64,17 @@ import io.opentelemetry.context.propagation.TextMapPropagator; import java.util.ArrayList; import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.stream.Stream; import opentelemetry14.context.propagation.TextMap; -import org.json.JSONArray; -import org.json.JSONException; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; -import org.skyscreamer.jsonassert.JSONAssert; class OpenTelemetry14Test extends AbstractOpenTelemetry14Test { private static final String SPAN_KIND_INTERNAL = "internal"; @@ -176,25 +174,22 @@ void testAddEvent() { result.addEvent("event"); result.end(); - String expectedEventTag = - "[" - + "{ \"time_unix_nano\": " - + eventTimeWithinSpan(result, 0) - + ", \"name\": \"event\" }" - + "]"; assertTraces( trace( span() - .tags( - defaultTags(), - tag(SPAN_KIND, is(SPAN_KIND_INTERNAL)), - tag(SPAN_EVENTS, isJson(expectedEventTag))))); + .tags(defaultTags(), tag(SPAN_KIND, is(SPAN_KIND_INTERNAL))) + .events(event("event")))); } static Stream testAddSingleEventArguments() { return Stream.of( arguments( - "empty attributes", "event1", TIME_MILLIS, MILLISECONDS, Attributes.empty(), null), + "empty attributes", + "event1", + TIME_MILLIS, + MILLISECONDS, + Attributes.empty(), + emptyMap()), arguments( "scalar attributes", "event2", @@ -207,7 +202,17 @@ static Stream testAddSingleEventArguments() { .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( + "string-key", + "string-value", + "long-key", + 123456789L, + "double-key", + 1234.5678, + "boolean-key-true", + true, + "boolean-key-false", + false)), arguments( "array attributes", "event3", @@ -219,7 +224,11 @@ static Stream testAddSingleEventArguments() { .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] }")); + attributes( + "string-key-array", asList("string-value1", "string-value2", "string-value3"), + "long-key-array", asList(123456L, 1234567L, 12345678L), + "double-key-array", asList(1234.5D, 1234.56D, 1234.567D), + "boolean-key-array", asList(true, false, true)))); } @ParameterizedTest(name = "[{index}] {0}") @@ -230,18 +239,8 @@ void testAddSingleEvent( long timestamp, TimeUnit unit, Attributes attributes, - String expectedAttributes) { + Map expectedAttributes) { SpanBuilder builder = this.otelTracer.spanBuilder("some-name"); - String expectedEventTag = - "[" - + "{ \"time_unix_nano\": " - + unit.toNanos(timestamp) - + ", \"name\": \"" - + name - + "\"" - + (expectedAttributes == null ? "" : ", \"attributes\": " + expectedAttributes) - + " }" - + "]"; Span result = builder.startSpan(); result.addEvent(name, attributes, timestamp, unit); @@ -250,10 +249,8 @@ void testAddSingleEvent( assertTraces( trace( span() - .tags( - defaultTags(), - tag(SPAN_KIND, is(SPAN_KIND_INTERNAL)), - tag(SPAN_EVENTS, isJson(expectedEventTag))))); + .tags(defaultTags(), tag(SPAN_KIND, is(SPAN_KIND_INTERNAL))) + .events(event(name).time(timestamp, unit).attributes(expectedAttributes)))); } @Test @@ -269,22 +266,15 @@ void testAddMultipleSpanEvents() { NANOSECONDS); result.end(); - String expectedEventTag = - "[" - + "{ \"time_unix_nano\": " - + TIME_NANO - + ", \"name\": \"event1\" }," - + "{ \"time_unix_nano\": " - + TIME_NANO - + ", \"name\": \"event2\", \"attributes\": {\"string-key\": \"string-value\"} }" - + "]"; assertTraces( trace( span() - .tags( - defaultTags(), - tag(SPAN_KIND, is(SPAN_KIND_INTERNAL)), - tag(SPAN_EVENTS, isJson(expectedEventTag))))); + .tags(defaultTags(), tag(SPAN_KIND, is(SPAN_KIND_INTERNAL))) + .events( + event("event1").time(TIME_NANO, NANOSECONDS), + event("event2") + .time(TIME_NANO, NANOSECONDS) + .attributes(singletonMap("string-key", "string-value"))))); } @Test @@ -324,9 +314,7 @@ void testConcurrentAddEvents() throws Exception { writer.waitForTraces(1); List firstTrace = writer.firstTrace(); assertEquals(1, firstTrace.size()); - String eventsTag = firstTrace.get(0).getTags().getString(SPAN_EVENTS); - JSONArray events = new JSONArray(eventsTag); - assertEquals(threadCount * eventsPerThread, events.length()); + assertEquals(threadCount * eventsPerThread, firstTrace.get(0).getEvents().size()); } @Test @@ -335,15 +323,6 @@ void testSimpleSpanLinks() { String spanId = "fedcba0987654321"; TraceState traceState = TraceState.builder().put("string-key", "string-value").build(); - String expectedLinksTag = - "[" - + "{ \"trace_id\": \"" - + traceId - + "\", \"span_id\": \"" - + spanId - + "\", \"flags\": 1, \"tracestate\": \"string-key=string-value\" }" - + "]"; - Span span = this.otelTracer .spanBuilder("some-name") @@ -355,46 +334,40 @@ void testSimpleSpanLinks() { assertTraces( trace( span() - .tags( - defaultTags(), - tag(SPAN_KIND, is(SPAN_KIND_INTERNAL)), - tag(SPAN_LINKS, isJson(expectedLinksTag))))); + .tags(defaultTags(), tag(SPAN_KIND, is(SPAN_KIND_INTERNAL))) + .links( + to(DDTraceId.fromHex(traceId), DDSpanId.fromHex(spanId)) + .traceFlags(SAMPLED_FLAG) + .traceState("string-key=string-value")))); } @Test void testMultipleSpanLinks() { SpanBuilder spanBuilder = this.otelTracer.spanBuilder("some-name"); - List expectedLinks = new ArrayList<>(); + List expectedLinks = new ArrayList<>(); for (int i = 0; i <= 9; i++) { String traceId = "1234567890abcdef1234567890abcde" + i; String spanId = "fedcba098765432" + i; TraceState traceState = TraceState.builder().put("string-key", "string-value" + i).build(); spanBuilder.addLink(SpanContext.create(traceId, spanId, TraceFlags.getSampled(), traceState)); expectedLinks.add( - "{ \"trace_id\": \"" - + traceId - + "\", \"span_id\": \"" - + spanId - + "\", \"flags\": 1, \"tracestate\": \"string-key=string-value" - + i - + "\" }"); + to(DDTraceId.fromHex(traceId), DDSpanId.fromHex(spanId)) + .traceFlags(SAMPLED_FLAG) + .traceState("string-key=string-value" + i)); } spanBuilder.startSpan().end(); - String expectedLinksTag = "[" + join(",", expectedLinks) + "]"; assertTraces( trace( span() - .tags( - defaultTags(), - tag(SPAN_KIND, is(SPAN_KIND_INTERNAL)), - tag(SPAN_LINKS, isJson(expectedLinksTag))))); + .tags(defaultTags(), tag(SPAN_KIND, is(SPAN_KIND_INTERNAL))) + .links(expectedLinks.toArray(new SpanLinkMatcher[0])))); } static Stream testSpanLinkAttributesArguments() { return Stream.of( - arguments("empty attributes", Attributes.empty(), null), + arguments("empty attributes", Attributes.empty(), emptyMap()), arguments( "scalar attributes", Attributes.builder() @@ -404,7 +377,12 @@ static Stream testSpanLinkAttributesArguments() { .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( + "string-key", "string-value", + "long-key", "123456789", + "double-key", "1234.5678", + "boolean-key-true", "true", + "boolean-key-false", "false")), arguments( "array attributes", Attributes.builder() @@ -413,12 +391,25 @@ static Stream testSpanLinkAttributesArguments() { .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\" }")); + attributes( + "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"))); } @ParameterizedTest(name = "[{index}] {0}") @MethodSource("testSpanLinkAttributesArguments") - void testSpanLinkAttributes(String scenario, Attributes attributes, String expectedAttributes) { + void testSpanLinkAttributes( + String scenario, Attributes attributes, Map expectedAttributes) { String traceId = "1234567890abcdef1234567890abcdef"; String spanId = "fedcba0987654321"; TraceState traceState = TraceState.builder().put("string-key", "string-value").build(); @@ -431,29 +422,20 @@ void testSpanLinkAttributes(String scenario, Attributes attributes, String expec .startSpan(); span.end(); - String expectedLinksTag = - "[" - + "{ \"trace_id\": \"" - + traceId - + "\", \"span_id\": \"" - + spanId - + "\", \"flags\": 1, \"tracestate\": \"string-key=string-value\"" - + (expectedAttributes == null ? "" : ", \"attributes\": " + expectedAttributes) - + " }" - + "]"; - assertTraces( trace( span() - .tags( - defaultTags(), - tag(SPAN_KIND, is(SPAN_KIND_INTERNAL)), - tag(SPAN_LINKS, isJson(expectedLinksTag))))); + .tags(defaultTags(), tag(SPAN_KIND, is(SPAN_KIND_INTERNAL))) + .links( + to(DDTraceId.fromHex(traceId), DDSpanId.fromHex(spanId)) + .traceFlags(SAMPLED_FLAG) + .traceState("string-key=string-value") + .attributes(SpanAttributes.fromMap(expectedAttributes))))); } static Stream testSpanLinksTraceStateArguments() { return Stream.of( - arguments("default trace state", TraceState.getDefault(), null), + arguments("default trace state", TraceState.getDefault(), ""), arguments( "single key-value", TraceState.builder().put("key", "value").build(), "key=value"), arguments( @@ -481,26 +463,14 @@ void testSpanLinksTraceState(String scenario, TraceState traceState, String expe .startSpan(); span.end(); - String expectedTraceStateJson = - expectedTraceState == null ? "" : ", \"tracestate\": \"" + expectedTraceState + "\""; - String expectedLinksTag = - "[" - + "{ \"trace_id\": \"" - + traceId - + "\", \"span_id\": \"" - + spanId - + "\", \"flags\": 1" - + expectedTraceStateJson - + " }" - + "]"; - assertTraces( trace( span() - .tags( - defaultTags(), - tag(SPAN_KIND, is(SPAN_KIND_INTERNAL)), - tag(SPAN_LINKS, isJson(expectedLinksTag))))); + .tags(defaultTags(), tag(SPAN_KIND, is(SPAN_KIND_INTERNAL))) + .links( + to(DDTraceId.fromHex(traceId), DDSpanId.fromHex(spanId)) + .traceFlags(SAMPLED_FLAG) + .traceState(expectedTraceState)))); } static Stream testSpanAttributesArguments() { @@ -747,7 +717,7 @@ static Stream testSpanRecordExceptionArguments() { null, null, null, - ", \"key\": \"value\"")); + singletonMap("key", "value"))); } @ParameterizedTest(name = "[{index}] {0}") @@ -759,7 +729,7 @@ void testSpanRecordException( String overriddenMessage, String overriddenType, String overriddenStacktrace, - String extraJson) { + Map extraAttributes) { Span result = this.otelTracer.spanBuilder("some-name").startSpan(); result.recordException(exception, attributes); result.end(); @@ -768,26 +738,14 @@ void testSpanRecordException( String errorType = overriddenType != null ? overriddenType : exception.getClass().getName(); String errorStackTrace = overriddenStacktrace != null ? overriddenStacktrace : stringifyErrorStack(exception); - String expectedAttributes = - "{" - + "\"exception.message\": \"" - + errorMessage - + "\", \"exception.type\": \"" - + errorType - + "\", \"exception.stacktrace\": \"" - + errorStackTrace - + "\"" - + (extraJson != null ? extraJson : "") - + "}"; - - String expectedEventTag = - "[" - + "{ \"time_unix_nano\": " - + eventTimeWithinSpan(result, 0) - + ", \"name\": \"exception\", \"attributes\": " - + expectedAttributes - + " }" - + "]"; + Map expectedAttributes = + attributes( + "exception.message", errorMessage, + "exception.type", errorType, + "exception.stacktrace", errorStackTrace); + if (extraAttributes != null) { + expectedAttributes.putAll(extraAttributes); + } assertTraces( trace( @@ -799,10 +757,10 @@ void testSpanRecordException( .tags( defaultTags(), tag(SPAN_KIND, is(SPAN_KIND_INTERNAL)), - tag(SPAN_EVENTS, isJson(expectedEventTag)), tag(ERROR_MSG, is(errorMessage)), tag(ERROR_TYPE, is(errorType)), - tag(ERROR_STACK, is(errorStackTrace))))); + tag(ERROR_STACK, is(errorStackTrace))) + .events(event("exception").attributes(expectedAttributes)))); } @Test @@ -825,10 +783,12 @@ void testSpanErrorReflectLastException() { .tags( defaultTags(), tag(SPAN_KIND, is(SPAN_KIND_INTERNAL)), - tag(SPAN_EVENTS, any()), tag(ERROR_MSG, is(lastException.getMessage())), tag(ERROR_TYPE, is(lastException.getClass().getName())), - tag(ERROR_STACK, is(stringifyErrorStack(lastException)))))); + tag(ERROR_STACK, is(stringifyErrorStack(lastException)))) + .events( + event("exception").attributes(exceptionAttributes(firstException)), + event("exception").attributes(exceptionAttributes(lastException))))); } @Test @@ -879,27 +839,18 @@ private static DDSpan getDDSpan(Span span) { return (DDSpan) ((WithAgentSpan) span).asAgentSpan(); } - /** Gets the time of the single span event, checking it comes from the same clock as the span. */ - private static long eventTimeWithinSpan(Span span, int eventIndex) { - DDSpan ddSpan = getDDSpan(span); - List events = ddSpan.getEvents(); - assertTrue(eventIndex < events.size()); - long eventTime = events.get(eventIndex).timeUnixNano(); - long startTime = ddSpan.getStartTime(); - long endTime = startTime + ddSpan.getDurationNano(); - assertTrue(startTime <= eventTime && eventTime <= endTime, "Event time outside span bounds"); - return eventTime; + private static Map exceptionAttributes(Throwable exception) { + return attributes( + "exception.message", exception.getMessage(), + "exception.type", exception.getClass().getName(), + "exception.stacktrace", stringifyErrorStack(exception)); } - private static Matcher isJson(String expected) { - return Matchers.validates( - s -> { - try { - JSONAssert.assertEquals(expected, s, true); - return true; - } catch (JSONException e) { - return false; - } - }); + private static Map attributes(Object... keyValues) { + Map attributes = new LinkedHashMap<>(); + for (int i = 0; i < keyValues.length; i += 2) { + attributes.put((String) keyValues[i], keyValues[i + 1]); + } + return attributes; } }