diff --git a/.github/g2j-migrated-modules.txt b/.github/g2j-migrated-modules.txt index 118406ecd1a..767c14f9c66 100644 --- a/.github/g2j-migrated-modules.txt +++ b/.github/g2j-migrated-modules.txt @@ -7,4 +7,5 @@ buildSrc/call-site-instrumentation-plugin components/json +dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.4 dd-trace-api diff --git a/dd-java-agent/instrumentation-testing/src/main/java/datadog/trace/agent/test/assertions/Matchers.java b/dd-java-agent/instrumentation-testing/src/main/java/datadog/trace/agent/test/assertions/Matchers.java index cf25a5fa581..973338bd1f1 100644 --- a/dd-java-agent/instrumentation-testing/src/main/java/datadog/trace/agent/test/assertions/Matchers.java +++ b/dd-java-agent/instrumentation-testing/src/main/java/datadog/trace/agent/test/assertions/Matchers.java @@ -1,9 +1,10 @@ package datadog.trace.agent.test.assertions; +import static org.junit.jupiter.api.AssertionFailureBuilder.assertionFailure; + import java.util.Optional; import java.util.function.Predicate; import java.util.regex.Pattern; -import org.opentest4j.AssertionFailedError; /** This class is a utility class to create generic matchers. */ public final class Matchers { @@ -103,12 +104,11 @@ public static Matcher any() { static void assertValue(Matcher matcher, T value, String message) { if (matcher != null && !matcher.test(value)) { Optional expected = matcher.expected(); - if (expected.isPresent()) { - throw new AssertionFailedError( - message + ". " + matcher.failureReason(), expected.get(), value); - } else { - throw new AssertionFailedError(message + ": " + value + ". " + matcher.failureReason()); - } + assertionFailure() + .message(message + ". " + matcher.failureReason()) + .expected(expected.orElse(null)) + .actual(value) + .buildAndThrow(); } } } 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 4ceb959c797..ce41e2126c8 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 @@ -10,6 +10,7 @@ import static datadog.trace.agent.test.assertions.Matchers.validates; import static datadog.trace.core.DDSpanAccessor.spanLinks; import static java.time.Duration.ofNanos; +import static org.junit.jupiter.api.AssertionFailureBuilder.assertionFailure; import datadog.trace.api.TagMap; import datadog.trace.bootstrap.instrumentation.api.AgentSpanLink; @@ -322,7 +323,7 @@ private void assertSpanTags(TagMap tags) { if (matcher == null) { uncheckedTagNames.add(key); } else { - assertValue(matcher, value, "Unexpected " + key + " tag value."); + assertValue(matcher, value, "Unexpected " + key + " tag value"); } }); // Remove matchers that accept missing tags @@ -344,10 +345,18 @@ private void assertSpanTags(TagMap tags) { * It might evolve into partial link collection testing, matching links using TID/SIP. */ private void assertSpanLinks(List links) { + // Check if links should be asserted at all + if (this.linkMatchers == null) { + return; + } int linkCount = links == null ? 0 : links.size(); - int expectedLinkCount = this.linkMatchers == null ? 0 : this.linkMatchers.length; + int expectedLinkCount = this.linkMatchers.length; if (linkCount != expectedLinkCount) { - throw new AssertionFailedError("Unexpected span link count", expectedLinkCount, linkCount); + assertionFailure() + .message("Unexpected span link count") + .expected(expectedLinkCount) + .actual(linkCount) + .buildAndThrow(); } for (int i = 0; i < expectedLinkCount; i++) { SpanLinkMatcher linkMatcher = this.linkMatchers[expectedLinkCount]; 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 21dae20ebaa..b9d439d0259 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 @@ -3,18 +3,28 @@ import static datadog.trace.agent.test.assertions.Matchers.any; import static datadog.trace.agent.test.assertions.Matchers.is; import static datadog.trace.agent.test.assertions.Matchers.isNonNull; +import static datadog.trace.api.DDTags.BASE_SERVICE; +import static datadog.trace.api.DDTags.DD_INTEGRATION; +import static datadog.trace.api.DDTags.DJM_ENABLED; +import static datadog.trace.api.DDTags.DSM_ENABLED; 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.LANGUAGE_TAG_KEY; +import static datadog.trace.api.DDTags.PARENT_ID; +import static datadog.trace.api.DDTags.PID_TAG; +import static datadog.trace.api.DDTags.PROFILING_CONTEXT_ENGINE; +import static datadog.trace.api.DDTags.PROFILING_ENABLED; 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_LINKS; import static datadog.trace.api.DDTags.THREAD_ID; import static datadog.trace.api.DDTags.THREAD_NAME; +import static datadog.trace.api.DDTags.TRACER_HOST; import static datadog.trace.common.sampling.RateByServiceTraceSampler.SAMPLING_AGENT_RATE; import static datadog.trace.common.writer.ddagent.TraceMapper.SAMPLING_PRIORITY_KEY; -import datadog.trace.api.DDTags; import java.util.HashMap; import java.util.Map; @@ -34,15 +44,17 @@ public static TagsMatcher defaultTags() { tagMatchers.put(SAMPLING_AGENT_RATE, any()); tagMatchers.put(SAMPLING_PRIORITY_KEY.toString(), any()); tagMatchers.put("_sample_rate", any()); - tagMatchers.put(DDTags.PID_TAG, any()); - tagMatchers.put(DDTags.SCHEMA_VERSION_TAG_KEY, any()); - tagMatchers.put(DDTags.PROFILING_ENABLED, any()); - tagMatchers.put(DDTags.PROFILING_CONTEXT_ENGINE, any()); - tagMatchers.put(DDTags.BASE_SERVICE, any()); - tagMatchers.put(DDTags.DSM_ENABLED, any()); - tagMatchers.put(DDTags.DJM_ENABLED, any()); - tagMatchers.put(DDTags.PARENT_ID, any()); - tagMatchers.put(DDTags.SPAN_LINKS, any()); // this is checked by LinksAsserter + tagMatchers.put(PID_TAG, any()); + tagMatchers.put(SCHEMA_VERSION_TAG_KEY, any()); + tagMatchers.put(PROFILING_ENABLED, any()); + tagMatchers.put(PROFILING_CONTEXT_ENGINE, any()); + tagMatchers.put(BASE_SERVICE, any()); + tagMatchers.put(DSM_ENABLED, any()); + tagMatchers.put(DJM_ENABLED, any()); + tagMatchers.put(PARENT_ID, any()); + tagMatchers.put(SPAN_LINKS, any()); // this is checked by LinksAsserter + tagMatchers.put(DD_INTEGRATION, any()); + tagMatchers.put(TRACER_HOST, any()); for (String tagName : REQUIRED_CODE_ORIGIN_TAGS) { tagMatchers.put(tagName, any()); diff --git a/dd-java-agent/instrumentation-testing/src/main/java/datadog/trace/agent/test/assertions/TraceAssertions.java b/dd-java-agent/instrumentation-testing/src/main/java/datadog/trace/agent/test/assertions/TraceAssertions.java index 58726369ba3..c2bb123103f 100644 --- a/dd-java-agent/instrumentation-testing/src/main/java/datadog/trace/agent/test/assertions/TraceAssertions.java +++ b/dd-java-agent/instrumentation-testing/src/main/java/datadog/trace/agent/test/assertions/TraceAssertions.java @@ -1,12 +1,12 @@ package datadog.trace.agent.test.assertions; import static java.util.function.Function.identity; +import static org.junit.jupiter.api.AssertionFailureBuilder.assertionFailure; import datadog.trace.core.DDSpan; import java.util.Comparator; import java.util.List; import java.util.function.Function; -import org.opentest4j.AssertionFailedError; /** * This class is a helper class to verify traces structure. @@ -87,11 +87,19 @@ public static void assertTraces( int traceCount = traces.size(); if (opts.ignoredAdditionalTraces) { if (traceCount < expectedTraceCount) { - throw new AssertionFailedError("Not enough of traces", expectedTraceCount, traceCount); + assertionFailure() + .message("Not enough of traces") + .expected(expectedTraceCount) + .actual(traceCount) + .buildAndThrow(); } } else { if (traceCount != expectedTraceCount) { - throw new AssertionFailedError("Invalid number of traces", expectedTraceCount, traceCount); + assertionFailure() + .message("Invalid number of traces") + .expected(expectedTraceCount) + .actual(traceCount) + .buildAndThrow(); } } if (opts.sorter != null) { diff --git a/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.4/src/test/groovy/OpenTelemetry14ActivationTest.groovy b/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.4/src/test/groovy/OpenTelemetry14ActivationTest.groovy deleted file mode 100644 index 50115948222..00000000000 --- a/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.4/src/test/groovy/OpenTelemetry14ActivationTest.groovy +++ /dev/null @@ -1,64 +0,0 @@ -import datadog.trace.agent.test.InstrumentationSpecification -import io.opentelemetry.api.GlobalOpenTelemetry -import io.opentelemetry.context.Context - -abstract class OpenTelemetry14ActivationTest extends InstrumentationSpecification { - abstract boolean shouldBeInjected() - - def "test instrumentation injection"() { - setup: - def tracer = GlobalOpenTelemetry.get().tracerProvider.get("some-instrumentation") - def builder = tracer.spanBuilder("some-name") - def result = builder.startSpan() - def context = Context.current() - - expect: - if (shouldBeInjected()) { - assert tracer.class.name.endsWith(".OtelTracer") - assert builder.class.name.endsWith(".OtelSpanBuilder") - assert result.class.name.endsWith(".OtelSpan") - assert context.class.name.endsWith(".OtelContext") - } else { - assert tracer.class.name.endsWith(".DefaultTracer") - assert context.class.name.endsWith(".ArrayBasedContext") - } - } -} - -// -// Below test variants are forked to allow GlobalOpenTelemetry static state to reset -// - -class OpenTelemetry14ActivationByInstrumentationNameForkedTest extends OpenTelemetry14ActivationTest { - @Override - void configurePreAgent() { - super.configurePreAgent() - injectSysConfig("integration.opentelemetry.experimental.enabled", "true") - } - - @Override - boolean shouldBeInjected() { - return true - } -} - -class OpenTelemetry14ActivationByOtelRfcNameForkedTest extends OpenTelemetry14ActivationTest { - @Override - void configurePreAgent() { - super.configurePreAgent() - injectSysConfig("trace.otel.enabled", "true") - } - - @Override - boolean shouldBeInjected() { - return true - } -} - -class OpenTelemetry14DisableByDefaultForkedTest extends OpenTelemetry14ActivationTest { - @Override - boolean shouldBeInjected() { - return false - } -} - diff --git a/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.4/src/test/groovy/OpenTelemetry14ConventionsTest.groovy b/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.4/src/test/groovy/OpenTelemetry14ConventionsTest.groovy deleted file mode 100644 index be9d596d735..00000000000 --- a/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.4/src/test/groovy/OpenTelemetry14ConventionsTest.groovy +++ /dev/null @@ -1,269 +0,0 @@ -import datadog.trace.agent.test.InstrumentationSpecification -import datadog.trace.api.DDTags -import datadog.trace.bootstrap.instrumentation.api.Tags -import io.opentelemetry.api.GlobalOpenTelemetry -import io.opentelemetry.context.Context -import io.opentelemetry.context.ThreadLocalContextStorage -import spock.lang.Subject - -import static datadog.trace.bootstrap.instrumentation.api.Tags.SPAN_KIND -import static datadog.opentelemetry.shim.trace.OtelConventions.OPERATION_NAME_SPECIFIC_ATTRIBUTE -import static datadog.opentelemetry.shim.trace.OtelConventions.SPAN_KIND_INTERNAL -import static datadog.opentelemetry.shim.trace.OtelConventions.toSpanKindTagValue -import static io.opentelemetry.api.common.AttributeKey.longKey -import static io.opentelemetry.api.common.AttributeKey.stringKey -import static io.opentelemetry.api.trace.SpanKind.CLIENT -import static io.opentelemetry.api.trace.SpanKind.CONSUMER -import static io.opentelemetry.api.trace.SpanKind.INTERNAL -import static io.opentelemetry.api.trace.SpanKind.PRODUCER -import static io.opentelemetry.api.trace.SpanKind.SERVER - -class OpenTelemetry14ConventionsTest extends InstrumentationSpecification { - @Subject - def tracer = GlobalOpenTelemetry.get().tracerProvider.get("conventions") - - @Override - void configurePreAgent() { - super.configurePreAgent() - - injectSysConfig("dd.integration.opentelemetry.experimental.enabled", "true") - } - - def "test span name conventions"() { - when: - def builder = tracer.spanBuilder("some-name") - .setSpanKind(kind) - attributes.forEach { key, value -> builder.setAttribute(key, value) } - builder.startSpan() - .end() - - then: - assertTraces(1) { - trace(1) { - span { - parent() - operationName "$expectedOperationName" - resourceName "some-name" - tags { - defaultTags() - "$SPAN_KIND" "${toSpanKindTagValue(kind == null ? INTERNAL : kind)}" - attributes.forEach { key, value -> - if (!OPERATION_NAME_SPECIFIC_ATTRIBUTE.equals(key)) { - tag(key, value) - } - } - } - } - } - } - - where: - kind | attributes | expectedOperationName - // Fallback behavior - null | [:] | "internal" - // Internal spans - INTERNAL | [:] | "internal" - // Server spans - SERVER | [:] | "server.request" - SERVER | ["http.request.method": "GET"] | "http.server.request" - SERVER | ["http.request.method": "GET"] | "http.server.request" - SERVER | ["network.protocol.name": "amqp"] | "amqp.server.request" - // Client spans - CLIENT | [:] | "client.request" - CLIENT | ["http.request.method": "GET"] | "http.client.request" - CLIENT | ["db.system": "mysql"] | "mysql.query" - CLIENT | ["network.protocol.name": "amqp"] | "amqp.client.request" - CLIENT | ["network.protocol.name": "AMQP"] | "amqp.client.request" - // Messaging spans - PRODUCER | [:] | "producer" - CONSUMER | [:] | "consumer" - CONSUMER | ["messaging.system": "rabbitmq", "messaging.operation": "publish"] | "rabbitmq.publish" - PRODUCER | ["messaging.system": "rabbitmq", "messaging.operation": "publish"] | "rabbitmq.publish" - CLIENT | ["messaging.system": "rabbitmq", "messaging.operation": "publish"] | "rabbitmq.publish" - SERVER | ["messaging.system": "rabbitmq", "messaging.operation": "publish"] | "rabbitmq.publish" - // RPC spans - CLIENT | ["rpc.system": "grpc"] | "grpc.client.request" - SERVER | ["rpc.system": "grpc"] | "grpc.server.request" - CLIENT | ["rpc.system": "aws-api"] | "aws.client.request" - CLIENT | ["rpc.system": "aws-api", "rpc.service": "helloworld"] | "aws.helloworld.request" - SERVER | ["rpc.system": "aws-api"] | "aws-api.server.request" - // FAAS spans - CLIENT | ["faas.invoked_provider": "alibaba_cloud", "faas.invoked_name": "my-function"] | "alibaba_cloud.my-function.invoke" - SERVER | ["faas.trigger": "datasource"] | "datasource.invoke" - // GraphQL spans - INTERNAL | ["graphql.operation.type": "query"] | "graphql.server.request" - null | ["graphql.operation.type": "query"] | "graphql.server.request" - // User override - CLIENT | ["db.system": "mysql", "operation.name": "db.query"] | "db.query" - CLIENT | ["db.system": "mysql", "operation.name": "DB.query"] | "db.query" - } - - def "test span specific tags"() { - setup: - def builder = tracer.spanBuilder("some-name") - def keyFor = (String key) -> useAttributeKey ? stringKey(key) : key - - when: - if (setInBuilder) { - builder.setAttribute(keyFor("operation.name"), "my-operation") - .setAttribute(keyFor("service.name"), "my-service") - .setAttribute(keyFor("resource.name"), "/my-resource") - .setAttribute(keyFor("span.type"), "http") - } - def result = builder.startSpan() - if (!setInBuilder) { - result.setAttribute(keyFor("operation.name"), "my-operation") - .setAttribute(keyFor("service.name"), "my-service") - .setAttribute(keyFor("resource.name"), "/my-resource") - .setAttribute(keyFor("span.type"), "http") - } - result.end() - - then: - assertTraces(1) { - trace(1) { - span { - parent() - operationName "my-operation" - resourceName "/my-resource" - serviceName "my-service" - spanType "http" - tags { - serviceNameSource "m" //service name was manually set - defaultTags() - "$SPAN_KIND" "$SPAN_KIND_INTERNAL" - } - } - } - } - - where: - setInBuilder | useAttributeKey - true | true - true | false - false | true - false | false - } - - def "test span analytics.event specific tag"() { - setup: - def builder = tracer.spanBuilder("some-name") - - when: - if (setInBuilder) { - builder.setAttribute("analytics.event", value) - } - def result = builder.startSpan() - if (!setInBuilder) { - result.setAttribute("analytics.event", value) - } - result.end() - - then: - assertTraces(1) { - trace(1) { - span { - parent() - operationName "internal" - tags { - defaultTags() - "$SPAN_KIND" "$SPAN_KIND_INTERNAL" - if (value != null) { - "$DDTags.ANALYTICS_SAMPLE_RATE" expectedMetric - } - } - } - } - } - - where: - setInBuilder | value | expectedMetric - true | true | 1 - true | Boolean.TRUE | 1 - true | false | 0 - true | Boolean.FALSE | 0 - true | null | 0 // Not used - true | "true" | 1 - true | "false" | 0 - true | "TRUE" | 1 - true | "something-else" | 0 - true | "" | 0 - false | true | 1 - false | Boolean.TRUE | 1 - false | false | 0 - false | Boolean.FALSE | 0 - false | null | 0 // Not used - false | "true" | 1 - false | "false" | 0 - false | "TRUE" | 1 - false | "something-else" | 0 - false | "" | 0 - } - - def "test span http.response.status_code specific tag"() { - setup: - def builder = tracer.spanBuilder("some-name") - - when: - if (setInBuilder) { - if (attributeKey) { - builder.setAttribute(longKey("http.response.status_code"), value) - } else { - builder.setAttribute("http.response.status_code", value) - } - } - def result = builder.startSpan() - if (!setInBuilder) { - if (attributeKey) { - result.setAttribute(longKey("http.response.status_code"), value) - } else { - result.setAttribute("http.response.status_code", value) - } - } - result.end() - - then: - assertTraces(1) { - trace(1) { - span { - parent() - operationName "internal" - tags { - defaultTags() - "$SPAN_KIND" "$SPAN_KIND_INTERNAL" - if (value != null) { - "$Tags.HTTP_STATUS" expectedStatus - } - } - } - } - } - - where: - setInBuilder | attributeKey | value | expectedStatus - true | false | null | 0 // Not used - true | false | 200 | 200 - true | false | 404L | 404 - true | false | 500 as Long | 500 - false | false | null | 0 // Not used - false | false | 200 | 200 - false | false | 404L | 404 - false | false | 500 as Long | 500 - true | true | null | 0 // Not used - true | true | 200 | 200 - true | true | 404L | 404 - true | true | 500 as Long | 500 - false | true | null | 0 // Not used - false | true | 200 | 200 - false | true | 404L | 404 - false | true | 500 as Long | 500 - } - - @Override - void cleanup() { - // Test for context leak - assert Context.current() == Context.root() - // Safely reset OTel context storage - ThreadLocalContextStorage.THREAD_LOCAL_STORAGE.remove() - } -} 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 deleted file mode 100644 index b40f8b4f7e2..00000000000 --- a/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.4/src/test/groovy/OpenTelemetry14Test.groovy +++ /dev/null @@ -1,808 +0,0 @@ -import datadog.opentelemetry.shim.trace.OtelSpanEvent -import datadog.trace.agent.test.InstrumentationSpecification -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 -import io.opentelemetry.api.trace.SpanContext -import io.opentelemetry.api.trace.TraceFlags -import io.opentelemetry.api.trace.TraceState -import io.opentelemetry.context.Context -import io.opentelemetry.context.ThreadLocalContextStorage -import opentelemetry14.context.propagation.TextMap -import org.skyscreamer.jsonassert.JSONAssert -import spock.lang.Subject - -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 io.opentelemetry.api.trace.SpanKind.CLIENT -import static io.opentelemetry.api.trace.SpanKind.CONSUMER -import static io.opentelemetry.api.trace.SpanKind.INTERNAL -import static io.opentelemetry.api.trace.SpanKind.PRODUCER -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 InstrumentationSpecification { - static final TIME_MILLIS = 1723220824705 - static final TIME_NANO = TIME_MILLIS * 1_000_000L - - @Subject - def tracer = GlobalOpenTelemetry.get().tracerProvider.get("some-instrumentation") - - @Override - void configurePreAgent() { - super.configurePreAgent() - - injectSysConfig("dd.integration.opentelemetry.experimental.enabled", "true") - } - - def "test parent span using active span"() { - setup: - def parentSpan = tracer.spanBuilder("some-name").startSpan() - def scope = parentSpan.makeCurrent() - - when: - def childSpan = tracer.spanBuilder("other-name").startSpan() - childSpan.end() - scope.close() - parentSpan.end() - - then: - assertTraces(1) { - trace(2) { - span { - parent() - operationName "internal" - resourceName "some-name" - } - span { - childOfPrevious() - operationName "internal" - resourceName "other-name" - } - } - } - } - - def "test parent span using reference"() { - setup: - def parentSpan = tracer.spanBuilder("some-name").startSpan() - - when: - def childSpan = tracer.spanBuilder("other-name") - .setParent(Context.current().with(parentSpan)) - .startSpan() - childSpan.end() - parentSpan.end() - - then: - assertTraces(1) { - trace(2) { - span { - parent() - operationName "internal" - resourceName "some-name" - } - span { - childOfPrevious() - operationName "internal" - resourceName "other-name" - } - } - } - } - - def "test parent span using propagation data"() { - setup: - def traceId = '00000000000000001111111111111111' - def spanId = '2222222222222222' - def headers = ['traceparent': "00-$traceId-$spanId-00" as String] - def propagator = GlobalOpenTelemetry.getPropagators().textMapPropagator - def context = propagator.extract(Context.root(), headers, TextMap.INSTANCE) - - when: - try (def scope = context.makeCurrent()) { - def childSpan = tracer.spanBuilder("some-name") - .startSpan() - childSpan.end() - } - - then: - assertTraces(1) { - trace(1) { - span { - traceDDId(DDTraceId.fromHex(traceId)) - parentSpanId(DDSpanId.fromHex(spanId).toLong() as BigInteger) - operationName "internal" - resourceName "some-name" - } - } - } - } - - def "test parent span using invalid reference"() { - when: - def invalidCurrentSpanContext = Context.root() // Contains a SpanContext with TID/SID to 0 as current span - def childSpan = tracer.spanBuilder("some-name") - .setParent(invalidCurrentSpanContext) - .startSpan() - childSpan.end() - - TEST_WRITER.waitForTraces(1) - def trace = TEST_WRITER.firstTrace() - - then: - trace.size() == 1 - trace[0].spanId != 0 - } - - def "test no parent to create new root span"() { - setup: - def parentSpan = tracer.spanBuilder("some-name").startSpan() - def scope = parentSpan.makeCurrent() - - when: - def childSpan = tracer.spanBuilder("other-name") - .setNoParent() - .startSpan() - childSpan.end() - scope.close() - parentSpan.end() - - then: - assertTraces(2) { - trace(1) { - span { - parent() - operationName "internal" - resourceName"some-name" - } - } - trace(1) { - span { - parent() - operationName "internal" - resourceName"other-name" - } - } - } - } - - def "test add event"() { - setup: - def builder = tracer.spanBuilder("some-name") - def timeSource = new ControllableTimeSource() - timeSource.set(1000) - OtelSpanEvent.setTimeSource(timeSource) - - when: - def result = builder.startSpan() - result.addEvent("event") - result.end() - - then: - def expectedEventTag = """ - [ - { "time_unix_nano": ${timeSource.getCurrentTimeNanos()}, - "name": "event" - } - ]""" - assertTraces(1) { - trace(1) { - 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 { - 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 }) - } - } - } - } - } - - def "test simple span links"() { - setup: - def traceId = "1234567890abcdef1234567890abcdef" as String - def spanId = "fedcba0987654321" as String - def traceState = TraceState.builder().put("string-key", "string-value").build() - - def expectedLinksTag = """ - [ - { trace_id: "${traceId}", - span_id: "${spanId}", - flags: 1, - tracestate: "string-key=string-value"} - ]""" - - when: - def span1 =tracer.spanBuilder("some-name") - .addLink(SpanContext.getInvalid()) // Should not be added - .addLink(SpanContext.create(traceId, spanId, TraceFlags.getSampled(), traceState)) - .startSpan() - span1.end() - - then: - assertTraces(1) { - trace(1) { - span { - ignoreSpanLinks() // check is done on the content of the tag below - tags { - defaultTags() - "$SPAN_KIND" "$SPAN_KIND_INTERNAL" - tag("_dd.span_links", { JSONAssert.assertEquals(expectedLinksTag, it as String, true); return true }) - } - } - } - } - } - - def "test multiple span links"() { - setup: - def spanBuilder = tracer.spanBuilder("some-name") - - when: - def links = [] - 0..9.each { - def traceId = "1234567890abcdef1234567890abcde$it" as String - def spanId = "fedcba098765432$it" as String - def traceState = TraceState.builder().put('string-key', 'string-value'+it).build() - links << """{ trace_id: "${traceId}", - span_id: "${spanId}", - flags: 1, - tracestate: "string-key=string-value$it"}""" - spanBuilder.addLink(SpanContext.create(traceId, spanId, TraceFlags.getSampled(), traceState)) - } - def expectedLinksTag = "[${links.join(',')}]" as String - - spanBuilder.startSpan().end() - - then: - assertTraces(1) { - trace(1) { - span { - ignoreSpanLinks() // check is done on the content of the tag below - tags { - defaultTags() - "$SPAN_KIND" "$SPAN_KIND_INTERNAL" - tag("_dd.span_links", { JSONAssert.assertEquals(expectedLinksTag, it as String, true); return true }) - } - } - } - } - } - - def "test span link attributes"() { - setup: - def traceId = "1234567890abcdef1234567890abcdef" as String - def spanId = "fedcba0987654321" as String - def traceState = TraceState.builder().put("string-key", "string-value").build() - - def expectedLinksTag = """ - [ - { trace_id: "${traceId}", - span_id: "${spanId}", - flags: 1, - tracestate: "string-key=string-value" - ${ expectedAttributes == null ? "" : ", attributes: " + expectedAttributes }} - ]""" - - when: - def span1 =tracer.spanBuilder("some-name") - .addLink(SpanContext.create(traceId, spanId, TraceFlags.getSampled(), traceState), attributes) - .startSpan() - span1.end() - - then: - assertTraces(1) { - trace(1) { - span { - ignoreSpanLinks() // check is done on the content of the tag below - tags { - defaultTags() - "$SPAN_KIND" "$SPAN_KIND_INTERNAL" - tag("_dd.span_links", { JSONAssert.assertEquals(expectedLinksTag, it as String, true); return true }) - } - } - } - } - - where: - attributes | expectedAttributes - Attributes.empty() | null - 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" }' - } - - def "test span links trace state"() { - setup: - def traceId = "1234567890abcdef1234567890abcdef" as String - def spanId = "fedcba0987654321" as String - - def expectedTraceStateJson = expectedTraceState == null ? '' : ", tracestate: \"$expectedTraceState\"" - def expectedLinksTag = """ - [ - { trace_id: "${traceId}", - span_id: "${spanId}", - flags: 1 - $expectedTraceStateJson - } - ]""" - - when: - def span1 =tracer.spanBuilder("some-name") - .addLink(SpanContext.create(traceId, spanId, TraceFlags.getSampled(), traceState)) - .startSpan() - span1.end() - - then: - assertTraces(1) { - trace(1) { - span { - ignoreSpanLinks() // check is done on the content of the tag below - tags { - defaultTags() - "$SPAN_KIND" "$SPAN_KIND_INTERNAL" - tag("_dd.span_links", { JSONAssert.assertEquals(expectedLinksTag, it as String, true); return true }) - } - } - } - } - - where: - traceState | expectedTraceState - TraceState.getDefault() | null - TraceState.builder().put("key", "value").build() | 'key=value' - TraceState.builder().put("key1", "value1").put("key2", "value2").put("key3", "value3").put("key4", "value4").put("key5", "value5").build() | 'key5=value5,key4=value4,key3=value3,key2=value2,key1=value1' - } - - def "test span attributes"() { - setup: - def builder = tracer.spanBuilder("some-name") - if (tagBuilder) { - builder.setAttribute(DDTags.RESOURCE_NAME, "some-resource") - .setAttribute("string", "a") - .setAttribute("null-string", null) - .setAttribute("empty_string", "") - .setAttribute("number", 1) - .setAttribute("boolean", true) - .setAttribute(AttributeKey.stringKey("null-string-attribute"), null) - .setAttribute(AttributeKey.stringKey("empty-string-attribute"), "") - .setAttribute(AttributeKey.stringArrayKey("string-array"), ["a", "b", "c"]) - .setAttribute(AttributeKey.booleanArrayKey("boolean-array"), [true, false]) - .setAttribute(AttributeKey.longArrayKey("long-array"), [1L, 2L, 3L, 4L]) - .setAttribute(AttributeKey.doubleArrayKey("double-array"), [1.23D, 4.56D]) - .setAttribute(AttributeKey.stringArrayKey("empty-array"), Collections.emptyList()) - .setAttribute(AttributeKey.stringArrayKey("null-array"), null) - } - def result = builder.startSpan() - if (tagSpan) { - result.setAttribute(DDTags.RESOURCE_NAME, "other-resource") - result.setAttribute("string", "b") - result.setAttribute("empty_string", "") - result.setAttribute("number", 2) - result.setAttribute("boolean", false) - result.setAttribute(AttributeKey.stringKey("null-string-attribute"), null) - result.setAttribute(AttributeKey.stringKey("empty-string-attribute"), "") - result.setAttribute(AttributeKey.stringArrayKey("string-array"), ["d", "e", "f"]) - result.setAttribute(AttributeKey.booleanArrayKey("boolean-array"), [false, true]) - result.setAttribute(AttributeKey.longArrayKey("long-array"), [5L, 6L, 7L, 8L]) - result.setAttribute(AttributeKey.doubleArrayKey("double-array"), [2.34D, 5.67D]) - result.setAttribute(AttributeKey.stringArrayKey("empty-array"), Collections.emptyList()) - result.setAttribute(AttributeKey.stringArrayKey("null-array"), null) - } - - when: - result.end() - - then: - assertTraces(1) { - trace(1) { - span { - parent() - operationName "internal" - if (tagSpan) { - resourceName "other-resource" - } else if (tagBuilder) { - resourceName "some-resource" - } else { - resourceName "some-name" - } - errored false - tags { - defaultTags() - "$SPAN_KIND" "$SPAN_KIND_INTERNAL" - if (tagSpan) { - "string" "b" - "empty_string" "" - "number" 2 - "boolean" false - "empty-string-attribute" "" - "string-array.0" "d" - "string-array.1" "e" - "string-array.2" "f" - "boolean-array.0" false - "boolean-array.1" true - "long-array.0" 5L - "long-array.1" 6L - "long-array.2" 7L - "long-array.3" 8L - "double-array.0" 2.34D - "double-array.1" 5.67D - "empty-array" "" - } else if (tagBuilder) { - "string" "a" - "empty_string" "" - "number" 1 - "boolean" true - "empty-string-attribute" "" - "string-array.0" "a" - "string-array.1" "b" - "string-array.2" "c" - "boolean-array.0" true - "boolean-array.1" false - "long-array.0" 1L - "long-array.1" 2L - "long-array.2" 3L - "long-array.3" 4L - "double-array.0" 1.23D - "double-array.1" 4.56D - "empty-array" "" - } - } - assert span.context().integrationName == "otel" - } - } - } - - where: - tagBuilder | tagSpan - true | false - true | true - false | false - false | true - } - - def "test span kinds"() { - setup: - def result = tracer.spanBuilder("some-name") - .setSpanKind(otelSpanKind) - .startSpan() - - when: - result.end() - - then: - assertTraces(1) { - trace(1) { - span { - tags { - defaultTags() - "$SPAN_KIND" "$tagSpanKind" - } - } - } - } - - where: - otelSpanKind | tagSpanKind - INTERNAL | SPAN_KIND_INTERNAL - SERVER | SPAN_KIND_SERVER - CLIENT | SPAN_KIND_CLIENT - PRODUCER | SPAN_KIND_PRODUCER - CONSUMER | SPAN_KIND_CONSUMER - } - - def "test span error status"() { - setup: - def result = tracer.spanBuilder("some-name").startSpan() - - when: - result.setStatus(ERROR, "some-error") - result.end() - - then: - assertTraces(1) { - trace(1) { - span { - parent() - operationName "internal" - resourceName "some-name" - errored true - tags { - defaultTags() - "$SPAN_KIND" "$SPAN_KIND_INTERNAL" - "$ERROR_MSG" "some-error" - } - } - } - } - } - - def "test span status transition"() { - setup: - def result = tracer.spanBuilder("some-name").startSpan() - - when: - result.setStatus(UNSET) - - then: - !result.delegate.isError() - result.delegate.getTag(ERROR_MSG) == null - - when: - result.setStatus(ERROR, "some error") - - then: - result.delegate.isError() - result.delegate.getTag(ERROR_MSG) == "some error" - - when: - result.setStatus(UNSET) - - then: - result.delegate.isError() - result.delegate.getTag(ERROR_MSG) == "some error" - - when: - result.setStatus(OK) - - then: - !result.delegate.isError() - result.delegate.getTag(ERROR_MSG) == null - - when: - result.end() - - then: - assertTraces(1) { - trace(1) { - span { - parent() - operationName "internal" - resourceName "some-name" - errored false - tags { - defaultTags() - "$SPAN_KIND" "$SPAN_KIND_INTERNAL" - } - } - } - } - } - - def "test span record exception"() { - setup: - def result = tracer.spanBuilder("some-name").startSpan() - 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 { - parent() - operationName "internal" - resourceName "some-name" - errored false - tags { - defaultTags() - "$SPAN_KIND" "$SPAN_KIND_INTERNAL" - 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"() { - setup: - def result = tracer.spanBuilder("some-name") - .setSpanKind(SERVER) - .startSpan() - - expect: - result.delegate.operationName == SPAN_KIND_INTERNAL - result.delegate.resourceName == "some-name" - - when: - result.updateName("other-name") - - then: - result.delegate.operationName == SPAN_KIND_INTERNAL - result.delegate.resourceName == "other-name" - - when: - result.end() - - then: - assertTraces(1) { - trace(1) { - span { - parent() - operationName "server.request" - resourceName "other-name" - } - } - } - } - - def "test span update after end"() { - setup: - def result = tracer.spanBuilder("some-name").startSpan() - - when: - result.setAttribute("string", "value") - result.setStatus(ERROR) - result.end() - result.updateName("other-name") - result.setAttribute("string", "other-value") - result.setStatus(OK) - result.addEvent("event") - result.recordException(new Throwable()) - - then: - assertTraces(1) { - trace(1) { - span { - parent() - operationName "internal" - resourceName"some-name" - errored true - tags { - defaultTags() - "$SPAN_KIND" "$SPAN_KIND_INTERNAL" - "string" "value" - } - } - } - } - } - - @Override - void cleanup() { - // Test for context leak - assert Context.current() == Context.root() - // Safely reset OTel context storage - ThreadLocalContextStorage.THREAD_LOCAL_STORAGE.remove() - } -} diff --git a/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.4/src/test/groovy/opentelemetry14/context/ContextTest.groovy b/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.4/src/test/groovy/opentelemetry14/context/ContextTest.groovy deleted file mode 100644 index 0691d20c921..00000000000 --- a/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.4/src/test/groovy/opentelemetry14/context/ContextTest.groovy +++ /dev/null @@ -1,453 +0,0 @@ -package opentelemetry14.context - -import datadog.trace.agent.test.InstrumentationSpecification -import datadog.trace.api.DDSpanId -import io.opentelemetry.api.GlobalOpenTelemetry -import io.opentelemetry.api.baggage.Baggage -import io.opentelemetry.api.trace.Span -import io.opentelemetry.context.Context -import io.opentelemetry.context.ContextKey -import io.opentelemetry.context.ImplicitContextKeyed -import io.opentelemetry.context.ThreadLocalContextStorage -import spock.lang.Subject - -import static datadog.opentelemetry.shim.context.OtelContext.OTEL_CONTEXT_ROOT_SPAN_KEY -import static datadog.opentelemetry.shim.context.OtelContext.OTEL_CONTEXT_SPAN_KEY -import static datadog.opentelemetry.shim.trace.OtelConventions.SPAN_KIND_INTERNAL - -class ContextTest extends InstrumentationSpecification { - @Subject - def tracer = GlobalOpenTelemetry.get().tracerProvider.get("context-instrumentation") - - @Override - void configurePreAgent() { - super.configurePreAgent() - - injectSysConfig("dd.integration.opentelemetry.experimental.enabled", "true") - } - - def "test Span.current/makeCurrent()"() { - setup: - def builder = tracer.spanBuilder("some-name") - def otelSpan = builder.startSpan() - - when: - def currentSpan = Span.current() - def currentSpanFromContext = Span.fromContext(Context.current()) - def currentSpanFromContextOrNull = Span.fromContextOrNull(Context.current()) - - then: "current span must be invalid or null" - currentSpan != null - !currentSpan.spanContext.valid - currentSpanFromContext != null - !currentSpanFromContext.spanContext.valid - currentSpanFromContextOrNull == null - - when: - def scope = otelSpan.makeCurrent() - currentSpan = Span.current() - currentSpanFromContext = Span.fromContext(Context.current()) - currentSpanFromContextOrNull = Span.fromContextOrNull(Context.current()) - - then: "OTel span must be current span" - currentSpan == otelSpan - currentSpanFromContext == otelSpan - currentSpanFromContextOrNull == otelSpan - - when: - def ddSpan = TEST_TRACER.startSpan("dd-api", "other-name") - def ddScope = TEST_TRACER.activateManualSpan(ddSpan) - currentSpan = Span.current() - - then: "Datadog span must be current span" - currentSpan.spanContext.traceId == ddSpan.traceId.toHexString() - currentSpan.spanContext.spanId == DDSpanId.toHexStringPadded(ddSpan.spanId) - - cleanup: - ddScope.close() - ddSpan.finish() - scope.close() - otelSpan.end() - } - - def "test Context.makeCurrent() to activate a span without prior active span"() { - setup: - def builder = tracer.spanBuilder("some-name") - def otelSpan = builder.startSpan() - - when: - def currentSpan = Span.current() - - then: - currentSpan != null - !currentSpan.spanContext.isValid() - - when: - def contextWithSpan = Context.current().with(otelSpan) - def scope = contextWithSpan.makeCurrent() - currentSpan = Span.current() - - then: - currentSpan == otelSpan - - when: - scope.close() - currentSpan = Span.current() - - then: - currentSpan != null - !currentSpan.spanContext.isValid() - - cleanup: - otelSpan.end() - } - - def "test Context.makeCurrent() to activate a span with another currently active span"() { - setup: - def ddSpan = TEST_TRACER.startSpan("dd-api", "some-name") - def ddScope = TEST_TRACER.activateManualSpan(ddSpan) - def builder = tracer.spanBuilder("other-name") - def otelSpan = builder.startSpan() - - when: - def currentSpan = Span.current() - - then: - currentSpan != null - currentSpan.spanContext.traceId == ddSpan.traceId.toHexStringPadded(32) - currentSpan.spanContext.spanId == DDSpanId.toHexStringPadded(ddSpan.spanId) - - when: - def contextWithSpan = Context.current().with(otelSpan) - def scope = contextWithSpan.makeCurrent() - currentSpan = Span.current() - - then: - currentSpan == otelSpan - - when: - scope.close() - currentSpan = Span.current() - - then: - currentSpan != null - currentSpan.spanContext.traceId == ddSpan.traceId.toHexStringPadded(32) - currentSpan.spanContext.spanId == DDSpanId.toHexStringPadded(ddSpan.spanId) - - cleanup: - otelSpan.end() - ddScope.close() - ddSpan.finish() - } - - def "test Context.makeCurrent() to activate an already active span"() { - when: - def ddSpan = TEST_TRACER.startSpan("dd-api", "some-name") - def ddScope = TEST_TRACER.activateManualSpan(ddSpan) - def currentSpan = Span.current() - - then: - currentSpan != null - currentSpan.spanContext.traceId == ddSpan.traceId.toHexStringPadded(32) - currentSpan.spanContext.spanId == DDSpanId.toHexStringPadded(ddSpan.spanId) - - when: - def contextWithSpan = Context.current().with(currentSpan) - def scope = contextWithSpan.makeCurrent() - currentSpan = Span.current() - - then: - currentSpan != null - currentSpan.spanContext.traceId == ddSpan.traceId.toHexStringPadded(32) - currentSpan.spanContext.spanId == DDSpanId.toHexStringPadded(ddSpan.spanId) - - when: - scope.close() - currentSpan = Span.current() - - then: - currentSpan != null - currentSpan.spanContext.traceId == ddSpan.traceId.toHexStringPadded(32) - currentSpan.spanContext.spanId == DDSpanId.toHexStringPadded(ddSpan.spanId) - - when: - ddScope.close() - ddSpan.finish() - currentSpan = Span.current() - - then: - currentSpan != null - !currentSpan.spanContext.isValid() - - cleanup: - ddScope.close() - ddSpan.finish() - } - - def "test clearing context"() { - when: - def rootScope = Context.root().makeCurrent() - then: - Context.current() == Context.root() - cleanup: - rootScope.close() - } - - def "test mixing manual and OTel instrumentation"() { - setup: - def otelParentSpan = tracer.spanBuilder("some-name").startSpan() - - when: - def otelParentScope = otelParentSpan.makeCurrent() - def activeSpan = TEST_TRACER.activeSpan() - - then: - activeSpan.operationName == SPAN_KIND_INTERNAL - activeSpan.resourceName == "some-name" - DDSpanId.toHexStringPadded(activeSpan.spanId) == otelParentSpan.getSpanContext().spanId - - when: - def ddChildSpan = TEST_TRACER.startSpan("dd-api", "other-name") - def ddChildScope = TEST_TRACER.activateManualSpan(ddChildSpan) - def current = Span.current() - - then: - DDSpanId.toHexStringPadded(ddChildSpan.spanId) == current.getSpanContext().spanId - - when: - def otelGrandChildSpan = tracer.spanBuilder("another-name").startSpan() - def otelGrandChildScope= otelGrandChildSpan.makeCurrent() - activeSpan = TEST_TRACER.activeSpan() - - then: - activeSpan.operationName == SPAN_KIND_INTERNAL - activeSpan.resourceName == "another-name" - DDSpanId.toHexStringPadded(activeSpan.spanId) == otelGrandChildSpan.getSpanContext().spanId - - when: - otelGrandChildScope.close() - otelGrandChildSpan.end() - ddChildScope.close() - ddChildSpan.finish() - otelParentScope.close() - otelParentSpan.end() - - then: - assertTraces(1) { - trace(3) { - span { - parent() - operationName "internal" - resourceName "some-name" - } - span { - childOfPrevious() - operationName "other-name" - } - span { - childOfPrevious() - operationName "internal" - resourceName "another-name" - } - } - } - - cleanup: - otelGrandChildScope?.close() - otelGrandChildSpan?.end() - ddChildScope?.close() - ddChildSpan?.finish() - otelParentScope.close() - otelParentSpan.end() - } - - def "test context spans retrieval"() { - setup: - def parentSpan = tracer.spanBuilder("some-name").startSpan() - def parentScope = parentSpan.makeCurrent() - def currentSpanKey = ContextKey.named(OTEL_CONTEXT_SPAN_KEY) - def rootSpanKey = ContextKey.named(OTEL_CONTEXT_ROOT_SPAN_KEY) - - when: - def current = Context.current() - - then: - current.get(currentSpanKey) == parentSpan - current.get(rootSpanKey) == parentSpan - - when: - def childSpan = tracer.spanBuilder("other-name").startSpan() - def childScope = childSpan.makeCurrent() - current = Context.current() - - then: - current.get(currentSpanKey) == childSpan - current.get(rootSpanKey) == parentSpan - - when: - childScope.close() - childSpan.end() - current = Context.current() - - then: - current.get(currentSpanKey) == parentSpan - current.get(rootSpanKey) == parentSpan - - cleanup: - parentScope.close() - parentSpan.end() - } - - def "test custom object storage"() { - setup: - def context = Context.root() - def originalContext = context - def data1 = new CustomData() - def data2 = new CustomData() - - when: - context = context.with(data1) - - then: - CustomData.fromContext(context) == data1 - CustomData.fromContext(originalContext) == null - - when: - context = context.with(data2) - - then: - CustomData.fromContext(context) == data2 - - when: - context = context.with(CustomData.KEY, null) - - then: - context.get(CustomData.KEY) == null - } - - def "test Baggage.current/makeCurrent()"() { - when: - def otelBaggage = Baggage.current() - def otelBaggageFromContext = Baggage.fromContext(Context.current()) - def otelBaggageFromContextOrNull = Baggage.fromContextOrNull(Context.current()) - - then: "current baggage must be empty or null" - otelBaggage != null - otelBaggage.isEmpty() - otelBaggageFromContext != null - otelBaggageFromContext.isEmpty() - otelBaggageFromContextOrNull == null - - when: - def otelScope = Baggage.builder() - .put("foo", "otel_value_to_be_replaced") - .put("FOO","OTEL_UNTOUCHED") - .put("remove_me_key", "otel_remove_me_value") - .build() - .makeCurrent() - otelBaggage = Baggage.current() - otelBaggageFromContext = Baggage.fromContext(Context.current()) - otelBaggageFromContextOrNull = Baggage.fromContextOrNull(Context.current()) - - then: "OTel baggage must be current" - otelBaggage != null - otelBaggage.size() == 3 - otelBaggage.getEntryValue("foo") == "otel_value_to_be_replaced" - otelBaggage.getEntryValue("FOO") == "OTEL_UNTOUCHED" - otelBaggage.getEntryValue("remove_me_key") == "otel_remove_me_value" - otelBaggage.asMap() == otelBaggageFromContext.asMap() - otelBaggage.asMap() == otelBaggageFromContextOrNull.asMap() - - when: - def ddContext = datadog.context.Context.current() - def ddBaggage = datadog.trace.bootstrap.instrumentation.api.Baggage.fromContext(ddContext) - ddBaggage.addItem("new_foo", "dd_new_value") - ddBaggage.addItem("foo", "dd_overwrite_value") - ddBaggage.removeItem("remove_me_key") - def ddScope = ddContext.with(ddBaggage).attach() - otelBaggage = Baggage.current() - otelBaggageFromContext = Baggage.fromContext(Context.current()) - otelBaggageFromContextOrNull = Baggage.fromContextOrNull(Context.current()) - - then: "baggage must contain Datadog changes" - otelBaggage != null - otelBaggage.size() == 3 - otelBaggage.getEntryValue("foo") == "dd_overwrite_value" - otelBaggage.getEntryValue("FOO") == "OTEL_UNTOUCHED" - otelBaggage.getEntryValue("new_foo") == "dd_new_value" - otelBaggage.asMap() == otelBaggageFromContext.asMap() - otelBaggage.asMap() == otelBaggageFromContextOrNull.asMap() - - when: - ddScope.close() - otelScope.close() - - then: "current baggage must be empty or null" - Baggage.current().isEmpty() - - when: - ddContext = datadog.context.Context.current() - ddBaggage = datadog.trace.bootstrap.instrumentation.api.Baggage.create([ - "foo" : "dd_value_to_be_replaced", - "FOO" : "DD_UNTOUCHED", - "remove_me_key" : "dd_remove_me_value" - ]) - ddScope = ddContext.with(ddBaggage).attach() - otelBaggage = Baggage.current() - otelBaggageFromContext = Baggage.fromContext(Context.current()) - otelBaggageFromContextOrNull = Baggage.fromContextOrNull(Context.current()) - - then: "Datadog baggage must be current" - otelBaggage != null - otelBaggage.size() == 3 - otelBaggage.getEntryValue("foo") == "dd_value_to_be_replaced" - otelBaggage.getEntryValue("FOO") == "DD_UNTOUCHED" - otelBaggage.getEntryValue("remove_me_key") == "dd_remove_me_value" - otelBaggage.asMap() == otelBaggageFromContext.asMap() - otelBaggage.asMap() == otelBaggageFromContextOrNull.asMap() - - when: - def builder = otelBaggage.toBuilder() - builder.put("new_foo", "otel_new_value") - builder.put("foo", "otel_overwrite_value") - builder.remove("remove_me_key") - otelScope = builder.build().makeCurrent() - otelBaggage = Baggage.current() - otelBaggageFromContext = Baggage.fromContext(Context.current()) - otelBaggageFromContextOrNull = Baggage.fromContextOrNull(Context.current()) - - then: "baggage must contain OTel changes" - otelBaggage != null - otelBaggage.size() == 3 - otelBaggage.getEntryValue("foo") == "otel_overwrite_value" - otelBaggage.getEntryValue("FOO") == "DD_UNTOUCHED" - otelBaggage.getEntryValue("new_foo") == "otel_new_value" - otelBaggage.asMap() == otelBaggageFromContext.asMap() - otelBaggage.asMap() == otelBaggageFromContextOrNull.asMap() - - cleanup: - otelScope.close() - ddScope.close() - } - - @Override - void cleanup() { - // Test for context leak - assert Context.current() == Context.root() - // Safely reset OTel context storage - ThreadLocalContextStorage.THREAD_LOCAL_STORAGE.remove() - } - - private static class CustomData implements ImplicitContextKeyed { - private static final ContextKey KEY = ContextKey.named('custom') - - @Override - Context storeInContext(Context context) { - return context.with(KEY, this) - } - - private static CustomData fromContext(Context context) { - return context.get(KEY) - } - } -} diff --git a/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.4/src/test/groovy/opentelemetry14/context/propagation/AbstractPropagatorTest.groovy b/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.4/src/test/groovy/opentelemetry14/context/propagation/AbstractPropagatorTest.groovy deleted file mode 100644 index bdf69488cfc..00000000000 --- a/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.4/src/test/groovy/opentelemetry14/context/propagation/AbstractPropagatorTest.groovy +++ /dev/null @@ -1,116 +0,0 @@ -package opentelemetry14.context.propagation - -import datadog.trace.agent.test.InstrumentationSpecification -import datadog.trace.api.DDSpanId -import datadog.trace.api.DDTraceId -import io.opentelemetry.api.GlobalOpenTelemetry -import io.opentelemetry.context.Context -import io.opentelemetry.context.ThreadLocalContextStorage -import io.opentelemetry.context.propagation.TextMapPropagator -import spock.lang.Subject - -import static datadog.trace.api.sampling.PrioritySampling.SAMPLER_KEEP - -abstract class AbstractPropagatorTest extends InstrumentationSpecification { - static int testInstance - - @Subject - def tracer = GlobalOpenTelemetry.get().tracerProvider.get("propagator" + testInstance++) - - @Override - void configurePreAgent() { - super.configurePreAgent() - - injectSysConfig("dd.integration.opentelemetry.experimental.enabled", "true") - injectSysConfig("dd.trace.propagation.style", style()) - } - - /** - * Gets the propagation style to configure to the agent. - * @return The propagation style. - */ - abstract String style() - - /** - * Gets the propagator to test. - * @return The propagator to test. - */ - abstract TextMapPropagator propagator() - - /** - * Get the test values as an array: - *
    - *
  1. Headers map
  2. - *
  3. Trace id
  4. - *
  5. Span id
  6. - *
  7. Whether the parent span is sampled
  8. - *
