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/AbstractInstrumentationTest.java b/dd-java-agent/instrumentation-testing/src/main/java/datadog/trace/agent/test/AbstractInstrumentationTest.java
index cfcc93ed006..42d543931ff 100644
--- a/dd-java-agent/instrumentation-testing/src/main/java/datadog/trace/agent/test/AbstractInstrumentationTest.java
+++ b/dd-java-agent/instrumentation-testing/src/main/java/datadog/trace/agent/test/AbstractInstrumentationTest.java
@@ -14,7 +14,7 @@
import datadog.trace.api.Config;
import datadog.trace.api.IdGenerationStrategy;
import datadog.trace.bootstrap.instrumentation.api.AgentSpan;
-import datadog.trace.bootstrap.instrumentation.api.AgentTracer;
+import datadog.trace.bootstrap.instrumentation.api.AgentTracer.TracerAPI;
import datadog.trace.common.writer.ListWriter;
import datadog.trace.core.CoreTracer;
import datadog.trace.core.DDSpan;
@@ -30,16 +30,26 @@
import java.util.function.Function;
import java.util.function.Predicate;
import net.bytebuddy.agent.ByteBuddyAgent;
+import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.extension.ExtendWith;
import org.opentest4j.AssertionFailedError;
/**
- * This class is an experimental base to run instrumentation tests using JUnit Jupiter. It is still
- * early development, and the overall API is expected to change to leverage its extension model. The
- * current implementation is inspired and kept close to it Groovy / Spock counterpart, the {@code
- * InstrumentationSpecification}.
+ * Base class for instrumentation tests using JUnit Jupiter.
+ *
+ *
It is still early development, and the overall API might change to leverage its extension
+ * model. The current implementation is inspired and kept close to its Groovy / Spock counterpart,
+ * the {@code InstrumentationSpecification}.
+ *
+ *
+ * {@code @BeforeAll}: Installs the agent and creates a shared tracer
+ * {@code @BeforeEach}: Flushes and resets the writer
+ * {@code @AfterEach}: Flushes the tracer
+ * {@code @AfterAll}: Closes the tracer and removes the agent transformer
+ *
*/
@ExtendWith({TestClassShadowingExtension.class, AllowContextTestingExtension.class})
public abstract class AbstractInstrumentationTest {
@@ -47,31 +57,29 @@ public abstract class AbstractInstrumentationTest {
static final long TIMEOUT_MILLIS = TimeUnit.SECONDS.toMillis(20);
- protected AgentTracer.TracerAPI tracer;
+ protected static final InstrumentationTestConfig testConfig = new InstrumentationTestConfig();
- protected ListWriter writer;
+ protected static TracerAPI tracer;
+ protected static ListWriter writer;
+ private static ClassFileTransformer activeTransformer;
+ private static ClassFileTransformerListener transformerListener;
- protected ClassFileTransformer activeTransformer;
- protected ClassFileTransformerListener transformerLister;
-
- @BeforeEach
- public void init() {
+ @BeforeAll
+ static void initAll() {
// If this fails, it's likely the result of another test loading Config before it can be
// injected into the bootstrap classpath.
- // If one test extends AgentTestRunner in a module, all tests must extend
assertNull(Config.class.getClassLoader(), "Config must load on the bootstrap classpath.");
- // Initialize test tracer
- this.writer = new ListWriter();
- // Initialize test tracer
- CoreTracer tracer =
+ // Create shared test writer and tracer
+ writer = new ListWriter();
+ CoreTracer coreTracer =
CoreTracer.builder()
- .writer(this.writer)
- .idGenerationStrategy(IdGenerationStrategy.fromName(idGenerationStrategyName()))
- .strictTraceWrites(useStrictTraceWrites())
+ .writer(writer)
+ .idGenerationStrategy(IdGenerationStrategy.fromName(testConfig.idGenerationStrategy))
+ .strictTraceWrites(testConfig.strictTraceWrites)
.build();
- TracerInstaller.forceInstallGlobalTracer(tracer);
- this.tracer = tracer;
+ TracerInstaller.forceInstallGlobalTracer(coreTracer);
+ tracer = coreTracer;
ClassInjector.enableClassInjection(INSTRUMENTATION);
@@ -85,33 +93,43 @@ public void init() {
.iterator()
.hasNext(),
"No instrumentation found");
- this.transformerLister = new ClassFileTransformerListener();
- this.activeTransformer =
+ transformerListener = new ClassFileTransformerListener();
+ activeTransformer =
AgentInstaller.installBytebuddyAgent(
- INSTRUMENTATION, true, AgentInstaller.getEnabledSystems(), this.transformerLister);
- }
-
- protected String idGenerationStrategyName() {
- return "SEQUENTIAL";
+ INSTRUMENTATION, true, AgentInstaller.getEnabledSystems(), transformerListener);
}
- private boolean useStrictTraceWrites() {
- return true;
+ @BeforeEach
+ public void init() {
+ tracer.flush();
+ writer.start();
}
@AfterEach
public void tearDown() {
- this.tracer.close();
- this.writer.close();
- if (this.activeTransformer != null) {
- INSTRUMENTATION.removeTransformer(this.activeTransformer);
- this.activeTransformer = null;
- }
+ tracer.flush();
+ }
- // All cleanups should happen before these assertions.
+ @AfterAll
+ static void tearDownAll() {
+ if (tracer != null) {
+ tracer.close();
+ tracer = null;
+ }
+ if (writer != null) {
+ writer.close();
+ writer = null;
+ }
+ if (activeTransformer != null) {
+ INSTRUMENTATION.removeTransformer(activeTransformer);
+ activeTransformer = null;
+ }
+ // All cleanups should happen before this verify call.
// If not, a failing assertion may prevent cleanup
- this.transformerLister.verify();
- this.transformerLister = null;
+ if (transformerListener != null) {
+ transformerListener.verify();
+ transformerListener = null;
+ }
}
/**
@@ -134,11 +152,11 @@ protected void assertTraces(
TraceMatcher... matchers) {
int expectedTraceCount = matchers.length;
try {
- this.writer.waitForTraces(expectedTraceCount);
+ writer.waitForTraces(expectedTraceCount);
} catch (InterruptedException | TimeoutException e) {
throw new AssertionFailedError("Timeout while waiting for traces", e);
}
- TraceAssertions.assertTraces(this.writer, options, matchers);
+ TraceAssertions.assertTraces(writer, options, matchers);
}
/**
@@ -149,7 +167,7 @@ protected void assertTraces(
*/
protected void blockUntilTracesMatch(Predicate>> predicate) {
long deadline = System.currentTimeMillis() + TIMEOUT_MILLIS;
- while (!predicate.test(this.writer)) {
+ while (!predicate.test(writer)) {
if (System.currentTimeMillis() > deadline) {
throw new RuntimeException(new TimeoutException("Timed out waiting for traces/spans."));
}
@@ -161,8 +179,8 @@ protected void blockUntilTracesMatch(Predicate>> predicate) {
}
}
- protected void blockUntilChildSpansFinished(final int numberOfSpans) {
- blockUntilChildSpansFinished(this.tracer.activeSpan(), numberOfSpans);
+ protected void blockUntilChildSpansFinished(int numberOfSpans) {
+ blockUntilChildSpansFinished(tracer.activeSpan(), numberOfSpans);
}
static void blockUntilChildSpansFinished(AgentSpan span, int numberOfSpans) {
@@ -190,4 +208,20 @@ static void blockUntilChildSpansFinished(AgentSpan span, int numberOfSpans) {
}
}
}
+
+ /** Configuration for {@link AbstractInstrumentationTest}. */
+ protected static class InstrumentationTestConfig {
+ private String idGenerationStrategy = "SEQUENTIAL";
+ private boolean strictTraceWrites = true;
+
+ public InstrumentationTestConfig idGenerationStrategy(String strategy) {
+ this.idGenerationStrategy = strategy;
+ return this;
+ }
+
+ public InstrumentationTestConfig strictTraceWrites(boolean strict) {
+ this.strictTraceWrites = strict;
+ return this;
+ }
+ }
}
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:
- *
- * Headers map
- * Trace id
- * Span id
- * Whether the parent span is sampled
- *
- *
- * @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..4ce19bc5683
--- /dev/null
+++ b/dd-java-agent/instrumentation/opentelemetry/opentelemetry-1.4/src/test/java/opentelemetry14/OpenTelemetry14ActivationTest.java
@@ -0,0 +1,67 @@
+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 {
+
+ 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
+
+@WithConfig(key = "integration.opentelemetry.experimental.enabled", value = "true")
+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/docs/how_to_test_with_junit.md b/docs/how_to_test_with_junit.md
new file mode 100644
index 00000000000..3966b3249c0
--- /dev/null
+++ b/docs/how_to_test_with_junit.md
@@ -0,0 +1,350 @@
+# How to Test With JUnit Guide
+
+This guide covers the JUnit 5 testing utilities for writing instrumentation and unit tests.
+
+## Config injection with `@WithConfig`
+
+`@WithConfig` declares configuration overrides for tests. It injects system properties (`dd.` prefix) or environment variables (`DD_` prefix) and rebuilds the `Config` singleton before each test.
+
+### Class-level config
+
+Applies to all tests in the class:
+
+```java
+@WithConfig(key = "service", value = "my-service")
+@WithConfig(key = "trace.analytics.enabled", value = "true")
+class MyTest extends DDJavaSpecification {
+ @Test
+ void test() {
+ // dd.service=my-service and dd.trace.analytics.enabled=true are set
+ }
+}
+```
+
+### Method-level config
+
+Applies to a single test method, in addition to class-level config:
+
+```java
+@WithConfig(key = "service", value = "my-service")
+class MyTest extends DDJavaSpecification {
+ @Test
+ @WithConfig(key = "trace.resolver.enabled", value = "false")
+ void testWithResolverDisabled() {
+ // dd.service=my-service AND dd.trace.resolver.enabled=false
+ }
+
+ @Test
+ void testWithDefaults() {
+ // only dd.service=my-service
+ }
+}
+```
+
+### Environment variables
+
+Use `env = true` to set an environment variable instead of a system property:
+
+```java
+@WithConfig(key = "AGENT_HOST", value = "localhost", env = true)
+```
+
+### Raw keys (no auto-prefix)
+
+Use `addPrefix = false` to skip the automatic `dd.`/`DD_` prefix:
+
+```java
+@WithConfig(key = "OTEL_SERVICE_NAME", value = "test", env = true, addPrefix = false)
+```
+
+### Config with constant references
+
+Annotation values accept compile-time constants:
+
+```java
+@WithConfig(key = TracerConfig.TRACE_RESOLVER_ENABLED, value = "false")
+```
+
+### Inheritance
+
+`@WithConfig` on a superclass applies to all subclasses. When a subclass adds its own `@WithConfig`, both the parent's and the subclass's configs are applied (parent first, subclass second). This allows base classes to set shared config while subclasses add specifics:
+
+```java
+@WithConfig(key = "integration.opentelemetry.experimental.enabled", value = "true")
+abstract class AbstractOtelTest extends AbstractInstrumentationTest { }
+
+@WithConfig(key = "trace.propagation.style", value = "b3multi")
+class B3MultiTest extends AbstractOtelTest {
+ // Both configs are active
+}
+```
+
+### Composed annotations
+
+Bundle multiple configs into a reusable annotation:
+
+```java
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.TYPE, ElementType.METHOD})
+@WithConfig(key = "iast.enabled", value = "true")
+@WithConfig(key = "iast.detection.mode", value = "FULL")
+@WithConfig(key = "iast.redaction.enabled", value = "false")
+public @interface IastFullDetection {}
+```
+
+Then reuse across test classes:
+
+```java
+@IastFullDetection
+class IastTagTest extends DDJavaSpecification { }
+
+@IastFullDetection
+class IastReporterTest extends DDJavaSpecification { }
+```
+
+### Imperative config injection
+
+For dynamic values that can't be expressed in annotations, use the static methods directly:
+
+```java
+@Test
+void testDynamic() {
+ String port = startServer();
+ WithConfigExtension.injectSysConfig("trace.agent.port", port);
+ // ...
+}
+```
+
+### Lifecycle
+
+Config is rebuilt from a clean slate before each test:
+
+1. **`beforeAll`**: class-level `@WithConfig` applied + config rebuilt (available for `@BeforeAll` methods)
+2. **`beforeEach`**: properties restored, class + method `@WithConfig` applied, config rebuilt
+3. **`afterEach`**: env vars cleared, properties restored, config rebuilt
+
+This means each test starts with a clean config, and method-level `@WithConfig` doesn't leak between tests.
+
+## Instrumentation tests with `AbstractInstrumentationTest`
+
+`AbstractInstrumentationTest` is the JUnit 5 base class for instrumentation tests. It installs the agent once per test class, creates a shared tracer and writer, and provides trace assertion helpers.
+
+### Lifecycle
+
+| Phase | Scope | What happens |
+|---|---|---|
+| `@BeforeAll initAll()` | Once per class | Creates tracer + writer, installs ByteBuddy agent |
+| `@BeforeEach init()` | Per test | Flushes tracer, resets writer |
+| `@AfterEach tearDown()` | Per test | Flushes tracer |
+| `@AfterAll tearDownAll()` | Once per class | Closes tracer, removes agent transformer |
+
+### Available fields
+
+- `tracer` — the DD `TracerAPI` instance (shared across tests in the class)
+- `writer` — the `ListWriter` that captures traces written by the tracer
+
+### Configuring the tracer
+
+The tracer can be configured at class level using the `testConfig` builder. Call it from a static initializer (runs before `@BeforeAll`):
+
+```java
+class MyTest extends AbstractInstrumentationTest {
+ static {
+ testConfig.idGenerationStrategy("RANDOM").strictTraceWrites(false);
+ }
+}
+```
+
+Available settings:
+
+| Method | Default | Description |
+|---|---|---|
+| `idGenerationStrategy(String)` | `"SEQUENTIAL"` | Span ID generation strategy |
+| `strictTraceWrites(boolean)` | `true` | Enable strict trace write validation |
+
+### Basic test
+
+```java
+class HttpInstrumentationTest extends AbstractInstrumentationTest {
+ @Test
+ void testHttpRequest() {
+ // exercise the instrumented code
+ makeHttpRequest("http://example.com/api");
+
+ // assert the trace structure
+ assertTraces(
+ trace(
+ span().root().operationName("http.request").resourceName("GET /api")));
+ }
+}
+```
+
+## Trace assertion API
+
+The assertion API verifies trace structure using a fluent builder pattern. Import the static factories:
+
+```java
+import static datadog.trace.agent.test.assertions.TraceMatcher.trace;
+import static datadog.trace.agent.test.assertions.SpanMatcher.span;
+import static datadog.trace.agent.test.assertions.TagsMatcher.*;
+import static datadog.trace.agent.test.assertions.Matchers.*;
+```
+
+### Asserting traces
+
+`assertTraces` waits for traces to arrive (20s timeout), then verifies the structure:
+
+```java
+// Single trace with 2 spans
+assertTraces(
+ trace(
+ span().root().operationName("parent"),
+ span().childOfPrevious().operationName("child")));
+
+// Multiple traces
+assertTraces(
+ trace(span().root().operationName("trace-1")),
+ trace(span().root().operationName("trace-2")));
+```
+
+### Trace options
+
+```java
+import static datadog.trace.agent.test.assertions.TraceAssertions.*;
+
+// Ignore extra traces beyond the expected ones
+assertTraces(IGNORE_ADDITIONAL_TRACES,
+ trace(span().root().operationName("expected")));
+
+// Sort traces by start time before assertion
+assertTraces(SORT_BY_START_TIME,
+ trace(span().root().operationName("first")),
+ trace(span().root().operationName("second")));
+```
+
+### Span matching
+
+```java
+span()
+ // Identity
+ .root() // root span (parent ID = 0)
+ .childOfPrevious() // child of previous span in trace
+ .childOf(parentSpanId) // child of specific span
+
+ // Properties
+ .operationName("http.request") // exact match
+ .operationName(Pattern.compile("http.*"))// regex match
+ .resourceName("GET /api") // exact match
+ .serviceName("my-service") // exact match
+ .type("web") // span type
+
+ // Error
+ .error() // expects error = true
+ .error(false) // expects error = false
+
+ // Duration
+ .durationShorterThan(Duration.ofMillis(100))
+ .durationLongerThan(Duration.ofMillis(1))
+
+ // Tags
+ .tags(
+ defaultTags(), // all standard DD tags
+ tag("http.method", is("GET")), // exact tag value
+ tag("db.type", is("postgres")))
+
+ // Span links
+ .links(
+ SpanLinkMatcher.to(otherSpan),
+ SpanLinkMatcher.any())
+```
+
+### Tag matching
+
+```java
+// Default DD tags (thread name, runtime ID, sampling, etc.)
+defaultTags()
+
+// Exact value
+tag("http.status", is(200))
+
+// Custom validation
+tag("response.body", validates(v -> ((String) v).contains("success")))
+
+// Any value (just check presence)
+tag("custom.tag", any())
+
+// Error tags from exception
+error(IOException.class)
+error(IOException.class, "Connection refused")
+error(new IOException("Connection refused"))
+
+// Check tag presence without value check
+includes("tag1", "tag2")
+```
+
+### Value matchers
+
+```java
+is("expected") // equality
+isNull() // null check
+isNonNull() // non-null check
+isTrue() // boolean true
+isFalse() // boolean false
+matches("regex.*") // regex match
+matches(Pattern.compile("..."))
+validates(v -> ...) // custom predicate
+any() // accept anything
+```
+
+### Span link matching
+
+```java
+// Link to a specific span
+SpanLinkMatcher.to(parentSpan)
+
+// Link with trace/span IDs
+SpanLinkMatcher.to(traceId, spanId)
+
+// Link with additional properties
+SpanLinkMatcher.to(span)
+ .traceFlags((byte) 0x01)
+ .traceState("vendor=value")
+
+// Accept any link
+SpanLinkMatcher.any()
+```
+
+### Sorting spans within a trace
+
+```java
+import static datadog.trace.agent.test.assertions.TraceMatcher.SORT_BY_START_TIME;
+
+assertTraces(
+ trace(
+ SORT_BY_START_TIME,
+ span().root().operationName("parent"),
+ span().childOfPrevious().operationName("child")));
+```
+
+### Waiting for traces
+
+```java
+// Wait until a condition is met (20s timeout)
+blockUntilTracesMatch(traces -> traces.size() >= 2);
+
+// Wait for child spans to finish
+blockUntilChildSpansFinished(3);
+```
+
+### Accessing traces directly
+
+For assertions not covered by the fluent API, access the writer directly:
+
+```java
+writer.waitForTraces(1);
+List trace = writer.firstTrace();
+DDSpan span = trace.get(0);
+
+assertEquals("expected-op", span.getOperationName().toString());
+assertEquals(42L, span.getTag("custom.metric"));
+```
diff --git a/utils/junit-utils/src/main/java/datadog/trace/junit/utils/config/WithConfig.java b/utils/junit-utils/src/main/java/datadog/trace/junit/utils/config/WithConfig.java
index 172c24f03ff..a77045210ca 100644
--- a/utils/junit-utils/src/main/java/datadog/trace/junit/utils/config/WithConfig.java
+++ b/utils/junit-utils/src/main/java/datadog/trace/junit/utils/config/WithConfig.java
@@ -1,9 +1,11 @@
package datadog.trace.junit.utils.config;
-import java.lang.annotation.ElementType;
+import static java.lang.annotation.ElementType.METHOD;
+import static java.lang.annotation.ElementType.TYPE;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.junit.jupiter.api.extension.ExtendWith;
@@ -27,8 +29,8 @@
* }
* }
*/
-@Retention(RetentionPolicy.RUNTIME)
-@Target({ElementType.TYPE, ElementType.METHOD})
+@Retention(RUNTIME)
+@Target({TYPE, METHOD})
@Repeatable(WithConfigs.class)
@ExtendWith(WithConfigExtension.class)
public @interface WithConfig {
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..28d175ee0f1 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;
@@ -62,6 +63,7 @@ public class WithConfigExtension
private static Field configInstanceField;
private static Constructor> configConstructor;
+ private static volatile boolean configTransformerInstalled = false;
private static volatile boolean isConfigInstanceModifiable = false;
private static volatile boolean configModificationFailed = false;
@@ -73,22 +75,33 @@ public class WithConfigExtension
@Override
public void beforeAll(ExtensionContext context) {
- installConfigTransformer();
+ if (!configTransformerInstalled) {
+ installConfigTransformer();
+ configTransformerInstalled = true;
+ }
makeConfigInstanceModifiable();
assertFalse(configModificationFailed, "Config class modification failed");
+ if (isConfigInstanceModifiable) {
+ checkConfigTransformation();
+ }
if (originalSystemProperties == null) {
saveProperties();
}
+ // Apply class-level @WithConfig so config is available before @BeforeAll methods
+ applyClassLevelConfig(context);
+ if (isConfigInstanceModifiable) {
+ rebuildConfig();
+ }
}
@Override
public void beforeEach(ExtensionContext context) {
restoreProperties();
environmentVariables.clear();
+ applyDeclaredConfig(context);
if (isConfigInstanceModifiable) {
rebuildConfig();
}
- applyDeclaredConfig(context);
}
@Override
@@ -108,14 +121,29 @@ 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);
+ private static void applyDeclaredConfig(ExtensionContext context) {
+ applyClassLevelConfig(context);
+ applyMethodLevelConfig(context);
+ }
+
+ private static void applyClassLevelConfig(ExtensionContext context) {
+ // 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);
+ }
+ }
+ }
+
+ private static void applyMethodLevelConfig(ExtensionContext context) {
// Method-level @WithConfig annotations (supports composed/meta-annotations)
context
.getTestMethod()
@@ -131,12 +159,22 @@ private void applyDeclaredConfig(ExtensionContext context) {
private static void applyConfig(WithConfig cfg) {
if (cfg.env()) {
- injectEnvConfig(cfg.key(), cfg.value(), cfg.addPrefix());
+ setEnvVariable(cfg.key(), cfg.value(), cfg.addPrefix());
} else {
- injectSysConfig(cfg.key(), cfg.value(), cfg.addPrefix());
+ setSysProperty(cfg.key(), cfg.value(), cfg.addPrefix());
}
}
+ private static void setSysProperty(String name, String value, boolean addPrefix) {
+ String prefixedName = name.startsWith("dd.") || !addPrefix ? name : "dd." + name;
+ System.setProperty(prefixedName, value);
+ }
+
+ private static void setEnvVariable(String name, String value, boolean addPrefix) {
+ String prefixedName = name.startsWith("DD_") || !addPrefix ? name : "DD_" + name;
+ environmentVariables.set(prefixedName, value);
+ }
+
// endregion
// region Public static API for imperative config injection
@@ -146,9 +184,7 @@ public static void injectSysConfig(String name, String value) {
}
public static void injectSysConfig(String name, String value, boolean addPrefix) {
- checkConfigTransformation();
- String prefixedName = name.startsWith("dd.") || !addPrefix ? name : "dd." + name;
- System.setProperty(prefixedName, value);
+ setSysProperty(name, value, addPrefix);
rebuildConfig();
}
@@ -157,7 +193,6 @@ public static void removeSysConfig(String name) {
}
public static void removeSysConfig(String name, boolean addPrefix) {
- checkConfigTransformation();
String prefixedName = name.startsWith("dd.") || !addPrefix ? name : "dd." + name;
System.clearProperty(prefixedName);
rebuildConfig();
@@ -168,9 +203,7 @@ public static void injectEnvConfig(String name, String value) {
}
public static void injectEnvConfig(String name, String value, boolean addPrefix) {
- checkConfigTransformation();
- String prefixedName = name.startsWith("DD_") || !addPrefix ? name : "DD_" + name;
- environmentVariables.set(prefixedName, value);
+ setEnvVariable(name, value, addPrefix);
rebuildConfig();
}
@@ -179,7 +212,6 @@ public static void removeEnvConfig(String name) {
}
public static void removeEnvConfig(String name, boolean addPrefix) {
- checkConfigTransformation();
String prefixedName = name.startsWith("DD_") || !addPrefix ? name : "DD_" + name;
environmentVariables.removePrefixed(prefixedName);
rebuildConfig();
@@ -245,7 +277,6 @@ static void makeConfigInstanceModifiable() {
private static void rebuildConfig() {
synchronized (WithConfigExtension.class) {
- checkConfigTransformation();
try {
Object newInstConfig = instConfigConstructor.newInstance();
instConfigInstanceField.set(null, newInstConfig);
diff --git a/utils/junit-utils/src/main/java/datadog/trace/junit/utils/config/WithConfigs.java b/utils/junit-utils/src/main/java/datadog/trace/junit/utils/config/WithConfigs.java
index 64a21fe5845..e0343437823 100644
--- a/utils/junit-utils/src/main/java/datadog/trace/junit/utils/config/WithConfigs.java
+++ b/utils/junit-utils/src/main/java/datadog/trace/junit/utils/config/WithConfigs.java
@@ -1,14 +1,16 @@
package datadog.trace.junit.utils.config;
-import java.lang.annotation.ElementType;
+import static java.lang.annotation.ElementType.METHOD;
+import static java.lang.annotation.ElementType.TYPE;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.junit.jupiter.api.extension.ExtendWith;
/** Container annotation for repeatable {@link WithConfig}. */
-@Retention(RetentionPolicy.RUNTIME)
-@Target({ElementType.TYPE, ElementType.METHOD})
+@Retention(RUNTIME)
+@Target({TYPE, METHOD})
@ExtendWith(WithConfigExtension.class)
public @interface WithConfigs {
WithConfig[] value();