From d192f4791916a6d1c9132f7beb82e86c58956350 Mon Sep 17 00:00:00 2001 From: Jordan Wong Date: Tue, 23 Jun 2026 08:11:20 -0400 Subject: [PATCH 1/6] feat(sparkjava-2.3): toolkit-generated sparkjava-2.3 instrumentation [DO NOT MERGE] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rebased onto current master to fix the stale-base CI failures on PR #11562. This branch contains only the toolkit-generated sparkjava-2.3 work: - SparkJavaDecorator, RoutesInstrumentation (with Routes.find advice) - SparkJavaTest, SparkJavaForkedTest (Java tests per R20) - build.gradle updated for the generated module structure - Old groovy test + TestSparkJavaApplication + stale gradle.lockfile removed (toolkit produces Java tests only) Supersedes #11562. The supported-configurations.json fix from #11562 is no longer needed — master already has the SPARKJAVA env var entries. Co-Authored-By: Claude Sonnet 4.6 --- .../spark/sparkjava-2.3/build.gradle | 5 +- .../spark/sparkjava-2.3/gradle.lockfile | 168 ----- .../sparkjava/RoutesInstrumentation.java | 11 +- .../sparkjava/SparkJavaDecorator.java | 31 + .../src/test/groovy/SparkJavaBasedTest.groovy | 73 -- .../test/java/TestSparkJavaApplication.java | 19 - .../sparkjava/SparkJavaForkedTest.java | 226 ++++++ .../sparkjava/SparkJavaTest.java | 687 ++++++++++++++++++ 8 files changed, 956 insertions(+), 264 deletions(-) delete mode 100644 dd-java-agent/instrumentation/spark/sparkjava-2.3/gradle.lockfile create mode 100644 dd-java-agent/instrumentation/spark/sparkjava-2.3/src/main/java/datadog/trace/instrumentation/sparkjava/SparkJavaDecorator.java delete mode 100644 dd-java-agent/instrumentation/spark/sparkjava-2.3/src/test/groovy/SparkJavaBasedTest.groovy delete mode 100644 dd-java-agent/instrumentation/spark/sparkjava-2.3/src/test/java/TestSparkJavaApplication.java create mode 100644 dd-java-agent/instrumentation/spark/sparkjava-2.3/src/test/java/datadog/trace/instrumentation/sparkjava/SparkJavaForkedTest.java create mode 100644 dd-java-agent/instrumentation/spark/sparkjava-2.3/src/test/java/datadog/trace/instrumentation/sparkjava/SparkJavaTest.java 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: + * + *

    + *
  • A server span is created for each HTTP request handled by a SparkJava route + *
  • The operation name is set to {@code spark.request} + *
  • The span type is {@code web} and span.kind is {@code server} + *
  • The component tag is set to {@code spark-java} + *
  • The resource name is enriched to {@code HTTP_METHOD route_pattern} (e.g., {@code GET + * /hello/:name}) + *
  • The http.route tag contains the parameterized route pattern, not the concrete path + *
  • HTTP tags (method, URL, status code) are set correctly + *
  • Error routes (500) set the error flag on the span + *
  • Unmatched routes (404) retain Jetty defaults — no SparkJava enrichment fires + *
  • Context propagation via Datadog headers links server spans to parent traces + *
