diff --git a/dd-java-agent/instrumentation/spark/sparkjava-2.3/build.gradle b/dd-java-agent/instrumentation/spark/sparkjava-2.3/build.gradle index d2c1dabe2a2..14ce833d991 100644 --- a/dd-java-agent/instrumentation/spark/sparkjava-2.3/build.gradle +++ b/dd-java-agent/instrumentation/spark/sparkjava-2.3/build.gradle @@ -1,4 +1,3 @@ - // building against 2.3 and testing against 2.4 because JettyHandler is available since 2.4 only muzzle { pass { @@ -22,3 +21,7 @@ dependencies { latestDepTestImplementation group: 'com.sparkjava', name: 'spark-core', version: '+' } + +tasks.withType(Test).configureEach { + jvmArgs += ['-Ddd.trace.enabled=true'] +} diff --git a/dd-java-agent/instrumentation/spark/sparkjava-2.3/gradle.lockfile b/dd-java-agent/instrumentation/spark/sparkjava-2.3/gradle.lockfile deleted file mode 100644 index 6f55ed31f4c..00000000000 --- a/dd-java-agent/instrumentation/spark/sparkjava-2.3/gradle.lockfile +++ /dev/null @@ -1,168 +0,0 @@ -# This is a Gradle generated file for dependency locking. -# Manual edits can break the build and are not advised. -# This file is expected to be part of source control. -# To regenerate this file, run: ./gradlew :dd-java-agent:instrumentation:spark:sparkjava-2.3:dependencies --write-locks -cafe.cryptography:curve25519-elisabeth:0.1.0=latestDepTestRuntimeClasspath,testRuntimeClasspath -cafe.cryptography:ed25519-elisabeth:0.1.0=latestDepTestRuntimeClasspath,testRuntimeClasspath -ch.qos.logback:logback-classic:1.2.13=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath -ch.qos.logback:logback-core:1.2.13=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath -com.blogspot.mydailyjava:weak-lock-free:0.17=buildTimeInstrumentationPlugin,compileClasspath,latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,muzzleTooling,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -com.datadoghq.okhttp3:okhttp:3.12.15=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath -com.datadoghq.okio:okio:1.17.6=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath -com.datadoghq:dd-instrument-java:0.0.4=buildTimeInstrumentationPlugin,compileClasspath,latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,muzzleBootstrap,muzzleTooling,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -com.datadoghq:dd-javac-plugin-client:0.2.2=buildTimeInstrumentationPlugin,compileClasspath,latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,muzzleBootstrap,muzzleTooling,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -com.datadoghq:java-dogstatsd-client:4.4.5=latestDepTestRuntimeClasspath,testRuntimeClasspath -com.datadoghq:sketches-java:0.8.3=latestDepTestRuntimeClasspath,testRuntimeClasspath -com.github.javaparser:javaparser-core:3.25.6=codenarc -com.github.jnr:jffi:1.3.15=latestDepTestRuntimeClasspath,testRuntimeClasspath -com.github.jnr:jnr-a64asm:1.0.0=latestDepTestRuntimeClasspath,testRuntimeClasspath -com.github.jnr:jnr-constants:0.10.4=latestDepTestRuntimeClasspath,testRuntimeClasspath -com.github.jnr:jnr-enxio:0.32.20=latestDepTestRuntimeClasspath,testRuntimeClasspath -com.github.jnr:jnr-ffi:2.2.19=latestDepTestRuntimeClasspath,testRuntimeClasspath -com.github.jnr:jnr-posix:3.1.22=latestDepTestRuntimeClasspath,testRuntimeClasspath -com.github.jnr:jnr-unixsocket:0.38.25=latestDepTestRuntimeClasspath,testRuntimeClasspath -com.github.jnr:jnr-x86asm:1.0.2=latestDepTestRuntimeClasspath,testRuntimeClasspath -com.github.spotbugs:spotbugs-annotations:4.9.8=compileClasspath,spotbugs -com.github.spotbugs:spotbugs:4.9.8=spotbugs -com.github.stephenc.jcip:jcip-annotations:1.0-1=spotbugs -com.google.auto.service:auto-service-annotations:1.1.1=annotationProcessor,compileClasspath,latestDepTestAnnotationProcessor,latestDepTestCompileClasspath,testAnnotationProcessor,testCompileClasspath -com.google.auto.service:auto-service:1.1.1=annotationProcessor,latestDepTestAnnotationProcessor,testAnnotationProcessor -com.google.auto:auto-common:1.2.1=annotationProcessor,latestDepTestAnnotationProcessor,testAnnotationProcessor -com.google.code.findbugs:jsr305:3.0.2=annotationProcessor,compileClasspath,latestDepTestAnnotationProcessor,latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,spotbugs,testAnnotationProcessor,testCompileClasspath,testRuntimeClasspath -com.google.code.gson:gson:2.13.2=spotbugs -com.google.errorprone:error_prone_annotations:2.18.0=annotationProcessor,latestDepTestAnnotationProcessor,testAnnotationProcessor -com.google.errorprone:error_prone_annotations:2.41.0=spotbugs -com.google.guava:failureaccess:1.0.1=annotationProcessor,latestDepTestAnnotationProcessor,testAnnotationProcessor -com.google.guava:guava:20.0=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath -com.google.guava:guava:32.0.1-jre=annotationProcessor,latestDepTestAnnotationProcessor,testAnnotationProcessor -com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava=annotationProcessor,latestDepTestAnnotationProcessor,testAnnotationProcessor -com.google.j2objc:j2objc-annotations:2.8=annotationProcessor,latestDepTestAnnotationProcessor,testAnnotationProcessor -com.google.re2j:re2j:1.7=latestDepTestRuntimeClasspath,testRuntimeClasspath -com.sparkjava:spark-core:2.3=compileClasspath -com.sparkjava:spark-core:2.4=testCompileClasspath,testRuntimeClasspath -com.sparkjava:spark-core:2.9.4=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath -com.squareup.moshi:moshi:1.11.0=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath -com.squareup.okhttp3:logging-interceptor:3.12.12=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath -com.squareup.okhttp3:okhttp:3.12.12=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath -com.squareup.okio:okio:1.17.5=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath -com.thoughtworks.qdox:qdox:1.12.1=codenarc -commons-fileupload:commons-fileupload:1.5=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath -commons-io:commons-io:2.11.0=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath -commons-io:commons-io:2.20.0=spotbugs -de.thetaphi:forbiddenapis:3.10=compileClasspath,latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath -io.leangen.geantyref:geantyref:1.3.16=latestDepTestRuntimeClasspath,testRuntimeClasspath -io.sqreen:libsqreen:17.3.0=latestDepTestRuntimeClasspath,testRuntimeClasspath -javax.servlet:javax.servlet-api:3.1.0=compileClasspath,latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath -jaxen:jaxen:2.0.0=spotbugs -junit:junit:4.13.2=latestDepTestRuntimeClasspath,testRuntimeClasspath -net.bytebuddy:byte-buddy-agent:1.18.10=buildTimeInstrumentationPlugin,compileClasspath,latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,muzzleTooling,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -net.bytebuddy:byte-buddy:1.18.10=buildTimeInstrumentationPlugin,compileClasspath,latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,muzzleTooling,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -net.java.dev.jna:jna-platform:5.8.0=latestDepTestRuntimeClasspath,testRuntimeClasspath -net.java.dev.jna:jna:5.8.0=latestDepTestRuntimeClasspath,testRuntimeClasspath -net.sf.saxon:Saxon-HE:12.9=spotbugs -org.apache.ant:ant-antlr:1.10.14=codenarc -org.apache.ant:ant-junit:1.10.14=codenarc -org.apache.bcel:bcel:6.11.0=spotbugs -org.apache.commons:commons-lang3:3.19.0=spotbugs -org.apache.commons:commons-text:1.14.0=spotbugs -org.apache.logging.log4j:log4j-api:2.25.2=spotbugs -org.apache.logging.log4j:log4j-core:2.25.2=spotbugs -org.apiguardian:apiguardian-api:1.1.2=latestDepTestCompileClasspath,testCompileClasspath -org.checkerframework:checker-qual:3.33.0=annotationProcessor,latestDepTestAnnotationProcessor,testAnnotationProcessor -org.codehaus.groovy:groovy-ant:3.0.23=codenarc -org.codehaus.groovy:groovy-docgenerator:3.0.23=codenarc -org.codehaus.groovy:groovy-groovydoc:3.0.23=codenarc -org.codehaus.groovy:groovy-json:3.0.23=codenarc -org.codehaus.groovy:groovy-json:3.0.25=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath -org.codehaus.groovy:groovy-templates:3.0.23=codenarc -org.codehaus.groovy:groovy-xml:3.0.23=codenarc -org.codehaus.groovy:groovy:3.0.23=codenarc -org.codehaus.groovy:groovy:3.0.25=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath -org.codenarc:CodeNarc:3.7.0=codenarc -org.dom4j:dom4j:2.2.0=spotbugs -org.eclipse.jetty.websocket:websocket-api:9.3.2.v20150730=compileClasspath -org.eclipse.jetty.websocket:websocket-api:9.3.6.v20151106=testCompileClasspath,testRuntimeClasspath -org.eclipse.jetty.websocket:websocket-api:9.4.48.v20220622=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath -org.eclipse.jetty.websocket:websocket-client:9.3.2.v20150730=compileClasspath -org.eclipse.jetty.websocket:websocket-client:9.3.6.v20151106=testCompileClasspath,testRuntimeClasspath -org.eclipse.jetty.websocket:websocket-client:9.4.48.v20220622=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath -org.eclipse.jetty.websocket:websocket-common:9.3.2.v20150730=compileClasspath -org.eclipse.jetty.websocket:websocket-common:9.3.6.v20151106=testCompileClasspath,testRuntimeClasspath -org.eclipse.jetty.websocket:websocket-common:9.4.48.v20220622=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath -org.eclipse.jetty.websocket:websocket-server:9.3.2.v20150730=compileClasspath -org.eclipse.jetty.websocket:websocket-server:9.3.6.v20151106=testCompileClasspath,testRuntimeClasspath -org.eclipse.jetty.websocket:websocket-server:9.4.48.v20220622=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath -org.eclipse.jetty.websocket:websocket-servlet:9.3.2.v20150730=compileClasspath -org.eclipse.jetty.websocket:websocket-servlet:9.3.6.v20151106=testCompileClasspath,testRuntimeClasspath -org.eclipse.jetty.websocket:websocket-servlet:9.4.48.v20220622=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath -org.eclipse.jetty:jetty-client:9.4.48.v20220622=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath -org.eclipse.jetty:jetty-http:9.3.2.v20150730=compileClasspath -org.eclipse.jetty:jetty-http:9.3.6.v20151106=testCompileClasspath,testRuntimeClasspath -org.eclipse.jetty:jetty-http:9.4.48.v20220622=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath -org.eclipse.jetty:jetty-io:9.3.2.v20150730=compileClasspath -org.eclipse.jetty:jetty-io:9.3.6.v20151106=testCompileClasspath,testRuntimeClasspath -org.eclipse.jetty:jetty-io:9.4.48.v20220622=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath -org.eclipse.jetty:jetty-security:9.3.2.v20150730=compileClasspath -org.eclipse.jetty:jetty-security:9.3.6.v20151106=testCompileClasspath,testRuntimeClasspath -org.eclipse.jetty:jetty-security:9.4.48.v20220622=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath -org.eclipse.jetty:jetty-server:9.3.2.v20150730=compileClasspath -org.eclipse.jetty:jetty-server:9.3.6.v20151106=testCompileClasspath,testRuntimeClasspath -org.eclipse.jetty:jetty-server:9.4.48.v20220622=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath -org.eclipse.jetty:jetty-servlet:9.3.2.v20150730=compileClasspath -org.eclipse.jetty:jetty-servlet:9.3.6.v20151106=testCompileClasspath,testRuntimeClasspath -org.eclipse.jetty:jetty-servlet:9.4.48.v20220622=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath -org.eclipse.jetty:jetty-util-ajax:9.4.48.v20220622=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath -org.eclipse.jetty:jetty-util:9.3.2.v20150730=compileClasspath -org.eclipse.jetty:jetty-util:9.3.6.v20151106=testCompileClasspath,testRuntimeClasspath -org.eclipse.jetty:jetty-util:9.4.48.v20220622=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath -org.eclipse.jetty:jetty-webapp:9.3.2.v20150730=compileClasspath -org.eclipse.jetty:jetty-webapp:9.3.6.v20151106=testCompileClasspath,testRuntimeClasspath -org.eclipse.jetty:jetty-webapp:9.4.48.v20220622=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath -org.eclipse.jetty:jetty-xml:9.3.2.v20150730=compileClasspath -org.eclipse.jetty:jetty-xml:9.3.6.v20151106=testCompileClasspath,testRuntimeClasspath -org.eclipse.jetty:jetty-xml:9.4.48.v20220622=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath -org.gmetrics:GMetrics:2.1.0=codenarc -org.hamcrest:hamcrest-core:1.3=latestDepTestRuntimeClasspath,testRuntimeClasspath -org.hamcrest:hamcrest:3.0=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath -org.jctools:jctools-core-jdk11:4.0.6=latestDepTestRuntimeClasspath,testRuntimeClasspath -org.jctools:jctools-core:4.0.6=latestDepTestRuntimeClasspath,testRuntimeClasspath -org.junit.jupiter:junit-jupiter-api:5.14.1=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath -org.junit.jupiter:junit-jupiter-engine:5.14.1=latestDepTestRuntimeClasspath,testRuntimeClasspath -org.junit.jupiter:junit-jupiter-params:5.14.1=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath -org.junit.jupiter:junit-jupiter:5.14.1=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath -org.junit.platform:junit-platform-commons:1.14.1=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath -org.junit.platform:junit-platform-engine:1.14.1=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath -org.junit.platform:junit-platform-launcher:1.14.1=latestDepTestRuntimeClasspath,testRuntimeClasspath -org.junit.platform:junit-platform-runner:1.14.1=latestDepTestRuntimeClasspath,testRuntimeClasspath -org.junit.platform:junit-platform-suite-api:1.14.1=latestDepTestRuntimeClasspath,testRuntimeClasspath -org.junit.platform:junit-platform-suite-commons:1.14.1=latestDepTestRuntimeClasspath,testRuntimeClasspath -org.junit:junit-bom:5.14.0=spotbugs -org.junit:junit-bom:5.14.1=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath -org.mockito:mockito-core:4.4.0=latestDepTestRuntimeClasspath,testRuntimeClasspath -org.objenesis:objenesis:3.3=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath -org.opentest4j:opentest4j:1.3.0=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath -org.ow2.asm:asm-analysis:9.7.1=latestDepTestRuntimeClasspath,testRuntimeClasspath -org.ow2.asm:asm-analysis:9.9=spotbugs -org.ow2.asm:asm-commons:9.9=spotbugs -org.ow2.asm:asm-commons:9.9.1=latestDepTestRuntimeClasspath,testRuntimeClasspath -org.ow2.asm:asm-tree:9.9=spotbugs -org.ow2.asm:asm-tree:9.9.1=latestDepTestRuntimeClasspath,testRuntimeClasspath -org.ow2.asm:asm-util:9.7.1=latestDepTestRuntimeClasspath,testRuntimeClasspath -org.ow2.asm:asm-util:9.9=spotbugs -org.ow2.asm:asm:9.9=spotbugs -org.ow2.asm:asm:9.9.1=buildTimeInstrumentationPlugin,compileClasspath,latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,muzzleTooling,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.slf4j:jcl-over-slf4j:1.7.30=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath -org.slf4j:jul-to-slf4j:1.7.30=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath -org.slf4j:log4j-over-slf4j:1.7.30=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath -org.slf4j:slf4j-api:1.7.30=buildTimeInstrumentationPlugin,compileClasspath,muzzleBootstrap,muzzleTooling,runtimeClasspath -org.slf4j:slf4j-api:1.7.32=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath -org.slf4j:slf4j-api:2.0.17=spotbugs,spotbugsSlf4j -org.slf4j:slf4j-simple:1.7.12=compileClasspath -org.slf4j:slf4j-simple:2.0.17=spotbugsSlf4j -org.snakeyaml:snakeyaml-engine:2.9=buildTimeInstrumentationPlugin,latestDepTestRuntimeClasspath,muzzleTooling,runtimeClasspath,testRuntimeClasspath -org.spockframework:spock-bom:2.4-groovy-3.0=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath -org.spockframework:spock-core:2.4-groovy-3.0=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath -org.tabletest:tabletest-junit:1.2.1=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath -org.tabletest:tabletest-parser:1.2.0=latestDepTestCompileClasspath,latestDepTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath -org.xmlresolver:xmlresolver:5.3.3=spotbugs -empty=spotbugsPlugins diff --git a/dd-java-agent/instrumentation/spark/sparkjava-2.3/src/main/java/datadog/trace/instrumentation/sparkjava/RoutesInstrumentation.java b/dd-java-agent/instrumentation/spark/sparkjava-2.3/src/main/java/datadog/trace/instrumentation/sparkjava/RoutesInstrumentation.java index b4dbe6e5c02..3534719bea7 100644 --- a/dd-java-agent/instrumentation/spark/sparkjava-2.3/src/main/java/datadog/trace/instrumentation/sparkjava/RoutesInstrumentation.java +++ b/dd-java-agent/instrumentation/spark/sparkjava-2.3/src/main/java/datadog/trace/instrumentation/sparkjava/RoutesInstrumentation.java @@ -3,6 +3,8 @@ import static datadog.trace.agent.tooling.bytebuddy.matcher.NameMatchers.named; import static datadog.trace.bootstrap.instrumentation.api.AgentTracer.activeSpan; import static datadog.trace.bootstrap.instrumentation.decorator.http.HttpResourceDecorator.HTTP_RESOURCE_DECORATOR; +import static datadog.trace.instrumentation.sparkjava.SparkJavaDecorator.DECORATE; +import static datadog.trace.instrumentation.sparkjava.SparkJavaDecorator.SPARK_JAVA; import static net.bytebuddy.matcher.ElementMatchers.isPublic; import static net.bytebuddy.matcher.ElementMatchers.returns; import static net.bytebuddy.matcher.ElementMatchers.takesArgument; @@ -11,6 +13,7 @@ import datadog.trace.agent.tooling.Instrumenter; import datadog.trace.agent.tooling.InstrumenterModule; import datadog.trace.bootstrap.instrumentation.api.AgentSpan; +import datadog.trace.bootstrap.instrumentation.api.Tags; import net.bytebuddy.asm.Advice; import spark.route.HttpMethod; import spark.routematch.RouteMatch; @@ -20,12 +23,12 @@ public class RoutesInstrumentation extends InstrumenterModule.Tracing implements Instrumenter.ForSingleType, Instrumenter.HasMethodAdvice { public RoutesInstrumentation() { - super("sparkjava", "sparkjava-2.4"); + super("sparkjava", "sparkjava-2.3"); } @Override - public boolean defaultEnabled() { - return false; + public String[] helperClassNames() { + return new String[] {packageName + ".SparkJavaDecorator"}; } @Override @@ -52,6 +55,8 @@ public static void routeMatchEnricher( final AgentSpan span = activeSpan(); if (span != null && routeMatch != null) { HTTP_RESOURCE_DECORATOR.withRoute(span, method.name(), routeMatch.getMatchUri()); + span.setSpanName(DECORATE.spanName()); + span.setTag(Tags.COMPONENT, SPARK_JAVA); } } } diff --git a/dd-java-agent/instrumentation/spark/sparkjava-2.3/src/main/java/datadog/trace/instrumentation/sparkjava/SparkJavaDecorator.java b/dd-java-agent/instrumentation/spark/sparkjava-2.3/src/main/java/datadog/trace/instrumentation/sparkjava/SparkJavaDecorator.java new file mode 100644 index 00000000000..4657f85ccad --- /dev/null +++ b/dd-java-agent/instrumentation/spark/sparkjava-2.3/src/main/java/datadog/trace/instrumentation/sparkjava/SparkJavaDecorator.java @@ -0,0 +1,31 @@ +package datadog.trace.instrumentation.sparkjava; + +import datadog.trace.bootstrap.instrumentation.api.UTF8BytesString; +import datadog.trace.bootstrap.instrumentation.decorator.BaseDecorator; + +public class SparkJavaDecorator extends BaseDecorator { + + public static final SparkJavaDecorator DECORATE = new SparkJavaDecorator(); + + public static final CharSequence SPARK_JAVA = UTF8BytesString.create("spark-java"); + public static final CharSequence SPARK_REQUEST = UTF8BytesString.create("spark.request"); + + @Override + protected String[] instrumentationNames() { + return new String[] {"sparkjava"}; + } + + @Override + protected CharSequence spanType() { + return "web"; + } + + @Override + protected CharSequence component() { + return SPARK_JAVA; + } + + public CharSequence spanName() { + return SPARK_REQUEST; + } +} diff --git a/dd-java-agent/instrumentation/spark/sparkjava-2.3/src/test/groovy/SparkJavaBasedTest.groovy b/dd-java-agent/instrumentation/spark/sparkjava-2.3/src/test/groovy/SparkJavaBasedTest.groovy deleted file mode 100644 index 2c33e8d745b..00000000000 --- a/dd-java-agent/instrumentation/spark/sparkjava-2.3/src/test/groovy/SparkJavaBasedTest.groovy +++ /dev/null @@ -1,73 +0,0 @@ -import datadog.trace.agent.test.InstrumentationSpecification -import datadog.trace.agent.test.utils.OkHttpUtils -import datadog.trace.agent.test.utils.PortUtils -import datadog.trace.api.DDSpanTypes -import datadog.trace.bootstrap.instrumentation.api.Tags -import okhttp3.OkHttpClient -import okhttp3.Request -import spark.Spark -import spock.lang.Shared - -class SparkJavaBasedTest extends InstrumentationSpecification { - - @Override - void configurePreAgent() { - super.configurePreAgent() - injectSysConfig("dd.integration.jetty.enabled", "true") - injectSysConfig("dd.integration.sparkjava.enabled", "true") - } - - @Shared - int port - - OkHttpClient client = OkHttpUtils.client() - - def setupSpec() { - port = PortUtils.randomOpenPort() - TestSparkJavaApplication.initSpark(port) - } - - def cleanupSpec() { - Spark.stop() - } - - def "generates spans"() { - setup: - def request = new Request.Builder() - .url("http://localhost:$port/param/asdf1234") - .get() - .build() - def response = client.newCall(request).execute() - - expect: - port != 0 - response.body().string() == "Hello asdf1234" - - assertTraces(1) { - trace(1) { - span { - operationName "servlet.request" - resourceName "GET /param/:param" - spanType DDSpanTypes.HTTP_SERVER - errored false - parent() - tags { - "$Tags.COMPONENT" "jetty-server" - "$Tags.SPAN_KIND" Tags.SPAN_KIND_SERVER - "$Tags.PEER_HOST_IPV4" "127.0.0.1" - "$Tags.PEER_PORT" Integer - "$Tags.HTTP_URL" "http://localhost:$port/param/asdf1234" - "$Tags.HTTP_HOSTNAME" "localhost" - "$Tags.HTTP_METHOD" "GET" - "$Tags.HTTP_STATUS" 200 - "$Tags.HTTP_ROUTE" String - "$Tags.HTTP_USER_AGENT" String - "$Tags.HTTP_CLIENT_IP" "127.0.0.1" - "$Tags.NETWORK_CLIENT_IP" "127.0.0.1" - defaultTags() - } - } - } - } - } -} diff --git a/dd-java-agent/instrumentation/spark/sparkjava-2.3/src/test/java/TestSparkJavaApplication.java b/dd-java-agent/instrumentation/spark/sparkjava-2.3/src/test/java/TestSparkJavaApplication.java deleted file mode 100644 index 93f904c7206..00000000000 --- a/dd-java-agent/instrumentation/spark/sparkjava-2.3/src/test/java/TestSparkJavaApplication.java +++ /dev/null @@ -1,19 +0,0 @@ -import spark.Spark; - -public class TestSparkJavaApplication { - - public static void initSpark(final int port) { - Spark.port(port); - Spark.get("/", (req, res) -> "Hello World"); - - Spark.get("/param/:param", (req, res) -> "Hello " + req.params("param")); - - Spark.get( - "/exception/:param", - (req, res) -> { - throw new RuntimeException(req.params("param")); - }); - - Spark.awaitInitialization(); - } -} diff --git a/dd-java-agent/instrumentation/spark/sparkjava-2.3/src/test/java/datadog/trace/instrumentation/sparkjava/SparkJavaForkedTest.java b/dd-java-agent/instrumentation/spark/sparkjava-2.3/src/test/java/datadog/trace/instrumentation/sparkjava/SparkJavaForkedTest.java new file mode 100644 index 00000000000..9b5de61b72b --- /dev/null +++ b/dd-java-agent/instrumentation/spark/sparkjava-2.3/src/test/java/datadog/trace/instrumentation/sparkjava/SparkJavaForkedTest.java @@ -0,0 +1,226 @@ +package datadog.trace.instrumentation.sparkjava; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import datadog.trace.agent.test.AbstractInstrumentationTest; +import datadog.trace.agent.test.utils.PortUtils; +import datadog.trace.core.DDSpan; +import java.io.BufferedReader; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeoutException; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import spark.Request; +import spark.Response; +import spark.Route; +import spark.Spark; + +/** + * Forked test for the SparkJava 2.x instrumentation, running in an isolated JVM. This validates + * that the {@link RoutesInstrumentation} loads and enriches Jetty server spans correctly when the + * agent starts from scratch — no leftover state from other test classes. + * + *