- * - * @return The tests values. - */ - abstract values() - - /** - * Evaluates the injected headers of a child span from a continuing trace. - * @param headers The injected headers. - * @param traceId The continued trace identifier. - * @param spanId The child span identifier. - * @param sampling The sampling decision. - */ - abstract void assertInjectedHeaders(Map headers, String traceId, String spanId, byte sampling) - - def "test context extraction and injection"() { - setup: - def propagator = propagator() - def expectedSampled = sampling == SAMPLER_KEEP - - when: - def context = propagator.extract(Context.root(), headers, TextMap.INSTANCE) - - then: - context != Context.root() - - when: - def localSpan = tracer.spanBuilder("some-name") - .setParent(context) - .startSpan() - def localSpanContext = localSpan.getSpanContext() - def localSpanId = localSpanContext.getSpanId() - def spanSampled = localSpanContext.getTraceFlags().isSampled() - def scope = localSpan.makeCurrent() - Map injectedHeaders = [:] - propagator.inject(Context.current(), injectedHeaders, new TextMap()) - scope.close() - localSpan.end() - - then: - assertTraces(1) { - trace(1) { - span { - operationName "internal" - resourceName "some-name" - traceDDId(expectedTraceId(traceId)) - parentSpanId(DDSpanId.fromHex(spanId).toLong() as BigInteger) - } - } - } - spanSampled == expectedSampled - assertInjectedHeaders(injectedHeaders, traceId, localSpanId, sampling) - - where: - values << values() - (headers, traceId, spanId, sampling) = values - } - - def expectedTraceId(String traceId) { - return DDTraceId.fromHex(traceId) - } - - @Override - void cleanup() { - // Test for context leak - assert Context.current() == Context.root() - // Safely reset OTel context storage - ThreadLocalContextStorage.THREAD_LOCAL_STORAGE.remove() - } -} diff --git a/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.4/src/test/groovy/opentelemetry14/context/propagation/AgentPropagatorTest.groovy b/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.4/src/test/groovy/opentelemetry14/context/propagation/AgentPropagatorTest.groovy deleted file mode 100644 index f221ddf19da..00000000000 --- a/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.4/src/test/groovy/opentelemetry14/context/propagation/AgentPropagatorTest.groovy +++ /dev/null @@ -1,12 +0,0 @@ -package opentelemetry14.context.propagation - -import io.opentelemetry.api.GlobalOpenTelemetry -import io.opentelemetry.context.propagation.TextMapPropagator - -abstract class AgentPropagatorTest extends AbstractPropagatorTest { - @Override - TextMapPropagator propagator() { - // Get agent propagator injected by instrumentation - return GlobalOpenTelemetry.get().getPropagators().getTextMapPropagator() - } -} diff --git a/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.4/src/test/groovy/opentelemetry14/context/propagation/B3MultiPropagatorTest.groovy b/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.4/src/test/groovy/opentelemetry14/context/propagation/B3MultiPropagatorTest.groovy deleted file mode 100644 index 9c4ef404225..00000000000 --- a/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.4/src/test/groovy/opentelemetry14/context/propagation/B3MultiPropagatorTest.groovy +++ /dev/null @@ -1,43 +0,0 @@ -package opentelemetry14.context.propagation - -import datadog.trace.core.propagation.B3TraceId - -import static datadog.trace.api.sampling.PrioritySampling.SAMPLER_DROP -import static datadog.trace.api.sampling.PrioritySampling.SAMPLER_KEEP -import static datadog.trace.api.sampling.PrioritySampling.UNSET -import static datadog.trace.core.propagation.B3HttpCodec.SAMPLING_PRIORITY_KEY -import static datadog.trace.core.propagation.B3HttpCodec.SPAN_ID_KEY -import static datadog.trace.core.propagation.B3HttpCodec.TRACE_ID_KEY - -class B3MultiPropagatorTest extends AgentPropagatorTest { - @Override - String style() { - return 'b3multi' - } - - @Override - def values() { - // spotless:off - return [ - [[(TRACE_ID_KEY): '1', (SPAN_ID_KEY):'2'], '1' , '2' , UNSET], - [[(TRACE_ID_KEY): '1111111111111111', (SPAN_ID_KEY):'2222222222222222'], '1111111111111111' , '2222222222222222', UNSET], - [[(TRACE_ID_KEY): '11111111111111111111111111111111', (SPAN_ID_KEY):'2222222222222222'], '11111111111111111111111111111111', '2222222222222222', UNSET], - [[(TRACE_ID_KEY): '11111111111111111111111111111111', (SPAN_ID_KEY):'2222222222222222', (SAMPLING_PRIORITY_KEY): '0'], '11111111111111111111111111111111', '2222222222222222', SAMPLER_DROP], - [[(TRACE_ID_KEY): '11111111111111111111111111111111', (SPAN_ID_KEY):'2222222222222222', (SAMPLING_PRIORITY_KEY): '1'], '11111111111111111111111111111111', '2222222222222222', SAMPLER_KEEP], - ] - // spotless:on - } - - @Override - def expectedTraceId(String traceId) { - return B3TraceId.fromHex(traceId) - } - - @Override - void assertInjectedHeaders(Map headers, String traceId, String spanId, byte sampling) { - def priorityKey = sampling == SAMPLER_DROP ? '0' : '1' // Deterministic sampler with rate to 1 if not explicitly dropped - assert headers[TRACE_ID_KEY] == traceId.padLeft(32, '0') - assert headers[SPAN_ID_KEY] == spanId.padLeft(8, '0') - assert headers[SAMPLING_PRIORITY_KEY] == priorityKey - } -} diff --git a/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.4/src/test/groovy/opentelemetry14/context/propagation/B3SinglePropagatorTest.groovy b/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.4/src/test/groovy/opentelemetry14/context/propagation/B3SinglePropagatorTest.groovy deleted file mode 100644 index 959183ee1c6..00000000000 --- a/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.4/src/test/groovy/opentelemetry14/context/propagation/B3SinglePropagatorTest.groovy +++ /dev/null @@ -1,40 +0,0 @@ -package opentelemetry14.context.propagation - - -import datadog.trace.core.propagation.B3TraceId - -import static datadog.trace.api.sampling.PrioritySampling.SAMPLER_DROP -import static datadog.trace.api.sampling.PrioritySampling.SAMPLER_KEEP -import static datadog.trace.api.sampling.PrioritySampling.UNSET -import static datadog.trace.core.propagation.B3HttpCodec.B3_KEY - -class B3SinglePropagatorTest extends AgentPropagatorTest { - @Override - String style() { - return 'b3single' - } - - @Override - def values() { - // spotless:off - return [ - [[(B3_KEY): '1-2'], '1', '2', UNSET], - [[(B3_KEY): '1111111111111111-2222222222222222'], '1111111111111111', '2222222222222222', UNSET], - [[(B3_KEY): '11111111111111111111111111111111-2222222222222222'], '11111111111111111111111111111111', '2222222222222222', UNSET], - [[(B3_KEY): '11111111111111111111111111111111-2222222222222222-0'], '11111111111111111111111111111111', '2222222222222222', SAMPLER_DROP], - [[(B3_KEY): '11111111111111111111111111111111-2222222222222222-1'], '11111111111111111111111111111111', '2222222222222222', SAMPLER_KEEP], - ] - // spotless:on - } - - @Override - def expectedTraceId(String traceId) { - return B3TraceId.fromHex(traceId) - } - - @Override - void assertInjectedHeaders(Map headers, String traceId, String spanId, byte sampling) { - def sampledValue = sampling == SAMPLER_DROP ? '0' : '1' // Deterministic sampler with rate to 1 if not explicitly dropped - assert headers[B3_KEY] == "${traceId.padLeft(32, '0')}-${spanId.padLeft(8, '0')}-$sampledValue" - } -} diff --git a/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.4/src/test/groovy/opentelemetry14/context/propagation/DatadogPropagatorTest.groovy b/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.4/src/test/groovy/opentelemetry14/context/propagation/DatadogPropagatorTest.groovy deleted file mode 100644 index 43355b189cb..00000000000 --- a/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.4/src/test/groovy/opentelemetry14/context/propagation/DatadogPropagatorTest.groovy +++ /dev/null @@ -1,46 +0,0 @@ -package opentelemetry14.context.propagation - - -import datadog.trace.api.DDTraceId - -import static datadog.trace.api.sampling.PrioritySampling.SAMPLER_DROP -import static datadog.trace.api.sampling.PrioritySampling.SAMPLER_KEEP -import static datadog.trace.api.sampling.PrioritySampling.UNSET -import static java.lang.Long.parseLong - -class DatadogPropagatorTest extends AgentPropagatorTest { - @Override - String style() { - return 'datadog' - } - - def values() { - // spotless:off - return [ - [['x-datadog-trace-id': "${parseLong('1111111111111111', 16)}", 'x-datadog-parent-id': "${parseLong('2222222222222222', 16)}"], '1111111111111111', '2222222222222222', UNSET], - [['x-datadog-trace-id': "${parseLong('1111111111111111', 16)}", 'x-datadog-parent-id': "${parseLong('2222222222222222', 16)}", 'x-datadog-tags': '_dd.p.tid=1111111111111111'], '11111111111111111111111111111111', '2222222222222222', UNSET], - [['x-datadog-trace-id': "${parseLong('1111111111111111', 16)}", 'x-datadog-parent-id': "${parseLong('2222222222222222', 16)}", 'x-datadog-sampling-priority': "$SAMPLER_KEEP", 'x-datadog-tags': '_dd.p.tid=1111111111111111'], '11111111111111111111111111111111', '2222222222222222', SAMPLER_KEEP], - [['x-datadog-trace-id': "${parseLong('1111111111111111', 16)}", 'x-datadog-parent-id': "${parseLong('2222222222222222', 16)}", 'x-datadog-sampling-priority': "$UNSET", 'x-datadog-tags': '_dd.p.tid=1111111111111111'], '11111111111111111111111111111111', '2222222222222222', UNSET], - [['x-datadog-trace-id': "${parseLong('1111111111111111', 16)}", 'x-datadog-parent-id': "${parseLong('2222222222222222', 16)}", 'x-datadog-sampling-priority': "$SAMPLER_DROP", 'x-datadog-tags': '_dd.p.tid=1111111111111111'], '11111111111111111111111111111111', '2222222222222222', SAMPLER_DROP], - ] - // spotless:on - } - - void assertInjectedHeaders(Map headers, String traceId, String spanId, byte sampling) { - assert headers['x-datadog-trace-id'] == Long.toString(DDTraceId.fromHex(traceId).toLong()) - assert headers['x-datadog-parent-id'] == spanId.replaceAll('^0+(?!$)', '') - def tags = [] - def samplingPriority = sampling == SAMPLER_DROP ? '0' : '1' // Deterministic sampler with rate to 1 if not explicitly dropped - if (sampling == UNSET) { - tags+= '_dd.p.dm=-1' - } - if (traceId.length() == 32) { - tags+= '_dd.p.tid='+ traceId.substring(0, 16) - } - if (sampling == UNSET) { - tags+= '_dd.p.ksr=1' - } - assert headers['x-datadog-tags'] == tags.join(',') - assert headers['x-datadog-sampling-priority'] == samplingPriority - } -} diff --git a/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.4/src/test/groovy/opentelemetry14/context/propagation/MissingTraceContextPropagatorTest.groovy b/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.4/src/test/groovy/opentelemetry14/context/propagation/MissingTraceContextPropagatorTest.groovy deleted file mode 100644 index 9bf0c0f2b4a..00000000000 --- a/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.4/src/test/groovy/opentelemetry14/context/propagation/MissingTraceContextPropagatorTest.groovy +++ /dev/null @@ -1,38 +0,0 @@ -package opentelemetry14.context.propagation - -import datadog.trace.agent.test.InstrumentationSpecification -import io.opentelemetry.api.GlobalOpenTelemetry -import io.opentelemetry.api.trace.Span -import io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator -import io.opentelemetry.context.propagation.TextMapPropagator - -import static io.opentelemetry.context.Context.root - -class MissingTraceContextPropagatorTest extends InstrumentationSpecification { - @Override - void configurePreAgent() { - super.configurePreAgent() - - injectSysConfig("dd.integration.opentelemetry.experimental.enabled", "true") - } - - def "extract on missing tracecontext should return an empty context"(TextMapPropagator propagator) { - setup: - def headers = ["User-Agent":"test"] - - when: - def context = propagator.extract(root(), headers, TextMap.INSTANCE) - def extractedSpan = Span.fromContext(context) - - then: "Should not have a valid tracing context" - extractedSpan != null - !extractedSpan.spanContext.valid - Span.fromContextOrNull(context) == null - - where: - propagator << [ - GlobalOpenTelemetry.get().getPropagators().getTextMapPropagator(), - W3CTraceContextPropagator.getInstance() - ] - } -} diff --git a/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.4/src/test/groovy/opentelemetry14/context/propagation/OtelW3cPropagatorTest.groovy b/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.4/src/test/groovy/opentelemetry14/context/propagation/OtelW3cPropagatorTest.groovy deleted file mode 100644 index 62516df7cb5..00000000000 --- a/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.4/src/test/groovy/opentelemetry14/context/propagation/OtelW3cPropagatorTest.groovy +++ /dev/null @@ -1,36 +0,0 @@ -package opentelemetry14.context.propagation - -import io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator -import io.opentelemetry.context.propagation.TextMapPropagator - -import static datadog.trace.api.sampling.PrioritySampling.SAMPLER_KEEP -import static datadog.trace.api.sampling.PrioritySampling.UNSET - -class OtelW3cPropagatorTest extends AbstractPropagatorTest { - @Override - String style() { - return 'datadog' - } - - @Override - TextMapPropagator propagator() { - // OpenTelemetry API W3C propagator - return W3CTraceContextPropagator.getInstance() - } - - @Override - def values() { - // spotless:off - return [ - [['traceparent': '00-00000000000000001111111111111111-2222222222222222-00'], '00000000000000001111111111111111', '2222222222222222', UNSET], - [['traceparent': '00-00000000000000001111111111111111-2222222222222222-01'], '00000000000000001111111111111111', '2222222222222222', SAMPLER_KEEP], - ] - // spotless:on - } - - @Override - void assertInjectedHeaders(Map headers, String traceId, String spanId, byte sampling) { - def sampleFlag = sampling == SAMPLER_KEEP ? '01' : '00' - assert headers['traceparent'] == "00-$traceId-$spanId-$sampleFlag" - } -} diff --git a/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.4/src/test/groovy/opentelemetry14/context/propagation/TextMap.groovy b/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.4/src/test/groovy/opentelemetry14/context/propagation/TextMap.groovy deleted file mode 100644 index d876b61a397..00000000000 --- a/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.4/src/test/groovy/opentelemetry14/context/propagation/TextMap.groovy +++ /dev/null @@ -1,25 +0,0 @@ -package opentelemetry14.context.propagation - -import io.opentelemetry.context.propagation.TextMapGetter -import io.opentelemetry.context.propagation.TextMapSetter - -import javax.annotation.Nullable - -class TextMap implements TextMapGetter>, TextMapSetter> { - static final INSTANCE = new TextMap() - - @Override - Iterable keys(Map carrier) { - return carrier.keySet() - } - - @Override - String get(@Nullable Map carrier, String key) { - return carrier.get(key) - } - - @Override - void set(@Nullable Map carrier, String key, String value) { - carrier.put(key, value) - } -} diff --git a/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.4/src/test/groovy/opentelemetry14/context/propagation/W3cPropagatorTest.groovy b/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.4/src/test/groovy/opentelemetry14/context/propagation/W3cPropagatorTest.groovy deleted file mode 100644 index ebf31c3c84d..00000000000 --- a/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.4/src/test/groovy/opentelemetry14/context/propagation/W3cPropagatorTest.groovy +++ /dev/null @@ -1,27 +0,0 @@ -package opentelemetry14.context.propagation - -import static datadog.trace.api.sampling.PrioritySampling.SAMPLER_KEEP -import static datadog.trace.api.sampling.PrioritySampling.UNSET - -class W3cPropagatorTest extends AgentPropagatorTest { - @Override - String style() { - return 'tracecontext' - } - - @Override - def values() { - // spotless:off - return [ - [['traceparent': '00-11111111111111111111111111111111-2222222222222222-00'], '11111111111111111111111111111111', '2222222222222222', UNSET], - [['traceparent': '00-11111111111111111111111111111111-2222222222222222-01'], '11111111111111111111111111111111', '2222222222222222', SAMPLER_KEEP], - ] - // spotless:on - } - - @Override - void assertInjectedHeaders(Map headers, String traceId, String spanId, byte sampling) { - def traceFlags = sampling == SAMPLER_KEEP ? '01' : '00' - assert headers['traceparent'] == "00-$traceId-$spanId-$traceFlags" - } -} diff --git a/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.4/src/test/groovy/opentelemetry14/context/propagation/W3cPropagatorTracestateTest.groovy b/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.4/src/test/groovy/opentelemetry14/context/propagation/W3cPropagatorTracestateTest.groovy deleted file mode 100644 index fe08288f85a..00000000000 --- a/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.4/src/test/groovy/opentelemetry14/context/propagation/W3cPropagatorTracestateTest.groovy +++ /dev/null @@ -1,78 +0,0 @@ -package opentelemetry14.context.propagation - -import datadog.trace.agent.test.InstrumentationSpecification -import io.opentelemetry.api.GlobalOpenTelemetry -import spock.lang.Subject - -import static io.opentelemetry.context.Context.current -import static io.opentelemetry.context.Context.root - -class W3cPropagatorTracestateTest extends InstrumentationSpecification { - @Subject - def tracer = GlobalOpenTelemetry.get().tracerProvider.get("tracecontext-propagator-tracestate") - - @Override - void configurePreAgent() { - super.configurePreAgent() - - injectSysConfig("dd.integration.opentelemetry.experimental.enabled", "true") - injectSysConfig("dd.trace.propagation.style", "tracecontext") - } - - def "test tracestate propagation"() { - setup: - // Get agent propagator injected by instrumentation - def propagator = GlobalOpenTelemetry.get().getPropagators().getTextMapPropagator() - def headers = [ - 'traceparent': '00-11111111111111111111111111111111-2222222222222222-00' - ] - def members = new String[0] - if (tracestate) { - headers['tracestate'] = tracestate - members = Arrays.stream(tracestate.split(',')) - .filter { - !it.startsWith("dd=") - } - .toArray(String[]::new) - } - - when: - def context = propagator.extract(root(), headers, TextMap.INSTANCE) - - then: - context != root() - - when: - def localSpan = tracer.spanBuilder("some-name") - .setParent(context) - .startSpan() - def scope = localSpan.makeCurrent() - Map injectedHeaders = [:] - propagator.inject(current(), injectedHeaders, TextMap.INSTANCE) - scope.close() - localSpan.end() - - then: - // Check tracestate was injected - def injectedTracestate = injectedHeaders['tracestate'] - injectedTracestate != null - // Check tracestate contains extracted members plus the Datadog one in first position - def injectedMembers = injectedTracestate.split(',') - injectedMembers.length == Math.min(1 + members.length, 32) - // Check datadog member (should be injected as first member) - injectedMembers[0] == "dd=s:0;p:${localSpan.spanContext.spanId};t.tid:1111111111111111" - // Check all other members - for (int i = 0; i< Math.min(members.length, 31); i++) { - assert injectedMembers[i+1] == members[i] - } - - where: - // spotless:off - tracestate << [ - "foo=1,bar=2", - "dd=s:0,foo=1,bar=2", - "foo=1,dd=s:0,bar=2", - ] - // spotless:on - } -} diff --git a/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.4/src/test/java/datadog/opentelemetry/shim/trace/OtelSpanEventTestHelper.java b/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.4/src/test/java/datadog/opentelemetry/shim/trace/OtelSpanEventTestHelper.java new file mode 100644 index 00000000000..a8c97ca719e --- /dev/null +++ b/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.4/src/test/java/datadog/opentelemetry/shim/trace/OtelSpanEventTestHelper.java @@ -0,0 +1,8 @@ +package datadog.opentelemetry.shim.trace; + +/** Test helper providing package-level access to {@link OtelSpanEvent} internals. */ +public class OtelSpanEventTestHelper { + public static String stringifyErrorStack(Throwable error) { + return OtelSpanEvent.stringifyErrorStack(error); + } +} diff --git a/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.4/src/test/java/opentelemetry14/AbstractOpenTelemetry14Test.java b/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.4/src/test/java/opentelemetry14/AbstractOpenTelemetry14Test.java new file mode 100644 index 00000000000..34664444a14 --- /dev/null +++ b/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.4/src/test/java/opentelemetry14/AbstractOpenTelemetry14Test.java @@ -0,0 +1,54 @@ +package opentelemetry14; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import datadog.trace.agent.test.AbstractInstrumentationTest; +import datadog.trace.junit.utils.config.WithConfig; +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.context.Context; +import java.lang.reflect.Field; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; + +/** + * Base class for OpenTelemetry 1.4 instrumentation tests with: + * + *
    + *
  • Instrumentation enabled using config + *
  • OTel {@link Tracer} setup before each test + *
  • Context leak detection and storage cleanup after each test + *
+ */ +@WithConfig(key = "integration.opentelemetry.experimental.enabled", value = "true") +public abstract class AbstractOpenTelemetry14Test extends AbstractInstrumentationTest { + private static int tracerInstance; + + protected Tracer otelTracer; + + @BeforeEach + void setupOtelTracer() { + this.otelTracer = + GlobalOpenTelemetry.get().getTracerProvider().get("test-tracer-" + tracerInstance++); + } + + @AfterEach + void checkOtelContextAndCleanup() { + try { + assertEquals(Context.current(), Context.root(), "OTel context leak detected"); + } finally { + clearContextStorage(); + } + } + + private static void clearContextStorage() { + try { + Class storageClass = Class.forName("io.opentelemetry.context.ThreadLocalContextStorage"); + Field field = storageClass.getDeclaredField("THREAD_LOCAL_STORAGE"); + field.setAccessible(true); + ((ThreadLocal) field.get(null)).remove(); + } catch (ReflectiveOperationException e) { + throw new AssertionError("Failed to clear OTel context storage", e); + } + } +} diff --git a/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.4/src/test/java/opentelemetry14/OpenTelemetry14ActivationTest.java b/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.4/src/test/java/opentelemetry14/OpenTelemetry14ActivationTest.java new file mode 100644 index 00000000000..d2eb61d0ad2 --- /dev/null +++ b/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.4/src/test/java/opentelemetry14/OpenTelemetry14ActivationTest.java @@ -0,0 +1,66 @@ +package opentelemetry14; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +import datadog.trace.junit.utils.config.WithConfig; +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanBuilder; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.context.Context; +import org.junit.jupiter.api.Test; + +abstract class OpenTelemetry14ActivationTest extends AbstractOpenTelemetry14Test { + + abstract boolean shouldBeInjected(); + + @Test + void testInstrumentationInjection() { + Tracer tracer = GlobalOpenTelemetry.get().getTracerProvider().get("some-instrumentation"); + SpanBuilder builder = tracer.spanBuilder("some-name"); + Span result = builder.startSpan(); + Context context = Context.current(); + + if (shouldBeInjected()) { + assertImplementation(tracer, "OtelTracer"); + assertImplementation(builder, "OtelSpanBuilder"); + assertImplementation(result, "OtelSpan"); + assertImplementation(context, "OtelContext"); + } else { + assertImplementation(tracer, "DefaultTracer"); + assertImplementation(context, "ArrayBasedContext"); + } + } + + private static void assertImplementation(Object instance, String expectedClassName) { + String actualClassName = instance.getClass().getName(); + assertTrue( + actualClassName.endsWith("." + expectedClassName), + "Expected " + expectedClassName + " but got " + actualClassName); + } +} + +// Forked test variants: each runs in its own JVM to allow GlobalOpenTelemetry static state to reset + +class OpenTelemetry14ActivationByInstrumentationNameForkedTest + extends OpenTelemetry14ActivationTest { + @Override + boolean shouldBeInjected() { + return true; + } +} + +@WithConfig(key = "trace.otel.enabled", value = "true") +class OpenTelemetry14ActivationByOtelRfcNameForkedTest extends OpenTelemetry14ActivationTest { + @Override + boolean shouldBeInjected() { + return true; + } +} + +class OpenTelemetry14DisableByDefaultForkedTest extends OpenTelemetry14ActivationTest { + @Override + boolean shouldBeInjected() { + return false; + } +} diff --git a/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.4/src/test/java/opentelemetry14/OpenTelemetry14ConventionsTest.java b/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.4/src/test/java/opentelemetry14/OpenTelemetry14ConventionsTest.java new file mode 100644 index 00000000000..1590561974f --- /dev/null +++ b/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.4/src/test/java/opentelemetry14/OpenTelemetry14ConventionsTest.java @@ -0,0 +1,325 @@ +package opentelemetry14; + +import static datadog.trace.agent.test.assertions.Matchers.is; +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; +import static datadog.trace.agent.test.assertions.TraceMatcher.trace; +import static datadog.trace.api.DDTags.ANALYTICS_SAMPLE_RATE; +import static datadog.trace.api.DDTags.DD_SVC_SRC; +import static datadog.trace.bootstrap.instrumentation.api.Tags.HTTP_STATUS; +import static datadog.trace.bootstrap.instrumentation.api.Tags.SPAN_KIND; +import static io.opentelemetry.api.common.AttributeKey.longKey; +import static io.opentelemetry.api.common.AttributeKey.stringKey; +import static io.opentelemetry.api.trace.SpanKind.CLIENT; +import static io.opentelemetry.api.trace.SpanKind.CONSUMER; +import static io.opentelemetry.api.trace.SpanKind.INTERNAL; +import static io.opentelemetry.api.trace.SpanKind.PRODUCER; +import static io.opentelemetry.api.trace.SpanKind.SERVER; +import static java.util.Collections.emptyMap; +import static org.junit.jupiter.params.provider.Arguments.arguments; + +import datadog.opentelemetry.shim.trace.OtelConventions; +import datadog.trace.agent.test.assertions.Matcher; +import datadog.trace.agent.test.assertions.Matchers; +import datadog.trace.agent.test.assertions.TagsMatcher; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanBuilder; +import io.opentelemetry.api.trace.SpanKind; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Stream; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +public class OpenTelemetry14ConventionsTest extends AbstractOpenTelemetry14Test { + private static final String SPAN_KIND_INTERNAL = "internal"; + private static final String OPERATION_NAME_SPECIFIC_ATTRIBUTE = "operation.name"; + + static Stream testSpanNameConventionsArguments() { + return Stream.of( + // Fallback behavior + arguments(null, emptyMap(), "internal"), + // Internal spans + arguments(INTERNAL, emptyMap(), "internal"), + // Server spans + arguments(SERVER, emptyMap(), "server.request"), + arguments(SERVER, attributes("http.request.method", "GET"), "http.server.request"), + arguments(SERVER, attributes("http.request.method", "GET"), "http.server.request"), + arguments(SERVER, attributes("network.protocol.name", "amqp"), "amqp.server.request"), + // Client spans + arguments(CLIENT, emptyMap(), "client.request"), + arguments(CLIENT, attributes("http.request.method", "GET"), "http.client.request"), + arguments(CLIENT, attributes("db.system", "mysql"), "mysql.query"), + arguments(CLIENT, attributes("network.protocol.name", "amqp"), "amqp.client.request"), + arguments(CLIENT, attributes("network.protocol.name", "AMQP"), "amqp.client.request"), + // Messaging spans + arguments(PRODUCER, emptyMap(), "producer"), + arguments(CONSUMER, emptyMap(), "consumer"), + arguments( + CONSUMER, + attributes("messaging.system", "rabbitmq", "messaging.operation", "publish"), + "rabbitmq.publish"), + arguments( + PRODUCER, + attributes("messaging.system", "rabbitmq", "messaging.operation", "publish"), + "rabbitmq.publish"), + arguments( + CLIENT, + attributes("messaging.system", "rabbitmq", "messaging.operation", "publish"), + "rabbitmq.publish"), + arguments( + SERVER, + attributes("messaging.system", "rabbitmq", "messaging.operation", "publish"), + "rabbitmq.publish"), + // RPC spans + arguments(CLIENT, attributes("rpc.system", "grpc"), "grpc.client.request"), + arguments(SERVER, attributes("rpc.system", "grpc"), "grpc.server.request"), + arguments(CLIENT, attributes("rpc.system", "aws-api"), "aws.client.request"), + arguments( + CLIENT, + attributes("rpc.system", "aws-api", "rpc.service", "helloworld"), + "aws.helloworld.request"), + arguments(SERVER, attributes("rpc.system", "aws-api"), "aws-api.server.request"), + // FAAS spans + arguments( + CLIENT, + attributes( + "faas.invoked_provider", "alibaba_cloud", "faas.invoked_name", "my-function"), + "alibaba_cloud.my-function.invoke"), + arguments(SERVER, attributes("faas.trigger", "datasource"), "datasource.invoke"), + // GraphQL spans + arguments( + INTERNAL, attributes("graphql.operation.type", "query"), "graphql.server.request"), + arguments(null, attributes("graphql.operation.type", "query"), "graphql.server.request"), + // User override + arguments( + CLIENT, attributes("db.system", "mysql", "operation.name", "db.query"), "db.query"), + arguments( + CLIENT, attributes("db.system", "mysql", "operation.name", "DB.query"), "db.query")); + } + + @ParameterizedTest(name = "[{index}] {0} {1} -> {2}") + @MethodSource("testSpanNameConventionsArguments") + void testSpanNameConventions( + SpanKind kind, Map attributes, String expectedOperationName) { + SpanBuilder builder = this.otelTracer.spanBuilder("some-name"); + if (kind != null) { + builder.setSpanKind(kind); + } + attributes.forEach(builder::setAttribute); + builder.startSpan().end(); + + String expectedSpanKindTag = OtelConventions.toSpanKindTagValue(kind == null ? INTERNAL : kind); + List tagMatchers = new ArrayList<>(); + tagMatchers.add(defaultTags()); + tagMatchers.add(tag(SPAN_KIND, is(expectedSpanKindTag))); + attributes.forEach( + (key, value) -> { + if (!OPERATION_NAME_SPECIFIC_ATTRIBUTE.equals(key)) { + tagMatchers.add(tag(key, is(value))); + } + }); + + assertTraces( + trace( + span() + .root() + .operationName(expectedOperationName) + .resourceName("some-name") + .tags(tagMatchers.toArray(new TagsMatcher[0])))); + } + + static Stream testSpanSpecificTagsArguments() { + return Stream.of( + arguments(true, true), + arguments(true, false), + arguments(false, true), + arguments(false, false)); + } + + @ParameterizedTest(name = "[{index}] setInBuilder={0} useAttributeKey={1}") + @MethodSource("testSpanSpecificTagsArguments") + void testSpanSpecificTags(boolean setInBuilder, boolean useAttributeKey) { + SpanBuilder builder = this.otelTracer.spanBuilder("some-name"); + + if (setInBuilder) { + if (useAttributeKey) { + builder + .setAttribute(stringKey("operation.name"), "my-operation") + .setAttribute(stringKey("service.name"), "my-service") + .setAttribute(stringKey("resource.name"), "/my-resource") + .setAttribute(stringKey("span.type"), "http"); + } else { + builder + .setAttribute("operation.name", "my-operation") + .setAttribute("service.name", "my-service") + .setAttribute("resource.name", "/my-resource") + .setAttribute("span.type", "http"); + } + } + Span result = builder.startSpan(); + if (!setInBuilder) { + if (useAttributeKey) { + result + .setAttribute(stringKey("operation.name"), "my-operation") + .setAttribute(stringKey("service.name"), "my-service") + .setAttribute(stringKey("resource.name"), "/my-resource") + .setAttribute(stringKey("span.type"), "http"); + } else { + result + .setAttribute("operation.name", "my-operation") + .setAttribute("service.name", "my-service") + .setAttribute("resource.name", "/my-resource") + .setAttribute("span.type", "http"); + } + } + result.end(); + + assertTraces( + trace( + span() + .root() + .operationName("my-operation") + .resourceName("/my-resource") + .serviceName("my-service") + .type("http") + .tags( + defaultTags(), + tag(SPAN_KIND, is(SPAN_KIND_INTERNAL)), + tag(DD_SVC_SRC, isManuallySet())))); + } + + static Stream testSpanAnalyticsEventSpecificTagArguments() { + return Stream.of( + arguments(true, true, 1), + arguments(true, Boolean.TRUE, 1), + arguments(true, false, 0), + arguments(true, Boolean.FALSE, 0), + arguments(true, null, 0), + arguments(true, "true", 1), + arguments(true, "false", 0), + arguments(true, "TRUE", 1), + arguments(true, "something-else", 0), + arguments(true, "", 0), + arguments(false, true, 1), + arguments(false, Boolean.TRUE, 1), + arguments(false, false, 0), + arguments(false, Boolean.FALSE, 0), + arguments(false, null, 0), + arguments(false, "true", 1), + arguments(false, "false", 0), + arguments(false, "TRUE", 1), + arguments(false, "something-else", 0), + arguments(false, "", 0)); + } + + @ParameterizedTest(name = "[{index}] setInBuilder={0} value={1}") + @MethodSource("testSpanAnalyticsEventSpecificTagArguments") + void testSpanAnalyticsEventSpecificTag(boolean setInBuilder, Object value, int expectedMetric) { + SpanBuilder builder = this.otelTracer.spanBuilder("some-name"); + + if (setInBuilder) { + if (value instanceof Boolean) { + builder.setAttribute("analytics.event", (boolean) value); + } else if (value instanceof String) { + builder.setAttribute("analytics.event", (String) value); + } + // null case: don't set anything + } + Span result = builder.startSpan(); + if (!setInBuilder) { + if (value instanceof Boolean) { + result.setAttribute("analytics.event", (boolean) value); + } else if (value instanceof String) { + result.setAttribute("analytics.event", (String) value); + } + // null case: don't set anything + } + result.end(); + + List tagMatchers = new ArrayList<>(); + tagMatchers.add(defaultTags()); + tagMatchers.add(tag(SPAN_KIND, is(SPAN_KIND_INTERNAL))); + if (value != null) { + tagMatchers.add(tag(ANALYTICS_SAMPLE_RATE, is(expectedMetric))); + } + assertTraces( + trace( + span().root().operationName("internal").tags(tagMatchers.toArray(new TagsMatcher[0])))); + } + + static Stream testSpanHttpResponseStatusCodeSpecificTagArguments() { + return Stream.of( + arguments(true, false, null, 0), + arguments(true, false, 200, 200), + arguments(true, false, 404L, 404), + arguments(true, false, (long) 500, 500), + arguments(false, false, null, 0), + arguments(false, false, 200, 200), + arguments(false, false, 404L, 404), + arguments(false, false, (long) 500, 500), + arguments(true, true, null, 0), + arguments(true, true, 200, 200), + arguments(true, true, 404L, 404), + arguments(true, true, (long) 500, 500), + arguments(false, true, null, 0), + arguments(false, true, 200, 200), + arguments(false, true, 404L, 404), + arguments(false, true, (long) 500, 500)); + } + + @ParameterizedTest(name = "[{index}] setInBuilder={0} attributeKey={1} value={2}") + @MethodSource("testSpanHttpResponseStatusCodeSpecificTagArguments") + void testSpanHttpResponseStatusCodeSpecificTag( + boolean setInBuilder, boolean attributeKey, Object value, int expectedStatus) { + SpanBuilder builder = this.otelTracer.spanBuilder("some-name"); + + if (setInBuilder) { + if (value != null) { + if (attributeKey) { + builder.setAttribute(longKey("http.response.status_code"), ((Number) value).longValue()); + } else { + builder.setAttribute("http.response.status_code", ((Number) value).longValue()); + } + } + } + Span result = builder.startSpan(); + if (!setInBuilder) { + if (value != null) { + if (attributeKey) { + result.setAttribute(longKey("http.response.status_code"), ((Number) value).longValue()); + } else { + result.setAttribute("http.response.status_code", ((Number) value).longValue()); + } + } + } + result.end(); + + List tagMatchers = new ArrayList<>(); + tagMatchers.add(defaultTags()); + tagMatchers.add(tag(SPAN_KIND, is(SPAN_KIND_INTERNAL))); + if (value != null) { + tagMatchers.add(tag(HTTP_STATUS, is(expectedStatus))); + } + + assertTraces( + trace( + span().root().operationName("internal").tags(tagMatchers.toArray(new TagsMatcher[0])))); + } + + static Map attributes(String... keyValues) { + Map map = new HashMap<>(); + for (int i = 0; i + 1 < keyValues.length; i += 2) { + map.put(keyValues[i], keyValues[i + 1]); + } + return map; + } + + static Matcher isManuallySet() { + return Matchers.validates(o -> "m".equals(o.toString())); + } +} 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 new file mode 100644 index 00000000000..c3c9ea09c9a --- /dev/null +++ b/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.4/src/test/java/opentelemetry14/OpenTelemetry14Test.java @@ -0,0 +1,854 @@ +package opentelemetry14; + +import static datadog.opentelemetry.shim.trace.OtelSpanEventTestHelper.stringifyErrorStack; +import static datadog.trace.agent.test.assertions.Matchers.any; +import static datadog.trace.agent.test.assertions.Matchers.is; +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; +import static datadog.trace.agent.test.assertions.TraceMatcher.trace; +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.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 io.opentelemetry.api.common.AttributeKey.booleanArrayKey; +import static io.opentelemetry.api.common.AttributeKey.doubleArrayKey; +import static io.opentelemetry.api.common.AttributeKey.longArrayKey; +import static io.opentelemetry.api.common.AttributeKey.stringArrayKey; +import static io.opentelemetry.api.common.AttributeKey.stringKey; +import static io.opentelemetry.api.trace.SpanKind.CLIENT; +import static io.opentelemetry.api.trace.SpanKind.CONSUMER; +import static io.opentelemetry.api.trace.SpanKind.INTERNAL; +import static io.opentelemetry.api.trace.SpanKind.PRODUCER; +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.lang.String.join; +import static java.util.Arrays.asList; +import static java.util.Collections.emptyList; +import static java.util.concurrent.TimeUnit.MILLISECONDS; +import static java.util.concurrent.TimeUnit.NANOSECONDS; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.params.provider.Arguments.arguments; + +import datadog.opentelemetry.shim.trace.OtelSpanEvent; +import datadog.trace.agent.test.assertions.Matcher; +import datadog.trace.agent.test.assertions.Matchers; +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.api.time.ControllableTimeSource; +import datadog.trace.bootstrap.instrumentation.api.WithAgentSpan; +import datadog.trace.core.DDSpan; +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanBuilder; +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.api.trace.TraceFlags; +import io.opentelemetry.api.trace.TraceState; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.context.propagation.TextMapPropagator; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.stream.Stream; +import opentelemetry14.context.propagation.TextMap; +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; + +public class OpenTelemetry14Test extends AbstractOpenTelemetry14Test { + private static final String SPAN_KIND_INTERNAL = "internal"; + private static final long TIME_MILLIS = 1723220824705L; + private static final long TIME_NANO = TIME_MILLIS * 1_000_000L; + + @Test + void testParentSpanUsingActiveSpan() { + Span parentSpan = this.otelTracer.spanBuilder("some-name").startSpan(); + try (Scope ignoredScope = parentSpan.makeCurrent()) { + Span childSpan = this.otelTracer.spanBuilder("other-name").startSpan(); + childSpan.end(); + } + parentSpan.end(); + + assertTraces( + trace( + span().root().operationName("internal").resourceName("some-name"), + span().childOfPrevious().operationName("internal").resourceName("other-name"))); + } + + @Test + void testParentSpanUsingReference() { + Span parentSpan = this.otelTracer.spanBuilder("some-name").startSpan(); + Span childSpan = + this.otelTracer + .spanBuilder("other-name") + .setParent(Context.current().with(parentSpan)) + .startSpan(); + childSpan.end(); + parentSpan.end(); + + assertTraces( + trace( + span().root().operationName("internal").resourceName("some-name"), + span().childOfPrevious().operationName("internal").resourceName("other-name"))); + } + + @Test + void testParentSpanUsingPropagationData() { + String traceId = "00000000000000001111111111111111"; + String spanId = "2222222222222222"; + String traceParent = "00-" + traceId + "-" + spanId + "-00"; + Map headers = new HashMap<>(); + headers.put("traceparent", traceParent); + TextMapPropagator propagator = GlobalOpenTelemetry.getPropagators().getTextMapPropagator(); + Context context = propagator.extract(Context.root(), headers, TextMap.INSTANCE); + + try (Scope ignoredScope = context.makeCurrent()) { + Span childSpan = this.otelTracer.spanBuilder("some-name").startSpan(); + childSpan.end(); + } + + assertTraces(trace(span().operationName("internal").resourceName("some-name"))); + + DDSpan ddSpan = this.writer.firstTrace().get(0); + assertEquals(DDTraceId.fromHex(traceId), ddSpan.getTraceId()); + assertEquals(DDSpanId.fromHex(spanId), ddSpan.getParentId()); + } + + @Test + void testParentSpanUsingInvalidReference() throws Exception { + Context invalidCurrentSpanContext = + Context.root(); // Contains a SpanContext with TID/SID to 0 as current span + Span childSpan = + this.otelTracer.spanBuilder("some-name").setParent(invalidCurrentSpanContext).startSpan(); + childSpan.end(); + + this.writer.waitForTraces(1); + List firstTrace = this.writer.firstTrace(); + + assertEquals(1, firstTrace.size()); + assertNotEquals(0, firstTrace.get(0).getSpanId()); + } + + @Test + void testNoParentToCreateNewRootSpan() { + Span parentSpan = this.otelTracer.spanBuilder("some-name").startSpan(); + try (Scope ignoredScope = parentSpan.makeCurrent()) { + Span childSpan = this.otelTracer.spanBuilder("other-name").setNoParent().startSpan(); + childSpan.end(); + } + parentSpan.end(); + + assertTraces( + trace(span().root().operationName("internal").resourceName("some-name")), + trace(span().root().operationName("internal").resourceName("other-name"))); + } + + @Test + void testAddEvent() { + SpanBuilder builder = this.otelTracer.spanBuilder("some-name"); + ControllableTimeSource timeSource = new ControllableTimeSource(); + timeSource.set(1000); + OtelSpanEvent.setTimeSource(timeSource); + + Span result = builder.startSpan(); + result.addEvent("event"); + result.end(); + + String expectedEventTag = + "[" + + "{ \"time_unix_nano\": " + + timeSource.getCurrentTimeNanos() + + ", \"name\": \"event\" }" + + "]"; + assertTraces( + trace( + span() + .tags( + defaultTags(), + tag(SPAN_KIND, is(SPAN_KIND_INTERNAL)), + tag(SPAN_EVENTS, isJson(expectedEventTag))))); + } + + static Stream testAddSingleEventArguments() { + return Stream.of( + arguments( + "empty attributes", "event1", TIME_MILLIS, MILLISECONDS, Attributes.empty(), null), + arguments( + "scalar attributes", + "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 }"), + arguments( + "array attributes", + "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] }")); + } + + @ParameterizedTest(name = "[{index}] {0}") + @MethodSource("testAddSingleEventArguments") + void testAddSingleEvent( + String scenario, + String name, + long timestamp, + TimeUnit unit, + Attributes attributes, + String 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); + result.end(); + + assertTraces( + trace( + span() + .tags( + defaultTags(), + tag(SPAN_KIND, is(SPAN_KIND_INTERNAL)), + tag(SPAN_EVENTS, isJson(expectedEventTag))))); + } + + @Test + void testAddMultipleSpanEvents() { + SpanBuilder builder = this.otelTracer.spanBuilder("some-name"); + + Span result = builder.startSpan(); + result.addEvent("event1", Attributes.empty(), TIME_NANO, NANOSECONDS); + result.addEvent( + "event2", + Attributes.builder().put("string-key", "string-value").build(), + TIME_NANO, + 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))))); + } + + @Test + void testSimpleSpanLinks() { + String traceId = "1234567890abcdef1234567890abcdef"; + 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") + .addLink(SpanContext.getInvalid()) + .addLink(SpanContext.create(traceId, spanId, TraceFlags.getSampled(), traceState)) + .startSpan(); + span.end(); + + assertTraces( + trace( + span() + .tags( + defaultTags(), + tag(SPAN_KIND, is(SPAN_KIND_INTERNAL)), + tag(SPAN_LINKS, isJson(expectedLinksTag))))); + } + + @Test + void testMultipleSpanLinks() { + SpanBuilder spanBuilder = this.otelTracer.spanBuilder("some-name"); + + 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 + + "\" }"); + } + spanBuilder.startSpan().end(); + + String expectedLinksTag = "[" + join(",", expectedLinks) + "]"; + assertTraces( + trace( + span() + .tags( + defaultTags(), + tag(SPAN_KIND, is(SPAN_KIND_INTERNAL)), + tag(SPAN_LINKS, isJson(expectedLinksTag))))); + } + + static Stream testSpanLinkAttributesArguments() { + return Stream.of( + arguments("empty attributes", Attributes.empty(), null), + arguments( + "scalar attributes", + 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\" }"), + arguments( + "array attributes", + 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\" }")); + } + + @ParameterizedTest(name = "[{index}] {0}") + @MethodSource("testSpanLinkAttributesArguments") + void testSpanLinkAttributes(String scenario, Attributes attributes, String expectedAttributes) { + String traceId = "1234567890abcdef1234567890abcdef"; + String spanId = "fedcba0987654321"; + TraceState traceState = TraceState.builder().put("string-key", "string-value").build(); + Span span = + this.otelTracer + .spanBuilder("some-name") + .addLink( + SpanContext.create(traceId, spanId, TraceFlags.getSampled(), traceState), + attributes) + .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))))); + } + + static Stream testSpanLinksTraceStateArguments() { + return Stream.of( + arguments("default trace state", TraceState.getDefault(), null), + arguments( + "single key-value", TraceState.builder().put("key", "value").build(), "key=value"), + arguments( + "multiple key-values", + TraceState.builder() + .put("key1", "value1") + .put("key2", "value2") + .put("key3", "value3") + .put("key4", "value4") + .put("key5", "value5") + .build(), + "key5=value5,key4=value4,key3=value3,key2=value2,key1=value1")); + } + + @ParameterizedTest(name = "[{index}] {0}") + @MethodSource("testSpanLinksTraceStateArguments") + void testSpanLinksTraceState(String scenario, TraceState traceState, String expectedTraceState) { + String traceId = "1234567890abcdef1234567890abcdef"; + String spanId = "fedcba0987654321"; + + Span span = + this.otelTracer + .spanBuilder("some-name") + .addLink(SpanContext.create(traceId, spanId, TraceFlags.getSampled(), traceState)) + .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))))); + } + + static Stream testSpanAttributesArguments() { + return Stream.of( + arguments("builder only", true, false), + arguments("builder and span", true, true), + arguments("neither", false, false), + arguments("span only", false, true)); + } + + @SuppressWarnings( + "DataFlowIssue") // Allow null values on non-null parameters for thourough testing + @ParameterizedTest(name = "[{index}] {0}") + @MethodSource("testSpanAttributesArguments") + void testSpanAttributes(String scenario, boolean tagBuilder, boolean tagSpan) { + SpanBuilder builder = this.otelTracer.spanBuilder("some-name"); + if (tagBuilder) { + builder + .setAttribute(DDTags.RESOURCE_NAME, "some-resource") + .setAttribute("string", "a") + .setAttribute("null-string", null) + .setAttribute("empty_string", "") + .setAttribute("number", 1) + .setAttribute("boolean", true) + .setAttribute(stringKey("null-string-attribute"), null) + .setAttribute(stringKey("empty-string-attribute"), "") + .setAttribute(stringArrayKey("string-array"), asList("a", "b", "c")) + .setAttribute(booleanArrayKey("boolean-array"), asList(true, false)) + .setAttribute(longArrayKey("long-array"), asList(1L, 2L, 3L, 4L)) + .setAttribute(doubleArrayKey("double-array"), asList(1.23D, 4.56D)) + .setAttribute(stringArrayKey("empty-array"), emptyList()) + .setAttribute(stringArrayKey("null-array"), null); + } + Span result = builder.startSpan(); + if (tagSpan) { + result.setAttribute(DDTags.RESOURCE_NAME, "other-resource"); + result.setAttribute("string", "b"); + result.setAttribute("empty_string", ""); + result.setAttribute("number", 2); + result.setAttribute("boolean", false); + result.setAttribute(stringKey("null-string-attribute"), null); + result.setAttribute(stringKey("empty-string-attribute"), ""); + result.setAttribute(stringArrayKey("string-array"), asList("d", "e", "f")); + result.setAttribute(booleanArrayKey("boolean-array"), asList(false, true)); + result.setAttribute(longArrayKey("long-array"), asList(5L, 6L, 7L, 8L)); + result.setAttribute(doubleArrayKey("double-array"), asList(2.34D, 5.67D)); + result.setAttribute(stringArrayKey("empty-array"), emptyList()); + result.setAttribute(stringArrayKey("null-array"), null); + } + + result.end(); + + String expectedResource; + if (tagSpan) { + expectedResource = "other-resource"; + } else if (tagBuilder) { + expectedResource = "some-resource"; + } else { + expectedResource = "some-name"; + } + + TagsMatcher[] tagsMatchers; + if (tagSpan) { + tagsMatchers = + new TagsMatcher[] { + defaultTags(), + tag(SPAN_KIND, is(SPAN_KIND_INTERNAL)), + tag("string", is("b")), + tag("empty_string", is("")), + tag("number", is(2L)), + tag("boolean", is(false)), + tag("empty-string-attribute", is("")), + tag("string-array.0", is("d")), + tag("string-array.1", is("e")), + tag("string-array.2", is("f")), + tag("boolean-array.0", is(false)), + tag("boolean-array.1", is(true)), + tag("long-array.0", is(5L)), + tag("long-array.1", is(6L)), + tag("long-array.2", is(7L)), + tag("long-array.3", is(8L)), + tag("double-array.0", is(2.34D)), + tag("double-array.1", is(5.67D)), + tag("empty-array", is("")) + }; + } else if (tagBuilder) { + tagsMatchers = + new TagsMatcher[] { + defaultTags(), + tag(SPAN_KIND, is(SPAN_KIND_INTERNAL)), + tag("string", is("a")), + tag("empty_string", is("")), + tag("number", is(1L)), + tag("boolean", is(true)), + tag("empty-string-attribute", is("")), + tag("string-array.0", is("a")), + tag("string-array.1", is("b")), + tag("string-array.2", is("c")), + tag("boolean-array.0", is(true)), + tag("boolean-array.1", is(false)), + tag("long-array.0", is(1L)), + tag("long-array.1", is(2L)), + tag("long-array.2", is(3L)), + tag("long-array.3", is(4L)), + tag("double-array.0", is(1.23D)), + tag("double-array.1", is(4.56D)), + tag("empty-array", is("")) + }; + } else { + tagsMatchers = new TagsMatcher[] {defaultTags(), tag(SPAN_KIND, is(SPAN_KIND_INTERNAL))}; + } + + assertTraces( + trace( + span() + .root() + .operationName("internal") + .resourceName(expectedResource) + .error(false) + .tags(tagsMatchers))); + } + + @Test + void testIntegrationName() { + Span span = this.otelTracer.spanBuilder("some-name").startSpan(); + span.end(); + + DDSpan ddSpan = this.writer.firstTrace().get(0); + assertEquals("otel", ddSpan.context().getIntegrationName().toString()); + } + + static Stream testSpanKindsArguments() { + return Stream.of( + arguments(INTERNAL, SPAN_KIND_INTERNAL), + arguments(SERVER, SPAN_KIND_SERVER), + arguments(CLIENT, SPAN_KIND_CLIENT), + arguments(PRODUCER, SPAN_KIND_PRODUCER), + arguments(CONSUMER, SPAN_KIND_CONSUMER)); + } + + @ParameterizedTest(name = "[{index}] {0}") + @MethodSource("testSpanKindsArguments") + void testSpanKinds(SpanKind otelSpanKind, String tagSpanKind) { + this.otelTracer.spanBuilder("some-name").setSpanKind(otelSpanKind).startSpan().end(); + + assertTraces(trace(span().tags(defaultTags(), tag(SPAN_KIND, is(tagSpanKind))))); + } + + @Test + void testSpanErrorStatus() { + Span result = this.otelTracer.spanBuilder("some-name").startSpan(); + result.setStatus(ERROR, "some-error"); + result.end(); + + assertTraces( + trace( + span() + .root() + .operationName("internal") + .resourceName("some-name") + .error(true) + .tags( + defaultTags(), + tag(SPAN_KIND, is(SPAN_KIND_INTERNAL)), + tag(ERROR_MSG, is("some-error"))))); + } + + @Test + void testSpanStatusTransition() { + Span result = this.otelTracer.spanBuilder("some-name").startSpan(); + DDSpan delegate = getDDSpan(result); + + // UNSET + result.setStatus(UNSET); + assertFalse(delegate.isError()); + assertNull(delegate.getTag(ERROR_MSG)); + + // ERROR + result.setStatus(ERROR, "some error"); + assertTrue(delegate.isError()); + assertEquals("some error", delegate.getTag(ERROR_MSG)); + + // UNSET after ERROR (should not clear error) + result.setStatus(UNSET); + assertTrue(delegate.isError()); + assertEquals("some error", delegate.getTag(ERROR_MSG)); + + // OK (should clear error) + result.setStatus(OK); + assertFalse(delegate.isError()); + assertNull(delegate.getTag(ERROR_MSG)); + + result.end(); + + assertTraces( + trace( + span() + .root() + .operationName("internal") + .resourceName("some-name") + .error(false) + .tags(defaultTags(), tag(SPAN_KIND, is(SPAN_KIND_INTERNAL))))); + } + + static Stream testSpanRecordExceptionArguments() { + return Stream.of( + arguments( + "basic exception", + new NullPointerException("Null pointer"), + Attributes.empty(), + null, + null, + null, + null), + arguments( + "overridden message", + new NumberFormatException("Number format exception"), + Attributes.builder().put("exception.message", "something-else").build(), + "something-else", + null, + null, + null), + arguments( + "overridden type", + new NullPointerException("Null pointer"), + Attributes.builder().put("exception.type", "CustomType").build(), + null, + "CustomType", + null, + null), + arguments( + "overridden stacktrace", + new NullPointerException("Null pointer"), + Attributes.builder().put("exception.stacktrace", "CustomTrace").build(), + null, + null, + "CustomTrace", + null), + arguments( + "extra attributes", + new NullPointerException("Null pointer"), + Attributes.builder().put("key", "value").build(), + null, + null, + null, + ", \"key\": \"value\"")); + } + + @ParameterizedTest(name = "[{index}] {0}") + @MethodSource("testSpanRecordExceptionArguments") + void testSpanRecordException( + String scenario, + Exception exception, + Attributes attributes, + String overriddenMessage, + String overriddenType, + String overriddenStacktrace, + String extraJson) { + ControllableTimeSource timeSource = new ControllableTimeSource(); + timeSource.set(1000); + OtelSpanEvent.setTimeSource(timeSource); + + Span result = this.otelTracer.spanBuilder("some-name").startSpan(); + result.recordException(exception, attributes); + result.end(); + + String errorMessage = overriddenMessage != null ? overriddenMessage : exception.getMessage(); + 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\": " + + timeSource.getCurrentTimeNanos() + + ", \"name\": \"exception\", \"attributes\": " + + expectedAttributes + + " }" + + "]"; + + assertTraces( + trace( + span() + .root() + .operationName("internal") + .resourceName("some-name") + .error(false) + .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))))); + } + + @Test + void testSpanErrorReflectLastException() { + Span span = this.otelTracer.spanBuilder("some-name").startSpan(); + NullPointerException firstException = new NullPointerException("Null pointer"); + NumberFormatException lastException = new NumberFormatException("Number format exception"); + + span.recordException(firstException); + span.recordException(lastException); + span.end(); + + assertTraces( + trace( + span() + .root() + .operationName("internal") + .resourceName("some-name") + .error(false) + .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)))))); + } + + @Test + void testSpanNameUpdate() { + Span result = this.otelTracer.spanBuilder("some-name").setSpanKind(SERVER).startSpan(); + DDSpan delegate = getDDSpan(result); + + assertEquals(SPAN_KIND_INTERNAL, delegate.getOperationName().toString()); + assertEquals("some-name", delegate.getResourceName().toString()); + + result.updateName("other-name"); + + assertEquals(SPAN_KIND_INTERNAL, delegate.getOperationName().toString()); + assertEquals("other-name", delegate.getResourceName().toString()); + + result.end(); + + assertTraces(trace(span().root().operationName("server.request").resourceName("other-name"))); + } + + @Test + void testSpanUpdateAfterEnd() { + Span result = this.otelTracer.spanBuilder("some-name").startSpan(); + + result.setAttribute("string", "value"); + result.setStatus(ERROR); + result.end(); + result.updateName("other-name"); + result.setAttribute("string", "other-value"); + result.setStatus(OK); + result.addEvent("event"); + result.recordException(new Throwable()); + + assertTraces( + trace( + span() + .root() + .operationName("internal") + .resourceName("some-name") + .error(true) + .tags( + defaultTags(), + tag(SPAN_KIND, is(SPAN_KIND_INTERNAL)), + tag("string", is("value"))))); + } + + private static DDSpan getDDSpan(Span span) { + return (DDSpan) ((WithAgentSpan) span).asAgentSpan(); + } + + private static Matcher isJson(String expected) { + return Matchers.validates( + s -> { + try { + JSONAssert.assertEquals(expected, s, true); + return true; + } catch (JSONException e) { + return false; + } + }); + } +} diff --git a/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.4/src/test/java/opentelemetry14/context/ContextTest.java b/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.4/src/test/java/opentelemetry14/context/ContextTest.java new file mode 100644 index 00000000000..85fe100a1f2 --- /dev/null +++ b/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.4/src/test/java/opentelemetry14/context/ContextTest.java @@ -0,0 +1,398 @@ +package opentelemetry14.context; + +import static datadog.trace.agent.test.assertions.SpanMatcher.span; +import static datadog.trace.agent.test.assertions.TraceMatcher.trace; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import datadog.trace.api.DDSpanId; +import datadog.trace.bootstrap.instrumentation.api.AgentScope; +import datadog.trace.bootstrap.instrumentation.api.AgentSpan; +import datadog.trace.core.DDSpan; +import io.opentelemetry.api.baggage.Baggage; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.ContextKey; +import io.opentelemetry.context.ImplicitContextKeyed; +import io.opentelemetry.context.Scope; +import java.util.HashMap; +import java.util.Map; +import opentelemetry14.AbstractOpenTelemetry14Test; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +public class ContextTest extends AbstractOpenTelemetry14Test { + + @DisplayName("test Span.current/makeCurrent()") + @Test + void testSpanCurrentMakeCurrent() { + Span otelSpan = this.otelTracer.spanBuilder("some-name").startSpan(); + + // Before making current: current span must be invalid or null + Span currentSpan = Span.current(); + Span currentSpanFromContext = Span.fromContext(Context.current()); + Span currentSpanFromContextOrNull = Span.fromContextOrNull(Context.current()); + + assertNotNull(currentSpan); + assertFalse(currentSpan.getSpanContext().isValid()); + assertNotNull(currentSpanFromContext); + assertFalse(currentSpanFromContext.getSpanContext().isValid()); + assertNull(currentSpanFromContextOrNull); + + // After making current: OTel span must be current span + Scope scope = otelSpan.makeCurrent(); + currentSpan = Span.current(); + currentSpanFromContext = Span.fromContext(Context.current()); + currentSpanFromContextOrNull = Span.fromContextOrNull(Context.current()); + + assertEquals(otelSpan, currentSpan); + assertEquals(otelSpan, currentSpanFromContext); + assertEquals(otelSpan, currentSpanFromContextOrNull); + + // After activating DD span: Datadog span must be current span + AgentSpan ddSpan = this.tracer.startSpan("dd-api", "other-name"); + AgentScope ddScope = this.tracer.activateManualSpan(ddSpan); + currentSpan = Span.current(); + + assertSpanEquals(ddSpan, currentSpan); + + // Cleanup + ddScope.close(); + ddSpan.finish(); + scope.close(); + otelSpan.end(); + } + + @DisplayName("test Context.makeCurrent() to activate a span without prior active span") + @Test + void testContextMakeCurrentWithoutPriorActiveSpan() { + Span otelSpan = this.otelTracer.spanBuilder("some-name").startSpan(); + + // No active span: current span is invalid + Span currentSpan = Span.current(); + assertNotNull(currentSpan); + assertFalse(currentSpan.getSpanContext().isValid()); + + // Make OTel span current via context + Context contextWithSpan = Context.current().with(otelSpan); + Scope scope = contextWithSpan.makeCurrent(); + currentSpan = Span.current(); + assertEquals(otelSpan, currentSpan); + + // After closing scope: current span is invalid again + scope.close(); + currentSpan = Span.current(); + assertNotNull(currentSpan); + assertFalse(currentSpan.getSpanContext().isValid()); + + // Cleanup + otelSpan.end(); + } + + @DisplayName("test Context.makeCurrent() to activate a span with another currently active span") + @Test + void testContextMakeCurrentWithAnotherActiveSpan() { + AgentSpan ddSpan = this.tracer.startSpan("dd-api", "some-name"); + AgentScope ddScope = this.tracer.activateManualSpan(ddSpan); + Span otelSpan = this.otelTracer.spanBuilder("other-name").startSpan(); + + // DD span is active: current OTel span reflects DD span + Span currentSpan = Span.current(); + assertNotNull(currentSpan); + assertSpanEquals(ddSpan, currentSpan); + + // Make OTel span current via context + Context contextWithSpan = Context.current().with(otelSpan); + Scope scope = contextWithSpan.makeCurrent(); + currentSpan = Span.current(); + assertEquals(otelSpan, currentSpan); + + // After closing scope: DD span is active again + scope.close(); + currentSpan = Span.current(); + assertNotNull(currentSpan); + assertSpanEquals(ddSpan, currentSpan); + + // Cleanup + otelSpan.end(); + ddScope.close(); + ddSpan.finish(); + } + + @DisplayName("test Context.makeCurrent() to activate an already active span") + @Test + void testContextMakeCurrentAlreadyActiveSpan() { + AgentSpan ddSpan = this.tracer.startSpan("dd-api", "some-name"); + AgentScope ddScope = this.tracer.activateManualSpan(ddSpan); + Span currentSpan = Span.current(); + + assertNotNull(currentSpan); + assertSpanEquals(ddSpan, currentSpan); + + // Make the already-current span current again via context + Context contextWithSpan = Context.current().with(currentSpan); + Scope scope = contextWithSpan.makeCurrent(); + currentSpan = Span.current(); + + assertNotNull(currentSpan); + assertSpanEquals(ddSpan, currentSpan); + + // After closing scope: still same DD span + scope.close(); + currentSpan = Span.current(); + + assertNotNull(currentSpan); + assertSpanEquals(ddSpan, currentSpan); + + // After closing DD scope and finishing: no valid span + ddScope.close(); + ddSpan.finish(); + currentSpan = Span.current(); + + assertNotNull(currentSpan); + assertFalse(currentSpan.getSpanContext().isValid()); + } + + @DisplayName("test clearing context") + @Test + void testClearingContext() { + Scope rootScope = Context.root().makeCurrent(); + assertEquals(Context.root(), Context.current()); + rootScope.close(); + } + + @DisplayName("test mixing manual and OTel instrumentation") + @Test + void testMixingManualAndOtelInstrumentation() throws Exception { + Span otelParentSpan = this.otelTracer.spanBuilder("some-name").startSpan(); + + // Activate OTel parent span and verify DD active span + Scope otelParentScope = otelParentSpan.makeCurrent(); + AgentSpan activeSpan = this.tracer.activeSpan(); + + assertEquals("internal", activeSpan.getOperationName().toString()); + assertEquals("some-name", activeSpan.getResourceName().toString()); + assertEquals( + DDSpanId.toHexStringPadded(activeSpan.getSpanId()), + otelParentSpan.getSpanContext().getSpanId()); + + // Activate DD child span and verify OTel current span + AgentSpan ddChildSpan = this.tracer.startSpan("dd-api", "other-name"); + AgentScope ddChildScope = this.tracer.activateManualSpan(ddChildSpan); + Span current = Span.current(); + + assertEquals( + DDSpanId.toHexStringPadded(ddChildSpan.getSpanId()), current.getSpanContext().getSpanId()); + + // Activate OTel grandchild span and verify DD active span + Span otelGrandChildSpan = this.otelTracer.spanBuilder("another-name").startSpan(); + Scope otelGrandChildScope = otelGrandChildSpan.makeCurrent(); + activeSpan = this.tracer.activeSpan(); + + assertEquals("internal", ((DDSpan) activeSpan).getOperationName().toString()); + assertEquals("another-name", ((DDSpan) activeSpan).getResourceName().toString()); + assertEquals( + DDSpanId.toHexStringPadded(activeSpan.getSpanId()), + otelGrandChildSpan.getSpanContext().getSpanId()); + + // Close everything and verify trace structure + otelGrandChildScope.close(); + otelGrandChildSpan.end(); + ddChildScope.close(); + ddChildSpan.finish(); + otelParentScope.close(); + otelParentSpan.end(); + + assertTraces( + trace( + span().root().operationName("internal").resourceName("some-name"), + span().childOfPrevious().operationName("other-name"), + span().childOfPrevious().operationName("internal").resourceName("another-name"))); + } + + @DisplayName("test context spans retrieval") + @Test + void testContextSpansRetrieval() { + Span parentSpan = this.otelTracer.spanBuilder("some-name").startSpan(); + Scope parentScope = parentSpan.makeCurrent(); + ContextKey currentSpanKey = ContextKey.named("opentelemetry-trace-span-key"); + ContextKey rootSpanKey = ContextKey.named("opentelemetry-traces-local-root-span"); + + // After activating parent: both current and root span keys point to parent + Context currentContext = Context.current(); + assertEquals(parentSpan, currentContext.get(currentSpanKey)); + assertEquals(parentSpan, currentContext.get(rootSpanKey)); + + // After activating child: current span key points to child, root span key still points to + // parent + Span childSpan = this.otelTracer.spanBuilder("other-name").startSpan(); + Scope childScope = childSpan.makeCurrent(); + currentContext = Context.current(); + + assertEquals(childSpan, currentContext.get(currentSpanKey)); + assertEquals(parentSpan, currentContext.get(rootSpanKey)); + + // After closing child: back to parent for both keys + childScope.close(); + childSpan.end(); + currentContext = Context.current(); + + assertEquals(parentSpan, currentContext.get(currentSpanKey)); + assertEquals(parentSpan, currentContext.get(rootSpanKey)); + + // Cleanup + parentScope.close(); + parentSpan.end(); + } + + @DisplayName("test custom object storage") + @Test + void testCustomObjectStorage() { + Context context = Context.root(); + Context originalContext = context; + CustomData data1 = new CustomData(); + CustomData data2 = new CustomData(); + + // Store data1 + context = context.with(data1); + assertEquals(data1, CustomData.fromContext(context)); + assertNull(CustomData.fromContext(originalContext)); + + // Replace with data2 + context = context.with(data2); + assertEquals(data2, CustomData.fromContext(context)); + + // Remove custom data + context = context.with(CustomData.KEY, null); + assertNull(context.get(CustomData.KEY)); + } + + @DisplayName("test Baggage.current/makeCurrent()") + @Test + void testBaggageCurrentMakeCurrent() { + // Initially: current baggage must be empty or null + Baggage otelBaggage = Baggage.current(); + Baggage otelBaggageFromContext = Baggage.fromContext(Context.current()); + Baggage otelBaggageFromContextOrNull = Baggage.fromContextOrNull(Context.current()); + + assertNotNull(otelBaggage); + assertTrue(otelBaggage.isEmpty()); + assertNotNull(otelBaggageFromContext); + assertTrue(otelBaggageFromContext.isEmpty()); + assertNull(otelBaggageFromContextOrNull); + + // After making OTel baggage current + Scope otelScope = + Baggage.builder() + .put("foo", "otel_value_to_be_replaced") + .put("FOO", "OTEL_UNTOUCHED") + .put("remove_me_key", "otel_remove_me_value") + .build() + .makeCurrent(); + otelBaggage = Baggage.current(); + otelBaggageFromContext = Baggage.fromContext(Context.current()); + otelBaggageFromContextOrNull = Baggage.fromContextOrNull(Context.current()); + + assertNotNull(otelBaggage); + assertEquals(3, otelBaggage.size()); + assertEquals("otel_value_to_be_replaced", otelBaggage.getEntryValue("foo")); + assertEquals("OTEL_UNTOUCHED", otelBaggage.getEntryValue("FOO")); + assertEquals("otel_remove_me_value", otelBaggage.getEntryValue("remove_me_key")); + assertEquals(otelBaggage.asMap(), otelBaggageFromContext.asMap()); + assertEquals(otelBaggage.asMap(), otelBaggageFromContextOrNull.asMap()); + + // After modifying DD baggage + datadog.context.Context ddContext = datadog.context.Context.current(); + datadog.trace.bootstrap.instrumentation.api.Baggage ddBaggage = + datadog.trace.bootstrap.instrumentation.api.Baggage.fromContext(ddContext); + ddBaggage.addItem("new_foo", "dd_new_value"); + ddBaggage.addItem("foo", "dd_overwrite_value"); + ddBaggage.removeItem("remove_me_key"); + datadog.context.ContextScope ddScope = ddContext.with(ddBaggage).attach(); + otelBaggage = Baggage.current(); + otelBaggageFromContext = Baggage.fromContext(Context.current()); + otelBaggageFromContextOrNull = Baggage.fromContextOrNull(Context.current()); + + assertNotNull(otelBaggage); + assertEquals(3, otelBaggage.size()); + assertEquals("dd_overwrite_value", otelBaggage.getEntryValue("foo")); + assertEquals("OTEL_UNTOUCHED", otelBaggage.getEntryValue("FOO")); + assertEquals("dd_new_value", otelBaggage.getEntryValue("new_foo")); + assertEquals(otelBaggage.asMap(), otelBaggageFromContext.asMap()); + assertEquals(otelBaggage.asMap(), otelBaggageFromContextOrNull.asMap()); + + // After closing both scopes: current baggage must be empty + ddScope.close(); + otelScope.close(); + assertTrue(Baggage.current().isEmpty()); + + // Create DD baggage from map and activate + ddContext = datadog.context.Context.current(); + Map ddBaggageItems = new HashMap<>(); + ddBaggageItems.put("foo", "dd_value_to_be_replaced"); + ddBaggageItems.put("FOO", "DD_UNTOUCHED"); + ddBaggageItems.put("remove_me_key", "dd_remove_me_value"); + ddBaggage = datadog.trace.bootstrap.instrumentation.api.Baggage.create(ddBaggageItems); + ddScope = ddContext.with(ddBaggage).attach(); + otelBaggage = Baggage.current(); + otelBaggageFromContext = Baggage.fromContext(Context.current()); + otelBaggageFromContextOrNull = Baggage.fromContextOrNull(Context.current()); + + assertNotNull(otelBaggage); + assertEquals(3, otelBaggage.size()); + assertEquals("dd_value_to_be_replaced", otelBaggage.getEntryValue("foo")); + assertEquals("DD_UNTOUCHED", otelBaggage.getEntryValue("FOO")); + assertEquals("dd_remove_me_value", otelBaggage.getEntryValue("remove_me_key")); + assertEquals(otelBaggage.asMap(), otelBaggageFromContext.asMap()); + assertEquals(otelBaggage.asMap(), otelBaggageFromContextOrNull.asMap()); + + // Build modified OTel baggage from existing + io.opentelemetry.api.baggage.BaggageBuilder builder = otelBaggage.toBuilder(); + builder.put("new_foo", "otel_new_value"); + builder.put("foo", "otel_overwrite_value"); + builder.remove("remove_me_key"); + otelScope = builder.build().makeCurrent(); + otelBaggage = Baggage.current(); + otelBaggageFromContext = Baggage.fromContext(Context.current()); + otelBaggageFromContextOrNull = Baggage.fromContextOrNull(Context.current()); + + assertNotNull(otelBaggage); + assertEquals(3, otelBaggage.size()); + assertEquals("otel_overwrite_value", otelBaggage.getEntryValue("foo")); + assertEquals("DD_UNTOUCHED", otelBaggage.getEntryValue("FOO")); + assertEquals("otel_new_value", otelBaggage.getEntryValue("new_foo")); + assertEquals(otelBaggage.asMap(), otelBaggageFromContext.asMap()); + assertEquals(otelBaggage.asMap(), otelBaggageFromContextOrNull.asMap()); + + // Cleanup + otelScope.close(); + ddScope.close(); + } + + // Using Object for ddSpan instead of AgentSpan as bootstrap types can't be referred in API + private static void assertSpanEquals(Object ddSpan, Span currentSpan) { + AgentSpan expectedDddSpan = (AgentSpan) ddSpan; + assertEquals( + expectedDddSpan.getTraceId().toHexStringPadded(32), + currentSpan.getSpanContext().getTraceId()); + assertEquals( + DDSpanId.toHexStringPadded(expectedDddSpan.getSpanId()), + currentSpan.getSpanContext().getSpanId()); + } + + private static class CustomData implements ImplicitContextKeyed { + static final ContextKey KEY = ContextKey.named("custom"); + + @Override + public Context storeInContext(Context context) { + return context.with(KEY, this); + } + + static CustomData fromContext(Context context) { + return context.get(KEY); + } + } +} diff --git a/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.4/src/test/java/opentelemetry14/context/propagation/AbstractPropagatorTest.java b/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.4/src/test/java/opentelemetry14/context/propagation/AbstractPropagatorTest.java new file mode 100644 index 00000000000..53ea24f7429 --- /dev/null +++ b/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.4/src/test/java/opentelemetry14/context/propagation/AbstractPropagatorTest.java @@ -0,0 +1,80 @@ +package opentelemetry14.context.propagation; + +import static datadog.trace.agent.test.assertions.SpanMatcher.span; +import static datadog.trace.agent.test.assertions.TraceMatcher.trace; +import static datadog.trace.api.DDTraceId.*; +import static datadog.trace.api.sampling.PrioritySampling.SAMPLER_KEEP; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; + +import datadog.trace.api.DDSpanId; +import datadog.trace.core.DDSpan; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.Scope; +import io.opentelemetry.context.propagation.TextMapPropagator; +import java.util.HashMap; +import java.util.Map; +import opentelemetry14.AbstractOpenTelemetry14Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +public abstract class AbstractPropagatorTest extends AbstractOpenTelemetry14Test { + + abstract TextMapPropagator propagator(); + + abstract void assertInjectedHeaders( + Map headers, String traceId, String spanId, byte sampling); + + @ParameterizedTest + @MethodSource("values") + void testContextExtractionAndInjection( + Map headers, String traceId, String spanId, byte sampling) { + TextMapPropagator propagator = propagator(); + boolean expectedSampled = sampling == SAMPLER_KEEP; + + Context context = propagator.extract(Context.root(), headers, TextMap.INSTANCE); + assertNotEquals(Context.root(), context); + + Span localSpan = this.otelTracer.spanBuilder("some-name").setParent(context).startSpan(); + String localSpanId = localSpan.getSpanContext().getSpanId(); + boolean spanSampled = localSpan.getSpanContext().getTraceFlags().isSampled(); + Map injectedHeaders = new HashMap<>(); + try (Scope ignoredScope = localSpan.makeCurrent()) { + propagator.inject(Context.current(), injectedHeaders, new TextMap()); + } + localSpan.end(); + + assertTraces(trace(span().operationName("internal").resourceName("some-name"))); + + DDSpan ddSpan = this.writer.firstTrace().get(0); + assertEquals(expectedTraceId(traceId), ddSpan.getTraceId().toString()); + assertEquals(DDSpanId.fromHex(spanId), ddSpan.getParentId()); + assertEquals(expectedSampled, spanSampled); + assertInjectedHeaders(injectedHeaders, traceId, localSpanId, sampling); + } + + String expectedTraceId(String traceId) { + return fromHex(traceId).toString(); + } + + static String padLeft(String value, int length, char padChar) { + if (value.length() >= length) { + return value; + } + StringBuilder sb = new StringBuilder(length); + for (int i = value.length(); i < length; i++) { + sb.append(padChar); + } + sb.append(value); + return sb.toString(); + } + + static Map headers(String... keyValues) { + Map map = new HashMap<>(); + for (int i = 0; i + 1 < keyValues.length; i += 2) { + map.put(keyValues[i], keyValues[i + 1]); + } + return map; + } +} diff --git a/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.4/src/test/java/opentelemetry14/context/propagation/AgentPropagatorTest.java b/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.4/src/test/java/opentelemetry14/context/propagation/AgentPropagatorTest.java new file mode 100644 index 00000000000..9bb5b8d0d3d --- /dev/null +++ b/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.4/src/test/java/opentelemetry14/context/propagation/AgentPropagatorTest.java @@ -0,0 +1,11 @@ +package opentelemetry14.context.propagation; + +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.context.propagation.TextMapPropagator; + +abstract class AgentPropagatorTest extends AbstractPropagatorTest { + @Override + TextMapPropagator propagator() { + return GlobalOpenTelemetry.get().getPropagators().getTextMapPropagator(); + } +} diff --git a/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.4/src/test/java/opentelemetry14/context/propagation/B3MultiPropagatorTest.java b/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.4/src/test/java/opentelemetry14/context/propagation/B3MultiPropagatorTest.java new file mode 100644 index 00000000000..8ce4cb027dc --- /dev/null +++ b/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.4/src/test/java/opentelemetry14/context/propagation/B3MultiPropagatorTest.java @@ -0,0 +1,68 @@ +package opentelemetry14.context.propagation; + +import static datadog.trace.api.sampling.PrioritySampling.SAMPLER_DROP; +import static datadog.trace.api.sampling.PrioritySampling.SAMPLER_KEEP; +import static datadog.trace.api.sampling.PrioritySampling.UNSET; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.params.provider.Arguments.arguments; + +import datadog.trace.core.propagation.B3TraceId; +import datadog.trace.junit.utils.config.WithConfig; +import java.util.Map; +import java.util.stream.Stream; +import org.junit.jupiter.params.provider.Arguments; + +@WithConfig(key = "trace.propagation.style", value = "b3multi") +class B3MultiPropagatorTest extends AgentPropagatorTest { + private static final String TRACE_ID_KEY = "X-B3-TraceId"; + private static final String SPAN_ID_KEY = "X-B3-SpanId"; + private static final String SAMPLING_PRIORITY_KEY = "X-B3-Sampled"; + + static Stream values() { + // spotless:off + return Stream.of( + arguments( + headers(TRACE_ID_KEY, "1", SPAN_ID_KEY, "2"), + "1", + "2", + UNSET), + arguments( + headers(TRACE_ID_KEY, "1111111111111111", SPAN_ID_KEY, "2222222222222222"), + "1111111111111111", + "2222222222222222", + UNSET), + arguments( + headers(TRACE_ID_KEY, "11111111111111111111111111111111", SPAN_ID_KEY, "2222222222222222"), + "11111111111111111111111111111111", + "2222222222222222", + UNSET), + arguments( + headers(TRACE_ID_KEY, "11111111111111111111111111111111", SPAN_ID_KEY, "2222222222222222", + SAMPLING_PRIORITY_KEY, "0"), + "11111111111111111111111111111111", + "2222222222222222", + SAMPLER_DROP), + arguments( + headers(TRACE_ID_KEY, "11111111111111111111111111111111", + SPAN_ID_KEY, "2222222222222222", + SAMPLING_PRIORITY_KEY, "1"), + "11111111111111111111111111111111", + "2222222222222222", + SAMPLER_KEEP)); + // spotless:on + } + + @Override + String expectedTraceId(String traceId) { + return B3TraceId.fromHex(traceId).toString(); + } + + @Override + void assertInjectedHeaders( + Map headers, String traceId, String spanId, byte sampling) { + String priorityKey = sampling == SAMPLER_DROP ? "0" : "1"; + assertEquals(padLeft(traceId, 32, '0'), headers.get(TRACE_ID_KEY)); + assertEquals(padLeft(spanId, 8, '0'), headers.get(SPAN_ID_KEY)); + assertEquals(priorityKey, headers.get(SAMPLING_PRIORITY_KEY)); + } +} diff --git a/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.4/src/test/java/opentelemetry14/context/propagation/B3SinglePropagatorTest.java b/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.4/src/test/java/opentelemetry14/context/propagation/B3SinglePropagatorTest.java new file mode 100644 index 00000000000..18cc1a18e14 --- /dev/null +++ b/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.4/src/test/java/opentelemetry14/context/propagation/B3SinglePropagatorTest.java @@ -0,0 +1,62 @@ +package opentelemetry14.context.propagation; + +import static datadog.trace.api.sampling.PrioritySampling.SAMPLER_DROP; +import static datadog.trace.api.sampling.PrioritySampling.SAMPLER_KEEP; +import static datadog.trace.api.sampling.PrioritySampling.UNSET; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.params.provider.Arguments.arguments; + +import datadog.trace.core.propagation.B3TraceId; +import datadog.trace.junit.utils.config.WithConfig; +import java.util.Map; +import java.util.stream.Stream; +import org.junit.jupiter.params.provider.Arguments; + +@WithConfig(key = "trace.propagation.style", value = "b3single") +class B3SinglePropagatorTest extends AgentPropagatorTest { + private static final String B3_KEY = "b3"; + + static Stream values() { + // spotless:off + return Stream.of( + arguments(headers(B3_KEY, "1-2"), + "1", + "2", + UNSET), + arguments( + headers(B3_KEY, "1111111111111111-2222222222222222"), + "1111111111111111", + "2222222222222222", + UNSET), + arguments( + headers(B3_KEY, "11111111111111111111111111111111-2222222222222222"), + "11111111111111111111111111111111", + "2222222222222222", + UNSET), + arguments( + headers(B3_KEY, "11111111111111111111111111111111-2222222222222222-0"), + "11111111111111111111111111111111", + "2222222222222222", + SAMPLER_DROP), + arguments( + headers(B3_KEY, "11111111111111111111111111111111-2222222222222222-1"), + "11111111111111111111111111111111", + "2222222222222222", + SAMPLER_KEEP)); + // spotless:on + } + + @Override + String expectedTraceId(String traceId) { + return B3TraceId.fromHex(traceId).toString(); + } + + @Override + void assertInjectedHeaders( + Map headers, String traceId, String spanId, byte sampling) { + String sampledValue = sampling == SAMPLER_DROP ? "0" : "1"; + String expected = + padLeft(traceId, 32, '0') + "-" + padLeft(spanId, 8, '0') + "-" + sampledValue; + assertEquals(expected, headers.get(B3_KEY)); + } +} diff --git a/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.4/src/test/java/opentelemetry14/context/propagation/DatadogPropagatorTest.java b/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.4/src/test/java/opentelemetry14/context/propagation/DatadogPropagatorTest.java new file mode 100644 index 00000000000..038b5208317 --- /dev/null +++ b/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.4/src/test/java/opentelemetry14/context/propagation/DatadogPropagatorTest.java @@ -0,0 +1,89 @@ +package opentelemetry14.context.propagation; + +import static datadog.trace.api.sampling.PrioritySampling.SAMPLER_DROP; +import static datadog.trace.api.sampling.PrioritySampling.SAMPLER_KEEP; +import static datadog.trace.api.sampling.PrioritySampling.UNSET; +import static java.lang.String.join; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.params.provider.Arguments.arguments; + +import datadog.trace.api.DDTraceId; +import datadog.trace.junit.utils.config.WithConfig; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.stream.Stream; +import org.junit.jupiter.params.provider.Arguments; + +@WithConfig(key = "trace.propagation.style", value = "datadog") +class DatadogPropagatorTest extends AgentPropagatorTest { + static Stream values() { + String traceIdDecimal = Long.toString(Long.parseLong("1111111111111111", 16)); + String parentIdDecimal = Long.toString(Long.parseLong("2222222222222222", 16)); + // spotless:off + return Stream.of( + arguments( + headers( + "x-datadog-trace-id", traceIdDecimal, + "x-datadog-parent-id", parentIdDecimal), + "1111111111111111", + "2222222222222222", + UNSET), + arguments( + headers( + "x-datadog-trace-id", traceIdDecimal, + "x-datadog-parent-id", parentIdDecimal, + "x-datadog-tags", "_dd.p.tid=1111111111111111"), + "11111111111111111111111111111111", + "2222222222222222", + UNSET), + arguments( + headers("x-datadog-trace-id", traceIdDecimal, + "x-datadog-parent-id", parentIdDecimal, + "x-datadog-sampling-priority", String.valueOf(SAMPLER_KEEP), + "x-datadog-tags", "_dd.p.tid=1111111111111111"), + "11111111111111111111111111111111", + "2222222222222222", + SAMPLER_KEEP), + arguments( + headers( + "x-datadog-trace-id", traceIdDecimal, + "x-datadog-parent-id", parentIdDecimal, + "x-datadog-sampling-priority", String.valueOf(UNSET), + "x-datadog-tags", "_dd.p.tid=1111111111111111"), + "11111111111111111111111111111111", + "2222222222222222", + UNSET), + arguments( + headers( + "x-datadog-trace-id", traceIdDecimal, + "x-datadog-parent-id", parentIdDecimal, + "x-datadog-sampling-priority", String.valueOf(SAMPLER_DROP), + "x-datadog-tags", "_dd.p.tid=1111111111111111"), + "11111111111111111111111111111111", + "2222222222222222", + SAMPLER_DROP)); + // spotless:on + } + + @Override + void assertInjectedHeaders( + Map headers, String traceId, String spanId, byte sampling) { + assertEquals( + Long.toString(DDTraceId.fromHex(traceId).toLong()), headers.get("x-datadog-trace-id")); + assertEquals(spanId.replaceAll("^0+(?!$)", ""), headers.get("x-datadog-parent-id")); + String samplingPriority = sampling == SAMPLER_DROP ? "0" : "1"; + List tags = new ArrayList<>(); + if (sampling == UNSET) { + tags.add("_dd.p.dm=-1"); + } + if (traceId.length() == 32) { + tags.add("_dd.p.tid=" + traceId.substring(0, 16)); + } + if (sampling == UNSET) { + tags.add("_dd.p.ksr=1"); + } + assertEquals(join(",", tags), headers.get("x-datadog-tags")); + assertEquals(samplingPriority, headers.get("x-datadog-sampling-priority")); + } +} diff --git a/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.4/src/test/java/opentelemetry14/context/propagation/MissingTraceContextPropagatorTest.java b/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.4/src/test/java/opentelemetry14/context/propagation/MissingTraceContextPropagatorTest.java new file mode 100644 index 00000000000..70c25164aac --- /dev/null +++ b/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.4/src/test/java/opentelemetry14/context/propagation/MissingTraceContextPropagatorTest.java @@ -0,0 +1,43 @@ +package opentelemetry14.context.propagation; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.params.provider.Arguments.arguments; + +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.propagation.TextMapPropagator; +import java.util.HashMap; +import java.util.Map; +import java.util.stream.Stream; +import opentelemetry14.AbstractOpenTelemetry14Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +class MissingTraceContextPropagatorTest extends AbstractOpenTelemetry14Test { + + static Stream testExtractOnMissingTracecontextArguments() { + return Stream.of( + arguments( + "agent propagator", GlobalOpenTelemetry.get().getPropagators().getTextMapPropagator()), + arguments("W3C propagator", W3CTraceContextPropagator.getInstance())); + } + + @ParameterizedTest(name = "[{index}] {0}") + @MethodSource("testExtractOnMissingTracecontextArguments") + void testExtractOnMissingTracecontext(String scenario, TextMapPropagator propagator) { + Map headers = new HashMap<>(); + headers.put("User-Agent", "test"); + + Context context = propagator.extract(Context.root(), headers, TextMap.INSTANCE); + Span extractedSpan = Span.fromContext(context); + + assertNotNull(extractedSpan); + assertFalse(extractedSpan.getSpanContext().isValid()); + assertNull(Span.fromContextOrNull(context)); + } +} diff --git a/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.4/src/test/java/opentelemetry14/context/propagation/OtelW3cPropagatorTest.java b/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.4/src/test/java/opentelemetry14/context/propagation/OtelW3cPropagatorTest.java new file mode 100644 index 00000000000..dc9893b25f1 --- /dev/null +++ b/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.4/src/test/java/opentelemetry14/context/propagation/OtelW3cPropagatorTest.java @@ -0,0 +1,43 @@ +package opentelemetry14.context.propagation; + +import static datadog.trace.api.sampling.PrioritySampling.SAMPLER_KEEP; +import static datadog.trace.api.sampling.PrioritySampling.UNSET; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.params.provider.Arguments.arguments; + +import datadog.trace.junit.utils.config.WithConfig; +import io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator; +import io.opentelemetry.context.propagation.TextMapPropagator; +import java.util.Map; +import java.util.stream.Stream; +import org.junit.jupiter.params.provider.Arguments; + +@WithConfig(key = "trace.propagation.style", value = "datadog") +class OtelW3cPropagatorTest extends AbstractPropagatorTest { + @Override + TextMapPropagator propagator() { + return W3CTraceContextPropagator.getInstance(); + } + + static Stream values() { + return Stream.of( + arguments( + headers("traceparent", "00-00000000000000001111111111111111-2222222222222222-00"), + "00000000000000001111111111111111", + "2222222222222222", + UNSET), + arguments( + headers("traceparent", "00-00000000000000001111111111111111-2222222222222222-01"), + "00000000000000001111111111111111", + "2222222222222222", + SAMPLER_KEEP)); + } + + @Override + void assertInjectedHeaders( + Map headers, String traceId, String spanId, byte sampling) { + String sampleFlag = sampling == SAMPLER_KEEP ? "01" : "00"; + String expectedTraceParent = "00-" + traceId + "-" + spanId + "-" + sampleFlag; + assertEquals(expectedTraceParent, headers.get("traceparent")); + } +} diff --git a/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.4/src/test/java/opentelemetry14/context/propagation/TextMap.java b/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.4/src/test/java/opentelemetry14/context/propagation/TextMap.java new file mode 100644 index 00000000000..8fdf35bba5f --- /dev/null +++ b/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.4/src/test/java/opentelemetry14/context/propagation/TextMap.java @@ -0,0 +1,29 @@ +package opentelemetry14.context.propagation; + +import io.opentelemetry.context.propagation.TextMapGetter; +import io.opentelemetry.context.propagation.TextMapSetter; +import java.util.Map; +import javax.annotation.Nullable; + +public class TextMap + implements TextMapGetter>, TextMapSetter> { + public static final TextMap INSTANCE = new TextMap(); + + @Override + public Iterable keys(Map carrier) { + return carrier.keySet(); + } + + @Override + @Nullable + public String get(@Nullable Map carrier, String key) { + return carrier == null ? null : carrier.get(key); + } + + @Override + public void set(@Nullable Map carrier, String key, String value) { + if (carrier != null) { + carrier.put(key, value); + } + } +} diff --git a/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.4/src/test/java/opentelemetry14/context/propagation/W3cPropagatorTest.java b/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.4/src/test/java/opentelemetry14/context/propagation/W3cPropagatorTest.java new file mode 100644 index 00000000000..e1612c2bded --- /dev/null +++ b/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.4/src/test/java/opentelemetry14/context/propagation/W3cPropagatorTest.java @@ -0,0 +1,35 @@ +package opentelemetry14.context.propagation; + +import static datadog.trace.api.sampling.PrioritySampling.SAMPLER_KEEP; +import static datadog.trace.api.sampling.PrioritySampling.UNSET; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.params.provider.Arguments.arguments; + +import datadog.trace.junit.utils.config.WithConfig; +import java.util.Map; +import java.util.stream.Stream; +import org.junit.jupiter.params.provider.Arguments; + +@WithConfig(key = "trace.propagation.style", value = "tracecontext") +class W3cPropagatorTest extends AgentPropagatorTest { + static Stream values() { + return Stream.of( + arguments( + headers("traceparent", "00-11111111111111111111111111111111-2222222222222222-00"), + "11111111111111111111111111111111", + "2222222222222222", + UNSET), + arguments( + headers("traceparent", "00-11111111111111111111111111111111-2222222222222222-01"), + "11111111111111111111111111111111", + "2222222222222222", + SAMPLER_KEEP)); + } + + @Override + void assertInjectedHeaders( + Map headers, String traceId, String spanId, byte sampling) { + String traceFlags = sampling == SAMPLER_KEEP ? "01" : "00"; + assertEquals("00-" + traceId + "-" + spanId + "-" + traceFlags, headers.get("traceparent")); + } +} diff --git a/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.4/src/test/java/opentelemetry14/context/propagation/W3cPropagatorTracestateTest.java b/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.4/src/test/java/opentelemetry14/context/propagation/W3cPropagatorTracestateTest.java new file mode 100644 index 00000000000..d51051ca9e0 --- /dev/null +++ b/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.4/src/test/java/opentelemetry14/context/propagation/W3cPropagatorTracestateTest.java @@ -0,0 +1,58 @@ +package opentelemetry14.context.propagation; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import datadog.trace.junit.utils.config.WithConfig; +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.propagation.TextMapPropagator; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import opentelemetry14.AbstractOpenTelemetry14Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +@WithConfig(key = "trace.propagation.style", value = "tracecontext") +class W3cPropagatorTracestateTest extends AbstractOpenTelemetry14Test { + @ParameterizedTest + @ValueSource(strings = {"foo=1,bar=2", "dd=s:0,foo=1,bar=2", "foo=1,dd=s:0,bar=2"}) + void testTracestatePropagation(String tracestate) { + TextMapPropagator propagator = + GlobalOpenTelemetry.get().getPropagators().getTextMapPropagator(); + Map headers = new HashMap<>(); + headers.put("traceparent", "00-11111111111111111111111111111111-2222222222222222-00"); + headers.put("tracestate", tracestate); + + String[] members = + Arrays.stream(tracestate.split(",")) + .filter(member -> !member.startsWith("dd=")) + .toArray(String[]::new); + + Context context = propagator.extract(Context.root(), headers, TextMap.INSTANCE); + assertNotEquals(Context.root(), context); + + Span localSpan = this.otelTracer.spanBuilder("some-name").setParent(context).startSpan(); + io.opentelemetry.context.Scope scope = localSpan.makeCurrent(); + Map injectedHeaders = new HashMap<>(); + propagator.inject(Context.current(), injectedHeaders, TextMap.INSTANCE); + scope.close(); + localSpan.end(); + + String injectedTracestate = injectedHeaders.get("tracestate"); + assertNotNull(injectedTracestate); + String[] injectedMembers = injectedTracestate.split(","); + assertEquals(Math.min(1 + members.length, 32), injectedMembers.length); + // Datadog member should be first + assertEquals( + "dd=s:0;p:" + localSpan.getSpanContext().getSpanId() + ";t.tid:1111111111111111", + injectedMembers[0]); + // All other members preserved in order + for (int i = 0; i < Math.min(members.length, 31); i++) { + assertEquals(members[i], injectedMembers[i + 1]); + } + } +} diff --git a/utils/junit-utils/src/main/java/datadog/trace/junit/utils/config/WithConfigExtension.java b/utils/junit-utils/src/main/java/datadog/trace/junit/utils/config/WithConfigExtension.java index 5041d8dc669..23865c35517 100644 --- a/utils/junit-utils/src/main/java/datadog/trace/junit/utils/config/WithConfigExtension.java +++ b/utils/junit-utils/src/main/java/datadog/trace/junit/utils/config/WithConfigExtension.java @@ -19,6 +19,7 @@ import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.Modifier; +import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -109,12 +110,20 @@ public void afterAll(ExtensionContext context) { } private void applyDeclaredConfig(ExtensionContext context) { - // Class-level @WithConfig annotations (supports composed/meta-annotations) - List classConfigs = - AnnotationSupport.findRepeatableAnnotations( - context.getRequiredTestClass(), WithConfig.class); - for (WithConfig cfg : classConfigs) { - applyConfig(cfg); + // Class-level @WithConfig annotations + // Walk the entire class hierarchy so annotations on superclasses are applied + // (topmost first, then subclass overrides) + Class testClass = context.getRequiredTestClass(); + List> hierarchy = new ArrayList<>(); + for (Class cls = testClass; cls != null; cls = cls.getSuperclass()) { + hierarchy.add(cls); + } + for (int i = hierarchy.size() - 1; i >= 0; i--) { + List classConfigs = + AnnotationSupport.findRepeatableAnnotations(hierarchy.get(i), WithConfig.class); + for (WithConfig cfg : classConfigs) { + applyConfig(cfg); + } } // Method-level @WithConfig annotations (supports composed/meta-annotations) context