+ */ +@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 + } + } +} From 53836a076ff33576fefcb4d53813947b780980fb Mon Sep 17 00:00:00 2001 From: Jordan Wong Date: Tue, 23 Jun 2026 08:33:05 -0400 Subject: [PATCH 2/6] fix(sparkjava-2.3): add missing supported-configurations.json entries Adds the three integration config entries needed for the new sparkjava-2.3 module: - DD_TRACE_SPARKJAVA_2_3_ENABLED (for super("sparkjava-2.3") in RoutesInstrumentation) - DD_TRACE_SPARKJAVA_ANALYTICS_ENABLED (for decorator instrumentationNames() -> "sparkjava") - DD_TRACE_SPARKJAVA_ANALYTICS_SAMPLE_RATE (same) Fixes the config-inversion-linter CI job on this PR. Co-Authored-By: Claude Sonnet 4.6 --- metadata/supported-configurations.json | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/metadata/supported-configurations.json b/metadata/supported-configurations.json index 09498b2beda..448c62fd2f1 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": "double", + "default": "1.0", + "aliases": ["DD_SPARKJAVA_ANALYTICS_SAMPLE_RATE"] + } + ], "DD_TRACE_SPARKJAVA_ENABLED": [ { "version": "A", From 48a84c0576759cbf935fb0ec33a6c8058e62517e Mon Sep 17 00:00:00 2001 From: Jordan Wong Date: Tue, 23 Jun 2026 09:28:43 -0400 Subject: [PATCH 3/6] fix(sparkjava-2.3): use 'decimal' type for ANALYTICS_SAMPLE_RATE (not 'double') dd-trace-java's supported-configurations.json schema uses 'decimal' for double-precision floats; 'double' fails the validate_supported_configurations_v2 GitLab CI check. Matches existing AKKA_HTTP_ANALYTICS_SAMPLE_RATE and other analytics-rate entries. Co-Authored-By: Claude Sonnet 4.6 --- metadata/supported-configurations.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/metadata/supported-configurations.json b/metadata/supported-configurations.json index 448c62fd2f1..e88ddb47f09 100644 --- a/metadata/supported-configurations.json +++ b/metadata/supported-configurations.json @@ -10156,7 +10156,7 @@ "DD_TRACE_SPARKJAVA_ANALYTICS_SAMPLE_RATE": [ { "version": "A", - "type": "double", + "type": "decimal", "default": "1.0", "aliases": ["DD_SPARKJAVA_ANALYTICS_SAMPLE_RATE"] } From aa7744fdd6292b6d3444bed4794a804489ff3dab Mon Sep 17 00:00:00 2001 From: Jordan Wong Date: Tue, 23 Jun 2026 23:23:11 -0400 Subject: [PATCH 4/6] fix(sparkjava-2.3): align with master conventions to unblock v2_local_file validator Three coordinated changes: 1. super('sparkjava', 'sparkjava-2.4') in RoutesInstrumentation (was 'sparkjava-2.3'). Matches master's existing module exactly. Master's DD_TRACE_SPARKJAVA_2_4_* entries already exist in the central feature-parity registry, so no new registry registration is needed. 2. Remove SparkJavaDecorator.java entirely. Its only function was providing a 'sparkjava' instrumentationName + a SPARK_JAVA component constant + a SPARK_REQUEST spanName constant. Master doesn't have a sparkjava-specific decorator; it just calls HTTP_RESOURCE_DECORATOR.withRoute(...). Moved the SPARK_JAVA and SPARK_REQUEST constants inline into RoutesAdvice. The decorator's instrumentationNames() was triggering a checkDecoratorAnalyticsConfigurations requirement for DD_TRACE_SPARKJAVA_ANALYTICS_* entries that don't exist in the registry. 3. Remove DD_TRACE_SPARKJAVA_2_3_ENABLED, DD_TRACE_SPARKJAVA_ANALYTICS_ENABLED, DD_TRACE_SPARKJAVA_ANALYTICS_SAMPLE_RATE from metadata/supported-configurations.json. Master's existing DD_TRACE_SPARKJAVA_2_4_ENABLED + DD_TRACE_SPARKJAVA_ENABLED cover the integration names this PR now uses. Local verification BUILD SUCCESSFUL: - ./gradlew checkConfigurations - ./gradlew :dd-java-agent:instrumentation:spark:sparkjava-2.3:compileTestJava - ./gradlew spotlessCheck This should clear validate_supported_configurations_v2_local_file CI failure. Research finding: the toolkit's extra structure (decorator + analytics requirement) creates a registry-registration dependency that doesn't exist on master. When master's existing module uses HTTP_RESOURCE_DECORATOR directly, regenerated modules should too. Documented in eval-research/hypotheses/sparkjava.md. Co-Authored-By: Claude Sonnet 4.6 --- .../sparkjava/RoutesInstrumentation.java | 15 ++++----- .../sparkjava/SparkJavaDecorator.java | 31 ------------------- metadata/supported-configurations.json | 24 -------------- 3 files changed, 6 insertions(+), 64 deletions(-) delete mode 100644 dd-java-agent/instrumentation/spark/sparkjava-2.3/src/main/java/datadog/trace/instrumentation/sparkjava/SparkJavaDecorator.java 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 3534719bea7..828cbfe95ef 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,8 +3,6 @@ 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; @@ -14,6 +12,7 @@ import datadog.trace.agent.tooling.InstrumenterModule; import datadog.trace.bootstrap.instrumentation.api.AgentSpan; import datadog.trace.bootstrap.instrumentation.api.Tags; +import datadog.trace.bootstrap.instrumentation.api.UTF8BytesString; import net.bytebuddy.asm.Advice; import spark.route.HttpMethod; import spark.routematch.RouteMatch; @@ -23,12 +22,7 @@ public class RoutesInstrumentation extends InstrumenterModule.Tracing implements Instrumenter.ForSingleType, Instrumenter.HasMethodAdvice { public RoutesInstrumentation() { - super("sparkjava", "sparkjava-2.3"); - } - - @Override - public String[] helperClassNames() { - return new String[] {packageName + ".SparkJavaDecorator"}; + super("sparkjava", "sparkjava-2.4"); } @Override @@ -48,6 +42,9 @@ public void methodAdvice(MethodTransformer transformer) { public static class RoutesAdvice { + private static final CharSequence SPARK_JAVA = UTF8BytesString.create("spark-java"); + private static final CharSequence SPARK_REQUEST = UTF8BytesString.create("spark.request"); + @Advice.OnMethodExit(suppress = Throwable.class) public static void routeMatchEnricher( @Advice.Argument(0) final HttpMethod method, @Advice.Return final RouteMatch routeMatch) { @@ -55,7 +52,7 @@ 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.setSpanName(SPARK_REQUEST); 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 deleted file mode 100644 index 4657f85ccad..00000000000 --- a/dd-java-agent/instrumentation/spark/sparkjava-2.3/src/main/java/datadog/trace/instrumentation/sparkjava/SparkJavaDecorator.java +++ /dev/null @@ -1,31 +0,0 @@ -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/metadata/supported-configurations.json b/metadata/supported-configurations.json index e88ddb47f09..09498b2beda 100644 --- a/metadata/supported-configurations.json +++ b/metadata/supported-configurations.json @@ -10129,14 +10129,6 @@ "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", @@ -10145,22 +10137,6 @@ "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", From f6d1263e73d51195864f2590c81e0e9fa50aca2c Mon Sep 17 00:00:00 2001 From: Jordan Wong Date: Tue, 23 Jun 2026 23:45:18 -0400 Subject: [PATCH 5/6] fix(sparkjava-2.3): remove static fields from RoutesAdvice to satisfy muzzle MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Muzzle failed AssertPass for spark-core 2.3, 2.6.0, 2.7.2, 2.9.4 with: FAILED MUZZLE VALIDATION: datadog.trace.instrumentation.sparkjava.RoutesInstrumentation mismatches: Missing class datadog.trace.instrumentation.sparkjava.RoutesInstrumentation$RoutesAdvice ByteBuddy advice classes get inlined into the target library's classes at runtime. Static fields referencing the agent's UTF8BytesString class cause muzzle to flag the advice as 'Missing class' from the target library's classpath perspective. Removed two static fields (SPARK_JAVA, SPARK_REQUEST) from RoutesAdvice and the two enrichment statements that used them (span.setSpanName, span.setTag). The instrumentation now matches master's RoutesAdvice exactly — only HTTP_RESOURCE_DECORATOR.withRoute(...) is called. Local verification BUILD SUCCESSFUL: ./gradlew :dd-java-agent:instrumentation:spark:sparkjava-2.3:muzzle Research finding: toolkit-generated advice classes that add static field constants violate muzzle constraints. Encode as R-rule: 'Advice classes must not declare static or instance fields; constants must live on a helper class referenced via helperClassNames()'. Co-Authored-By: Claude Sonnet 4.6 --- .../instrumentation/sparkjava/RoutesInstrumentation.java | 7 ------- 1 file changed, 7 deletions(-) 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 828cbfe95ef..563bf6af957 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 @@ -11,8 +11,6 @@ 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 datadog.trace.bootstrap.instrumentation.api.UTF8BytesString; import net.bytebuddy.asm.Advice; import spark.route.HttpMethod; import spark.routematch.RouteMatch; @@ -42,9 +40,6 @@ public void methodAdvice(MethodTransformer transformer) { public static class RoutesAdvice { - private static final CharSequence SPARK_JAVA = UTF8BytesString.create("spark-java"); - private static final CharSequence SPARK_REQUEST = UTF8BytesString.create("spark.request"); - @Advice.OnMethodExit(suppress = Throwable.class) public static void routeMatchEnricher( @Advice.Argument(0) final HttpMethod method, @Advice.Return final RouteMatch routeMatch) { @@ -52,8 +47,6 @@ public static void routeMatchEnricher( final AgentSpan span = activeSpan(); if (span != null && routeMatch != null) { HTTP_RESOURCE_DECORATOR.withRoute(span, method.name(), routeMatch.getMatchUri()); - span.setSpanName(SPARK_REQUEST); - span.setTag(Tags.COMPONENT, SPARK_JAVA); } } } From 2c372a3c6da5f296ba7ffaa78f852db31c1624b1 Mon Sep 17 00:00:00 2001 From: Jordan Wong Date: Wed, 24 Jun 2026 00:41:41 -0400 Subject: [PATCH 6/6] fix(sparkjava-2.3): restore enrichment via helperClassNames + SparkJavaNaming helper Previous commit removed SPARK_JAVA/SPARK_REQUEST constants entirely to satisfy muzzle, but broke 68 tests (SparkJavaTest + SparkJavaForkedTest) that assert on the enrichment tags (span name 'spark.request', component 'spark-java'). Correct fix: put constants on a separate helper class registered via helperClassNames(). Muzzle understands helper classes as part of the agent's classpath (not the target library's), so static fields on them are fine. Created SparkJavaNaming.java with public static final CharSequence SPARK_JAVA and SPARK_REQUEST. Restored the two enrichment statements in RoutesAdvice using SparkJavaNaming.SPARK_REQUEST and SparkJavaNaming.SPARK_JAVA. Added helperClassNames() override in RoutesInstrumentation pointing to SparkJavaNaming. Local verification BUILD SUCCESSFUL: muzzle, spotbugsMain, checkConfigurations, spotlessCheck. Research finding: toolkit-generated Advice classes that need constants from agent classpath should put them on a separate helper class registered via helperClassNames(), NOT as inline static fields and NOT on a BaseDecorator subclass (which triggers checkDecoratorAnalyticsConfigurations). A minimal naming-only helper avoids both pitfalls. Co-Authored-By: Claude Sonnet 4.6 --- .../sparkjava/RoutesInstrumentation.java | 8 ++++++++ .../instrumentation/sparkjava/SparkJavaNaming.java | 11 +++++++++++ 2 files changed, 19 insertions(+) create mode 100644 dd-java-agent/instrumentation/spark/sparkjava-2.3/src/main/java/datadog/trace/instrumentation/sparkjava/SparkJavaNaming.java 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 563bf6af957..747106fecec 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 @@ -11,6 +11,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; @@ -23,6 +24,11 @@ public RoutesInstrumentation() { super("sparkjava", "sparkjava-2.4"); } + @Override + public String[] helperClassNames() { + return new String[] {packageName + ".SparkJavaNaming"}; + } + @Override public String instrumentedType() { return "spark.route.Routes"; @@ -47,6 +53,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(SparkJavaNaming.SPARK_REQUEST); + span.setTag(Tags.COMPONENT, SparkJavaNaming.SPARK_JAVA); } } } diff --git a/dd-java-agent/instrumentation/spark/sparkjava-2.3/src/main/java/datadog/trace/instrumentation/sparkjava/SparkJavaNaming.java b/dd-java-agent/instrumentation/spark/sparkjava-2.3/src/main/java/datadog/trace/instrumentation/sparkjava/SparkJavaNaming.java new file mode 100644 index 00000000000..878d4b6e5bd --- /dev/null +++ b/dd-java-agent/instrumentation/spark/sparkjava-2.3/src/main/java/datadog/trace/instrumentation/sparkjava/SparkJavaNaming.java @@ -0,0 +1,11 @@ +package datadog.trace.instrumentation.sparkjava; + +import datadog.trace.bootstrap.instrumentation.api.UTF8BytesString; + +public final class SparkJavaNaming { + + public static final CharSequence SPARK_JAVA = UTF8BytesString.create("spark-java"); + public static final CharSequence SPARK_REQUEST = UTF8BytesString.create("spark.request"); + + private SparkJavaNaming() {} +}