This test focuses on the core enrichment contract: when a request matches a SparkJava route, + * the server span gets operation name {@code spark.request}, component {@code spark-java}, and the + * resource name / http.route reflect the parameterized route pattern. + */ +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +public class SparkJavaForkedTest extends AbstractInstrumentationTest { + + private int actualPort; + + @BeforeAll + void setupServer() { + actualPort = PortUtils.randomOpenPort(); + Spark.port(actualPort); + + Spark.get( + "/ping", + new Route() { + @Override + public Object handle(Request request, Response response) { + response.type("text/plain"); + return "pong"; + } + }); + + Spark.get( + "/items/:id", + new Route() { + @Override + public Object handle(Request request, Response response) { + response.type("application/json"); + return "{\"id\": \"" + request.params(":id") + "\"}"; + } + }); + + Spark.get( + "/fail", + new Route() { + @Override + public Object handle(Request request, Response response) { + throw new RuntimeException("Forked test error"); + } + }); + + Spark.awaitInitialization(); + } + + @AfterAll + void tearDownServer() throws InterruptedException { + Spark.stop(); + Thread.sleep(500); + } + + @Test + void simpleRouteEnrichesServerSpan() throws InterruptedException, TimeoutException { + httpGet("/ping"); + + DDSpan serverSpan = waitForServerSpan(); + assertServerSpan(serverSpan, "GET", "/ping", 200, false); + } + + @Test + void parameterizedRoutePatternInResourceName() throws InterruptedException, TimeoutException { + httpGet("/items/42"); + + DDSpan serverSpan = waitForServerSpan(); + assertServerSpan(serverSpan, "GET", "/items/:id", 200, false); + } + + @Test + void errorRouteProducesErrorSpan() throws InterruptedException, TimeoutException { + httpGet("/fail"); + + DDSpan serverSpan = waitForServerSpan(); + assertServerSpan(serverSpan, "GET", "/fail", 500, true); + } + + // --------------------------------------------------------------- + // Helper methods + // --------------------------------------------------------------- + + /** + * Validates the complete structure of a server span, covering both SparkJava enrichment and the + * underlying Jetty server span baseline. This single-point-of-assertion prevents regressions when + * new required tags are added. + * + *

SparkJava enrichment (set by {@link RoutesInstrumentation}): + * + *

+ * + *

Jetty baseline (set by the Jetty server instrumentation): + * + *

+ * + * @param span the server span to validate + * @param httpMethod the expected HTTP method (e.g., "GET", "POST") + * @param route the expected route pattern (e.g., "/items/:id") + * @param statusCode the expected HTTP status code + * @param isError whether the span should be marked as errored + */ + private void assertServerSpan( + DDSpan span, String httpMethod, String route, int statusCode, boolean isError) { + assertNotNull(span, "Expected a server span for " + httpMethod + " " + route); + + // SparkJava enrichment assertions + assertEquals( + "spark.request", + span.getOperationName().toString(), + "Operation name should be 'spark.request'"); + assertEquals( + "spark-java", + String.valueOf(span.getTag("component")), + "component tag should be 'spark-java'"); + assertEquals( + httpMethod + " " + route, + span.getResourceName().toString(), + "Resource name should be HTTP_METHOD + route_pattern"); + assertEquals( + route, + String.valueOf(span.getTag("http.route")), + "http.route should contain the route pattern, not the actual path"); + + // Jetty baseline assertions + assertEquals("web", span.getSpanType(), "Span type should be 'web'"); + assertEquals( + "server", String.valueOf(span.getTag("span.kind")), "span.kind should be 'server'"); + assertEquals(httpMethod, String.valueOf(span.getTag("http.method")), "http.method tag"); + assertEquals(statusCode, span.getTag("http.status_code"), "http.status_code tag"); + assertNotNull(span.getTag("http.url"), "http.url tag should be set"); + assertEquals(isError, span.isError(), "error flag"); + } + + /** + * Waits for at least one trace to be written and returns the server span. + * + * @return the server span (never null — fails assertion if not found) + * @throws InterruptedException if the thread is interrupted while waiting + * @throws TimeoutException if no trace is written within the timeout + */ + private DDSpan waitForServerSpan() throws InterruptedException, TimeoutException { + writer.waitForTraces(1); + List spans = new ArrayList<>(); + for (List trace : writer) { + spans.addAll(trace); + } + DDSpan serverSpan = null; + for (DDSpan span : spans) { + if ("server".equals(String.valueOf(span.getTag("span.kind"))) + || "web".equals(span.getSpanType())) { + serverSpan = span; + break; + } + } + assertNotNull(serverSpan, "Expected to find a server span in the collected traces"); + return serverSpan; + } + + /** + * Makes an HTTP GET request to the SparkJava server. + * + * @param path the request path + * @return the HTTP status code + */ + private int httpGet(String path) { + try { + URL url = new URL("http://localhost:" + actualPort + path); + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + conn.setRequestMethod("GET"); + conn.setConnectTimeout(5000); + conn.setReadTimeout(5000); + int status = conn.getResponseCode(); + InputStream is = + conn.getResponseCode() >= 400 ? conn.getErrorStream() : conn.getInputStream(); + if (is != null) { + BufferedReader reader = new BufferedReader(new InputStreamReader(is)); + while (reader.readLine() != null) { + // drain + } + reader.close(); + } + conn.disconnect(); + return status; + } catch (Exception e) { + throw new RuntimeException("HTTP GET failed for path " + path, e); + } + } +} diff --git a/dd-java-agent/instrumentation/spark/sparkjava-2.3/src/test/java/datadog/trace/instrumentation/sparkjava/SparkJavaTest.java b/dd-java-agent/instrumentation/spark/sparkjava-2.3/src/test/java/datadog/trace/instrumentation/sparkjava/SparkJavaTest.java new file mode 100644 index 00000000000..6abafc9051b --- /dev/null +++ b/dd-java-agent/instrumentation/spark/sparkjava-2.3/src/test/java/datadog/trace/instrumentation/sparkjava/SparkJavaTest.java @@ -0,0 +1,687 @@ +package datadog.trace.instrumentation.sparkjava; + +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 static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import datadog.trace.agent.test.AbstractInstrumentationTest; +import datadog.trace.agent.test.utils.PortUtils; +import datadog.trace.api.DDTraceId; +import datadog.trace.core.DDSpan; +import java.io.BufferedReader; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeoutException; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import spark.Request; +import spark.Response; +import spark.Route; +import spark.Spark; + +/** + * Tests for the SparkJava 2.x HTTP server instrumentation. + * + *

SparkJava runs on an embedded Jetty server. The Jetty instrumentation creates the server span, + * and the SparkJava {@link RoutesInstrumentation} enriches it with route information from the + * {@code Routes.find()} method. + * + *

Acceptance criteria verified by these tests: + * + *

+ */ +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +public class SparkJavaTest extends AbstractInstrumentationTest { + + private int actualPort; + + @BeforeAll + void setupServer() { + actualPort = PortUtils.randomOpenPort(); + Spark.port(actualPort); + + Spark.get( + "/hello", + new Route() { + @Override + public Object handle(Request request, Response response) { + response.type("text/plain"); + return "Hello, World!"; + } + }); + + Spark.get( + "/hello/:name", + new Route() { + @Override + public Object handle(Request request, Response response) { + String name = request.params(":name"); + response.type("text/plain"); + return "Hello, " + name + "!"; + } + }); + + Spark.post( + "/users", + new Route() { + @Override + public Object handle(Request request, Response response) { + response.type("application/json"); + response.status(201); + return "{\"created\": true}"; + } + }); + + Spark.put( + "/users/:id", + new Route() { + @Override + public Object handle(Request request, Response response) { + String id = request.params(":id"); + response.type("application/json"); + return "{\"updated\": true, \"id\": \"" + id + "\"}"; + } + }); + + Spark.delete( + "/users/:id", + new Route() { + @Override + public Object handle(Request request, Response response) { + String id = request.params(":id"); + response.type("application/json"); + return "{\"deleted\": true, \"id\": \"" + id + "\"}"; + } + }); + + Spark.get( + "/error", + new Route() { + @Override + public Object handle(Request request, Response response) { + throw new RuntimeException("Intentional error for testing"); + } + }); + + Spark.get( + "/files/*", + new Route() { + @Override + public Object handle(Request request, Response response) { + response.type("text/plain"); + return "file content for " + request.splat()[0]; + } + }); + + Spark.before( + "/filtered/*", + new spark.Filter() { + @Override + public void handle(Request request, Response response) { + response.header("X-Filtered", "true"); + } + }); + + Spark.get( + "/filtered/resource", + new Route() { + @Override + public Object handle(Request request, Response response) { + response.type("text/plain"); + return "filtered response"; + } + }); + + Spark.after( + "/after-filtered/*", + new spark.Filter() { + @Override + public void handle(Request request, Response response) { + response.header("X-After-Filtered", "true"); + } + }); + + Spark.get( + "/after-filtered/resource", + new Route() { + @Override + public Object handle(Request request, Response response) { + response.type("text/plain"); + return "after-filtered response"; + } + }); + + Spark.awaitInitialization(); + } + + @AfterAll + void tearDownServer() throws InterruptedException { + Spark.stop(); + Thread.sleep(500); + } + + // --------------------------------------------------------------- + // Route enrichment tests — verify SparkJava sets operation name, + // component, resource name, and http.route on the Jetty server span + // --------------------------------------------------------------- + + @Test + void getRouteCreatesServerSpanWithCorrectTags() throws InterruptedException, TimeoutException { + httpGet("/hello"); + + DDSpan serverSpan = waitForServerSpan(); + assertServerSpan(serverSpan, "GET", "/hello", 200, false); + } + + @Test + void getRouteWithPathParamUsesParameterizedRoutePattern() + throws InterruptedException, TimeoutException { + httpGet("/hello/spark-user"); + + DDSpan serverSpan = waitForServerSpan(); + // The route pattern should be /hello/:name (parameterized), not /hello/spark-user (actual path) + assertServerSpan(serverSpan, "GET", "/hello/:name", 200, false); + } + + @Test + void postRouteCreatesServerSpanWithCorrectStatusCode() + throws InterruptedException, TimeoutException { + httpRequest("/users", "POST", "test-body"); + + DDSpan serverSpan = waitForServerSpan(); + assertServerSpan(serverSpan, "POST", "/users", 201, false); + } + + @Test + void putRouteWithPathParamCreatesServerSpan() throws InterruptedException, TimeoutException { + httpRequest("/users/42", "PUT", "update-body"); + + DDSpan serverSpan = waitForServerSpan(); + assertServerSpan(serverSpan, "PUT", "/users/:id", 200, false); + } + + @Test + void deleteRouteWithPathParamCreatesServerSpan() throws InterruptedException, TimeoutException { + httpRequest("/users/99", "DELETE", null); + + DDSpan serverSpan = waitForServerSpan(); + assertServerSpan(serverSpan, "DELETE", "/users/:id", 200, false); + } + + @Test + void wildcardRouteUsesWildcardPattern() throws InterruptedException, TimeoutException { + httpGet("/files/documents/report.pdf"); + + DDSpan serverSpan = waitForServerSpan(); + assertServerSpan(serverSpan, "GET", "/files/*", 200, false); + } + + @Test + void beforeFilterDoesNotInterfereWithRouteEnrichment() + throws InterruptedException, TimeoutException { + httpGet("/filtered/resource"); + + DDSpan serverSpan = waitForServerSpan(); + assertServerSpan(serverSpan, "GET", "/filtered/resource", 200, false); + } + + @Test + void afterFilterDoesNotInterfereWithSpanData() throws InterruptedException, TimeoutException { + httpGet("/after-filtered/resource"); + + DDSpan serverSpan = waitForServerSpan(); + assertServerSpan(serverSpan, "GET", "/after-filtered/resource", 200, false); + } + + // --------------------------------------------------------------- + // Span structure tests — verify individual span attributes + // --------------------------------------------------------------- + + @Test + void serverSpanHasCorrectType() throws InterruptedException, TimeoutException { + httpGet("/hello"); + + DDSpan serverSpan = waitForServerSpan(); + assertEquals("web", serverSpan.getSpanType(), "HTTP server spans should have type 'web'"); + assertEquals( + "server", + String.valueOf(serverSpan.getTag("span.kind")), + "Span kind should be 'server' for HTTP server spans"); + } + + @Test + void serverSpanHasCorrectOperationName() throws InterruptedException, TimeoutException { + httpGet("/hello"); + + DDSpan serverSpan = waitForServerSpan(); + assertEquals( + "spark.request", + serverSpan.getOperationName().toString(), + "Operation name should be 'spark.request' for SparkJava routes"); + } + + @Test + void serverSpanIncludesHttpUrlTag() throws InterruptedException, TimeoutException { + httpGet("/hello"); + + DDSpan serverSpan = waitForServerSpan(); + String httpUrl = String.valueOf(serverSpan.getTag("http.url")); + assertNotNull(httpUrl, "Expected http.url tag to be set"); + assertTrue( + httpUrl.contains("/hello"), + "http.url tag should contain the request path, got: " + httpUrl); + assertTrue(httpUrl.startsWith("http"), "http.url tag should be a full URL, got: " + httpUrl); + } + + // --------------------------------------------------------------- + // Error handling tests + // --------------------------------------------------------------- + + @Test + void errorRouteCreatesServerSpanWithErrorFlag() throws InterruptedException, TimeoutException { + httpGet("/error"); + + DDSpan serverSpan = waitForServerSpan(); + assertServerSpan(serverSpan, "GET", "/error", 500, true); + // SparkJava catches exceptions internally via its ExceptionMapper before they propagate + // to Jetty. The error flag is set solely from the 500 status code by Jetty's + // HttpServerDecorator.onResponse(). Because the exception never reaches the Jetty handler, + // error.type/error.message/error.stack are not populated on the span. + assertNull( + serverSpan.getTag("error.type"), + "error.type should not be set — SparkJava catches exceptions before Jetty sees them"); + assertNull( + serverSpan.getTag("error.message"), + "error.message should not be set — SparkJava catches exceptions before Jetty sees them"); + assertNull( + serverSpan.getTag("error.stack"), + "error.stack should not be set — SparkJava catches exceptions before Jetty sees them"); + } + + @Test + void notFoundRouteCreates404Span() throws InterruptedException, TimeoutException { + httpGet("/nonexistent"); + + DDSpan serverSpan = waitForServerSpan(); + // For 404, Routes.find() returns null so SparkJava enrichment does not fire. + // The span retains Jetty defaults — no http.route or spark-java component tag is expected. + // We can't use assertServerSpan() here because it asserts SparkJava-specific enrichment + // (operation name, component, http.route) that won't be present on an unmatched route. + assertEquals("web", serverSpan.getSpanType(), "Span type should be 'web' even for 404"); + assertEquals( + "server", String.valueOf(serverSpan.getTag("span.kind")), "span.kind should be 'server'"); + assertEquals(404, serverSpan.getTag("http.status_code"), "http.status_code should be 404"); + assertEquals("GET", String.valueOf(serverSpan.getTag("http.method")), "http.method tag"); + assertNotNull(serverSpan.getTag("http.url"), "http.url tag should be set even for 404"); + assertEquals(false, serverSpan.isError(), "404 should not be marked as an error"); + } + + // --------------------------------------------------------------- + // Context propagation tests + // --------------------------------------------------------------- + + @Test + void contextPropagationLinksServerSpanToParentTrace() + throws InterruptedException, TimeoutException { + Map headers = new HashMap<>(); + headers.put("x-datadog-trace-id", "123456789"); + headers.put("x-datadog-parent-id", "987654321"); + httpGetWithHeaders("/hello", headers); + + DDSpan serverSpan = waitForServerSpan(); + assertServerSpan(serverSpan, "GET", "/hello", 200, false); + assertEquals( + DDTraceId.from("123456789"), + serverSpan.getTraceId(), + "Server span should inherit the trace ID from the propagated Datadog headers"); + assertEquals( + 987654321L, + serverSpan.getParentId(), + "Server span's parent ID should match the x-datadog-parent-id header value"); + } + + @Test + void contextPropagationPreservesSparkJavaRouteEnrichment() + throws InterruptedException, TimeoutException { + Map headers = new HashMap<>(); + headers.put("x-datadog-trace-id", "111111111"); + headers.put("x-datadog-parent-id", "222222222"); + httpGetWithHeaders("/hello", headers); + + DDSpan serverSpan = waitForServerSpan(); + // Verify SparkJava route enrichment still works with propagated context + assertServerSpan(serverSpan, "GET", "/hello", 200, false); + // Verify context propagation + assertEquals( + DDTraceId.from("111111111"), + serverSpan.getTraceId(), + "Trace ID should be inherited from propagated headers"); + assertEquals(222222222L, serverSpan.getParentId()); + } + + @Test + void contextPropagationWorksWithParameterizedRoutes() + throws InterruptedException, TimeoutException { + Map headers = new HashMap<>(); + headers.put("x-datadog-trace-id", "333333333"); + headers.put("x-datadog-parent-id", "444444444"); + httpGetWithHeaders("/hello/sparkuser", headers); + + DDSpan serverSpan = waitForServerSpan(); + assertServerSpan(serverSpan, "GET", "/hello/:name", 200, false); + assertEquals( + DDTraceId.from("333333333"), + serverSpan.getTraceId(), + "Trace ID should be inherited from propagated headers"); + assertEquals( + 444444444L, serverSpan.getParentId(), "Parent ID should match propagated header value"); + } + + @Test + void contextPropagationPreservesErrorStatusOnErrorRoutes() + throws InterruptedException, TimeoutException { + Map headers = new HashMap<>(); + headers.put("x-datadog-trace-id", "555555555"); + headers.put("x-datadog-parent-id", "666666666"); + httpGetWithHeaders("/error", headers); + + DDSpan serverSpan = waitForServerSpan(); + assertServerSpan(serverSpan, "GET", "/error", 500, true); + assertEquals( + DDTraceId.from("555555555"), + serverSpan.getTraceId(), + "Trace ID should be inherited even for error routes"); + assertEquals( + 666666666L, + serverSpan.getParentId(), + "Parent ID should match propagated header even for error routes"); + } + + @Test + void differentPropagatedContextsProduceDistinctTraces() + throws InterruptedException, TimeoutException { + Map headers1 = new HashMap<>(); + headers1.put("x-datadog-trace-id", "100000001"); + headers1.put("x-datadog-parent-id", "200000001"); + httpGetWithHeaders("/hello", headers1); + + Map headers2 = new HashMap<>(); + headers2.put("x-datadog-trace-id", "100000002"); + headers2.put("x-datadog-parent-id", "200000002"); + httpGetWithHeaders("/hello", headers2); + + writer.waitForTraces(2); + List allSpans = flattenTraces(); + + // Find both server spans + DDSpan firstServerSpan = null; + DDSpan secondServerSpan = null; + for (DDSpan span : allSpans) { + if ("server".equals(String.valueOf(span.getTag("span.kind"))) + || "web".equals(span.getSpanType())) { + if (DDTraceId.from("100000001").equals(span.getTraceId())) { + firstServerSpan = span; + } else if (DDTraceId.from("100000002").equals(span.getTraceId())) { + secondServerSpan = span; + } + } + } + + assertNotNull(firstServerSpan, "Expected server span for first request (trace 100000001)"); + assertNotNull(secondServerSpan, "Expected server span for second request (trace 100000002)"); + + // Verify each span links to its own propagated context + assertNotEquals( + firstServerSpan.getTraceId(), + secondServerSpan.getTraceId(), + "Each request should have its own distinct trace ID from propagated context"); + assertEquals(200000001L, firstServerSpan.getParentId()); + assertEquals(200000002L, secondServerSpan.getParentId()); + + // Both should still have correct route enrichment + assertEquals("GET /hello", firstServerSpan.getResourceName().toString()); + assertEquals("GET /hello", secondServerSpan.getResourceName().toString()); + } + + // --------------------------------------------------------------- + // Helper methods + // --------------------------------------------------------------- + + /** + * Waits for at least one trace to be written, then finds and returns the server span. This + * combines the common pattern of waiting + flattening + finding into a single call, reducing + * boilerplate in test methods. + * + * @return the server span (never null — fails assertion if not found) + * @throws InterruptedException if the thread is interrupted while waiting + * @throws TimeoutException if no trace is written within the timeout + */ + private DDSpan waitForServerSpan() throws InterruptedException, TimeoutException { + writer.waitForTraces(1); + List spans = flattenTraces(); + DDSpan serverSpan = findServerSpan(spans); + assertNotNull(serverSpan, "Expected to find a server span in the collected traces"); + return serverSpan; + } + + /** + * Flattens all collected traces into a single list of spans for easier assertion. + * + * @return all spans from all collected traces + */ + private List flattenTraces() { + List result = new ArrayList<>(); + for (List trace : writer) { + result.addAll(trace); + } + return result; + } + + /** + * Finds the server span in the list of spans. The server span is identified by having {@code + * span.kind=server} or by having a {@code web} span type. + * + * @param spans the list of spans to search + * @return the server span, or {@code null} if not found + */ + private DDSpan findServerSpan(List spans) { + for (DDSpan span : spans) { + if ("server".equals(String.valueOf(span.getTag("span.kind"))) + || "web".equals(span.getSpanType())) { + return span; + } + } + return null; + } + + /** + * Validates the complete structure of a server span, covering both SparkJava enrichment and the + * underlying Jetty server span baseline. This single-point-of-assertion prevents regressions when + * new required tags are added. + * + *

SparkJava enrichment (set by {@link RoutesInstrumentation}): + * + *

    + *
  • operation name = {@code spark.request} + *
  • component = {@code spark-java} + *
  • resource name = {@code HTTP_METHOD route_pattern} + *
  • http.route = parameterized route pattern + *
+ * + *

Jetty baseline (set by the Jetty server instrumentation): + * + *

    + *
  • span type = {@code web} + *
  • span.kind = {@code server} + *
  • http.method, http.status_code, http.url + *
  • error flag (from HTTP status code) + *
+ * + * @param span the server span to validate + * @param httpMethod the expected HTTP method (e.g., "GET", "POST") + * @param route the expected route pattern (e.g., "/hello/:name") + * @param statusCode the expected HTTP status code + * @param isError whether the span should be marked as errored + */ + private void assertServerSpan( + DDSpan span, String httpMethod, String route, int statusCode, boolean isError) { + assertNotNull(span, "Expected a server span for " + httpMethod + " " + route); + + // SparkJava enrichment assertions + assertEquals( + "spark.request", + span.getOperationName().toString(), + "Operation name should be 'spark.request'"); + assertEquals( + "spark-java", + String.valueOf(span.getTag("component")), + "component tag should be 'spark-java'"); + assertEquals( + httpMethod + " " + route, + span.getResourceName().toString(), + "Resource name should be HTTP_METHOD + route_pattern"); + assertEquals( + route, + String.valueOf(span.getTag("http.route")), + "http.route should contain the route pattern, not the actual path"); + + // Jetty baseline assertions + assertEquals("web", span.getSpanType(), "Span type should be 'web'"); + assertEquals( + "server", String.valueOf(span.getTag("span.kind")), "span.kind should be 'server'"); + assertEquals(httpMethod, String.valueOf(span.getTag("http.method")), "http.method tag"); + assertEquals(statusCode, span.getTag("http.status_code"), "http.status_code tag"); + assertNotNull(span.getTag("http.url"), "http.url tag should be set"); + assertEquals(isError, span.isError(), "error flag"); + } + + /** + * Makes an HTTP GET request to the SparkJava server with custom headers. Used for context + * propagation tests to inject Datadog trace headers (e.g., {@code x-datadog-trace-id}, {@code + * x-datadog-parent-id}) that simulate an upstream service propagating its trace context. + * + * @param path the request path (e.g., {@code /hello}) + * @param headers map of header name to value to set on the request + * @return the HTTP status code + */ + private int httpGetWithHeaders(String path, Map headers) { + try { + URL url = new URL("http://localhost:" + actualPort + path); + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + conn.setRequestMethod("GET"); + conn.setConnectTimeout(5000); + conn.setReadTimeout(5000); + if (headers != null) { + for (Map.Entry entry : headers.entrySet()) { + conn.setRequestProperty(entry.getKey(), entry.getValue()); + } + } + int status = conn.getResponseCode(); + drainResponse(conn); + conn.disconnect(); + return status; + } catch (Exception e) { + throw new RuntimeException("HTTP GET failed for path " + path, e); + } + } + + /** + * Makes an HTTP GET request to the SparkJava server. + * + * @param path the request path (e.g., {@code /hello}) + * @return the HTTP status code + */ + private int httpGet(String path) { + try { + URL url = new URL("http://localhost:" + actualPort + path); + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + conn.setRequestMethod("GET"); + conn.setConnectTimeout(5000); + conn.setReadTimeout(5000); + int status = conn.getResponseCode(); + drainResponse(conn); + conn.disconnect(); + return status; + } catch (Exception e) { + throw new RuntimeException("HTTP GET failed for path " + path, e); + } + } + + /** + * Makes an HTTP request with the specified method and optional body. + * + * @param path the request path + * @param method the HTTP method (e.g., POST, PUT, DELETE) + * @param body the request body, or {@code null} for no body + * @return the HTTP status code + */ + private int httpRequest(String path, String method, String body) { + try { + URL url = new URL("http://localhost:" + actualPort + path); + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + conn.setRequestMethod(method); + conn.setConnectTimeout(5000); + conn.setReadTimeout(5000); + + if (body != null) { + conn.setDoOutput(true); + conn.setRequestProperty("Content-Type", "text/plain"); + try (OutputStream os = conn.getOutputStream()) { + os.write(body.getBytes("UTF-8")); + } + } + + int status = conn.getResponseCode(); + drainResponse(conn); + conn.disconnect(); + return status; + } catch (Exception e) { + throw new RuntimeException("HTTP " + method + " failed for path " + path, e); + } + } + + /** + * Drains the response body to ensure the server-side processing completes fully before the + * connection is closed. + * + * @param conn the HTTP connection to drain + */ + private void drainResponse(HttpURLConnection conn) { + try { + InputStream is = + conn.getResponseCode() >= 400 ? conn.getErrorStream() : conn.getInputStream(); + if (is != null) { + BufferedReader reader = new BufferedReader(new InputStreamReader(is)); + while (reader.readLine() != null) { + // drain + } + reader.close(); + } + } catch (Exception ignored) { + // ignore drain errors + } + } +} diff --git a/metadata/supported-configurations.json b/metadata/supported-configurations.json index 09498b2beda..e88ddb47f09 100644 --- a/metadata/supported-configurations.json +++ b/metadata/supported-configurations.json @@ -10129,6 +10129,14 @@ "aliases": [] } ], + "DD_TRACE_SPARKJAVA_2_3_ENABLED": [ + { + "version": "A", + "type": "boolean", + "default": "false", + "aliases": ["DD_TRACE_INTEGRATION_SPARKJAVA_2_3_ENABLED", "DD_INTEGRATION_SPARKJAVA_2_3_ENABLED"] + } + ], "DD_TRACE_SPARKJAVA_2_4_ENABLED": [ { "version": "A", @@ -10137,6 +10145,22 @@ "aliases": ["DD_TRACE_INTEGRATION_SPARKJAVA_2_4_ENABLED", "DD_INTEGRATION_SPARKJAVA_2_4_ENABLED"] } ], + "DD_TRACE_SPARKJAVA_ANALYTICS_ENABLED": [ + { + "version": "A", + "type": "boolean", + "default": "false", + "aliases": ["DD_SPARKJAVA_ANALYTICS_ENABLED"] + } + ], + "DD_TRACE_SPARKJAVA_ANALYTICS_SAMPLE_RATE": [ + { + "version": "A", + "type": "decimal", + "default": "1.0", + "aliases": ["DD_SPARKJAVA_ANALYTICS_SAMPLE_RATE"] + } + ], "DD_TRACE_SPARKJAVA_ENABLED": [ { "version": "A",