From 45057c58bf73f60f5200ea73b85886bb72bc3772 Mon Sep 17 00:00:00 2001 From: Adrien Boitreaud Date: Wed, 18 Feb 2026 20:05:34 +0100 Subject: [PATCH 1/6] initial spark launcher instrumentation --- .../spark/spark-common/build.gradle | 2 + .../spark/SparkExitAdvice.java | 8 + .../spark/SparkLauncherAdvice.java | 144 ++++++++++++++++++ .../spark/SparkLauncherInstrumentation.java | 52 +++++++ .../spark/AbstractSparkTest.groovy | 67 ++++++++ 5 files changed, 273 insertions(+) create mode 100644 dd-java-agent/instrumentation/spark/spark-common/src/main/java/datadog/trace/instrumentation/spark/SparkLauncherAdvice.java create mode 100644 dd-java-agent/instrumentation/spark/spark-common/src/main/java/datadog/trace/instrumentation/spark/SparkLauncherInstrumentation.java diff --git a/dd-java-agent/instrumentation/spark/spark-common/build.gradle b/dd-java-agent/instrumentation/spark/spark-common/build.gradle index 84dd5cca6a5..724bfec3f67 100644 --- a/dd-java-agent/instrumentation/spark/spark-common/build.gradle +++ b/dd-java-agent/instrumentation/spark/spark-common/build.gradle @@ -10,6 +10,7 @@ configurations.configureEach { dependencies { compileOnly group: 'org.apache.spark', name: 'spark-core_2.12', version: '2.4.0' compileOnly group: 'org.apache.spark', name: 'spark-sql_2.12', version: '2.4.0' + compileOnly group: 'org.apache.spark', name: 'spark-launcher_2.12', version: '2.4.0' testFixturesImplementation group: 'com.datadoghq', name: 'sketches-java', version: '0.8.2' testFixturesImplementation group: 'com.google.protobuf', name: 'protobuf-java', version: '3.14.0' @@ -21,6 +22,7 @@ dependencies { testFixturesCompileOnly group: 'org.apache.spark', name: 'spark-core_2.12', version: '2.4.0' testFixturesCompileOnly group: 'org.apache.spark', name: 'spark-sql_2.12', version: '2.4.0' testFixturesCompileOnly group: 'org.apache.spark', name: 'spark-yarn_2.12', version: '2.4.0' + testFixturesCompileOnly group: 'org.apache.spark', name: 'spark-launcher_2.12', version: '2.4.0' testFixturesCompileOnly(libs.bundles.groovy) testFixturesCompileOnly(libs.bundles.spock) diff --git a/dd-java-agent/instrumentation/spark/spark-common/src/main/java/datadog/trace/instrumentation/spark/SparkExitAdvice.java b/dd-java-agent/instrumentation/spark/spark-common/src/main/java/datadog/trace/instrumentation/spark/SparkExitAdvice.java index 1306da2b373..6a26391af1c 100644 --- a/dd-java-agent/instrumentation/spark/spark-common/src/main/java/datadog/trace/instrumentation/spark/SparkExitAdvice.java +++ b/dd-java-agent/instrumentation/spark/spark-common/src/main/java/datadog/trace/instrumentation/spark/SparkExitAdvice.java @@ -21,6 +21,14 @@ public static void enter(@Advice.Argument(0) int exitCode) { .getMethod( "finishApplication", long.class, Throwable.class, int.class, String.class); method.invoke(datadogListener, System.currentTimeMillis(), null, exitCode, null); + } else { + // No Spark listener means we may be in a launcher-only process (e.g. SparkLauncher on EMR) + Class adviceClass = + Thread.currentThread() + .getContextClassLoader() + .loadClass("datadog.trace.instrumentation.spark.SparkLauncherAdvice"); + Method finishMethod = adviceClass.getDeclaredMethod("finishLauncherSpan", int.class); + finishMethod.invoke(null, exitCode); } } catch (Exception ignored) { } diff --git a/dd-java-agent/instrumentation/spark/spark-common/src/main/java/datadog/trace/instrumentation/spark/SparkLauncherAdvice.java b/dd-java-agent/instrumentation/spark/spark-common/src/main/java/datadog/trace/instrumentation/spark/SparkLauncherAdvice.java new file mode 100644 index 00000000000..e245a4a16c4 --- /dev/null +++ b/dd-java-agent/instrumentation/spark/spark-common/src/main/java/datadog/trace/instrumentation/spark/SparkLauncherAdvice.java @@ -0,0 +1,144 @@ +package datadog.trace.instrumentation.spark; + +import datadog.trace.bootstrap.instrumentation.api.AgentSpan; +import datadog.trace.bootstrap.instrumentation.api.AgentTracer; +import net.bytebuddy.asm.Advice; +import org.apache.spark.launcher.SparkAppHandle; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +class SparkLauncherAdvice { + + private static final Logger log = LoggerFactory.getLogger(SparkLauncherAdvice.class); + + /** The launcher span, accessible from SparkExitAdvice via reflection. */ + static volatile AgentSpan launcherSpan; + + private static volatile boolean shutdownHookRegistered = false; + + private static synchronized void createLauncherSpan(String resource) { + if (launcherSpan != null) { + return; + } + + AgentTracer.TracerAPI tracer = AgentTracer.get(); + AgentSpan span = + tracer + .buildSpan("spark.launcher") + .withSpanType("spark") + .withResourceName(resource) + .start(); + span.setSamplingPriority( + datadog.trace.api.sampling.PrioritySampling.USER_KEEP, + datadog.trace.api.sampling.SamplingMechanism.DATA_JOBS); + launcherSpan = span; + + if (!shutdownHookRegistered) { + shutdownHookRegistered = true; + Runtime.getRuntime() + .addShutdownHook( + new Thread( + () -> { + AgentSpan s = launcherSpan; + if (s != null) { + log.info("Finishing spark.launcher span from shutdown hook"); + s.finish(); + launcherSpan = null; + } + })); + } + } + + static synchronized void finishLauncherSpan(int exitCode) { + AgentSpan span = launcherSpan; + if (span == null) { + return; + } + if (exitCode != 0) { + span.setError(true); + span.setTag("error.type", "Launcher process failed with exit code " + exitCode); + } + span.finish(); + launcherSpan = null; + } + + public static class StartApplicationAdvice { + @Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class) + public static void exit( + @Advice.Return SparkAppHandle handle, @Advice.Thrown Throwable throwable) { + createLauncherSpan("SparkLauncher.startApplication"); + + if (throwable != null) { + AgentSpan span = launcherSpan; + if (span != null) { + span.setError(true); + span.addThrowable(throwable); + span.finish(); + launcherSpan = null; + } + return; + } + + if (handle != null) { + try { + handle.addListener(new AppHandleListener()); + } catch (Exception e) { + log.debug("Failed to register SparkAppHandle listener", e); + } + } + } + } + + public static class LaunchAdvice { + @Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class) + public static void exit(@Advice.Thrown Throwable throwable) { + createLauncherSpan("SparkLauncher.launch"); + + if (throwable != null) { + AgentSpan span = launcherSpan; + if (span != null) { + span.setError(true); + span.addThrowable(throwable); + span.finish(); + launcherSpan = null; + } + } + } + } + + static class AppHandleListener implements SparkAppHandle.Listener { + @Override + public void stateChanged(SparkAppHandle handle) { + SparkAppHandle.State state = handle.getState(); + AgentSpan span = launcherSpan; + if (span != null) { + span.setTag("spark.launcher.app_state", state.toString()); + + String appId = handle.getAppId(); + if (appId != null) { + span.setTag("spark.app_id", appId); + } + + if (state.isFinal()) { + if (state == SparkAppHandle.State.FAILED + || state == SparkAppHandle.State.KILLED + || state == SparkAppHandle.State.LOST) { + span.setError(true); + span.setTag("error.type", "Spark application " + state); + } + } + } + } + + @Override + public void infoChanged(SparkAppHandle handle) { + AgentSpan span = launcherSpan; + if (span != null) { + String appId = handle.getAppId(); + if (appId != null) { + span.setTag("spark.app_id", appId); + } + } + } + } +} diff --git a/dd-java-agent/instrumentation/spark/spark-common/src/main/java/datadog/trace/instrumentation/spark/SparkLauncherInstrumentation.java b/dd-java-agent/instrumentation/spark/spark-common/src/main/java/datadog/trace/instrumentation/spark/SparkLauncherInstrumentation.java new file mode 100644 index 00000000000..669643912ea --- /dev/null +++ b/dd-java-agent/instrumentation/spark/spark-common/src/main/java/datadog/trace/instrumentation/spark/SparkLauncherInstrumentation.java @@ -0,0 +1,52 @@ +package datadog.trace.instrumentation.spark; + +import static datadog.trace.agent.tooling.bytebuddy.matcher.NameMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.isDeclaredBy; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; + +import com.google.auto.service.AutoService; +import datadog.trace.agent.tooling.Instrumenter; +import datadog.trace.agent.tooling.InstrumenterModule; +import datadog.trace.api.InstrumenterConfig; + +@AutoService(InstrumenterModule.class) +public class SparkLauncherInstrumentation extends InstrumenterModule.Tracing + implements Instrumenter.ForSingleType, Instrumenter.HasMethodAdvice { + + public SparkLauncherInstrumentation() { + super("spark-launcher"); + } + + @Override + protected boolean defaultEnabled() { + return InstrumenterConfig.get().isDataJobsEnabled(); + } + + @Override + public String instrumentedType() { + return "org.apache.spark.launcher.SparkLauncher"; + } + + @Override + public String[] helperClassNames() { + return new String[] { + packageName + ".SparkLauncherAdvice", + packageName + ".SparkLauncherAdvice$AppHandleListener", + }; + } + + @Override + public void methodAdvice(MethodTransformer transformer) { + transformer.applyAdvice( + isMethod() + .and(named("startApplication")) + .and(isDeclaredBy(named("org.apache.spark.launcher.SparkLauncher"))), + packageName + ".SparkLauncherAdvice$StartApplicationAdvice"); + + transformer.applyAdvice( + isMethod() + .and(named("launch")) + .and(isDeclaredBy(named("org.apache.spark.launcher.SparkLauncher"))), + packageName + ".SparkLauncherAdvice$LaunchAdvice"); + } +} diff --git a/dd-java-agent/instrumentation/spark/spark-common/src/testFixtures/groovy/datadog/trace/instrumentation/spark/AbstractSparkTest.groovy b/dd-java-agent/instrumentation/spark/spark-common/src/testFixtures/groovy/datadog/trace/instrumentation/spark/AbstractSparkTest.groovy index c39f42f473c..5280d43e49c 100644 --- a/dd-java-agent/instrumentation/spark/spark-common/src/testFixtures/groovy/datadog/trace/instrumentation/spark/AbstractSparkTest.groovy +++ b/dd-java-agent/instrumentation/spark/spark-common/src/testFixtures/groovy/datadog/trace/instrumentation/spark/AbstractSparkTest.groovy @@ -780,4 +780,71 @@ abstract class AbstractSparkTest extends InstrumentationSpecification { } } } + + def "generate spark.launcher span from startApplication"() { + setup: + // Reset any previous launcher span + SparkLauncherAdvice.launcherSpan = null + + when: + // Directly invoke the advice to simulate what would happen when startApplication is called + def tracer = datadog.trace.bootstrap.instrumentation.api.AgentTracer.get() + def span = tracer + .buildSpan("spark.launcher") + .withSpanType("spark") + .withResourceName("SparkLauncher.startApplication") + .start() + span.setSamplingPriority( + datadog.trace.api.sampling.PrioritySampling.USER_KEEP, + datadog.trace.api.sampling.SamplingMechanism.DATA_JOBS) + SparkLauncherAdvice.launcherSpan = span + + // Simulate a non-zero exit finishing the launcher span + SparkLauncherAdvice.finishLauncherSpan(1) + + then: + assertTraces(1) { + trace(1) { + span { + operationName "spark.launcher" + spanType "spark" + resourceName "SparkLauncher.startApplication" + errored true + assert span.tags["error.type"] == "Launcher process failed with exit code 1" + } + } + } + } + + def "generate spark.launcher span with successful exit"() { + setup: + SparkLauncherAdvice.launcherSpan = null + + when: + def tracer = datadog.trace.bootstrap.instrumentation.api.AgentTracer.get() + def span = tracer + .buildSpan("spark.launcher") + .withSpanType("spark") + .withResourceName("SparkLauncher.launch") + .start() + span.setSamplingPriority( + datadog.trace.api.sampling.PrioritySampling.USER_KEEP, + datadog.trace.api.sampling.SamplingMechanism.DATA_JOBS) + SparkLauncherAdvice.launcherSpan = span + + // Simulate a successful exit + SparkLauncherAdvice.finishLauncherSpan(0) + + then: + assertTraces(1) { + trace(1) { + span { + operationName "spark.launcher" + spanType "spark" + resourceName "SparkLauncher.launch" + errored false + } + } + } + } } From ae18996e4a42992131d87d49abc80041db40d228 Mon Sep 17 00:00:00 2001 From: Adrien Boitreaud Date: Wed, 18 Feb 2026 20:21:02 +0100 Subject: [PATCH 2/6] use ddtags --- .../spark/SparkLauncherAdvice.java | 19 +++++++------------ .../spark/SparkLauncherInstrumentation.java | 3 +-- .../spark/AbstractSparkTest.groovy | 2 +- 3 files changed, 9 insertions(+), 15 deletions(-) diff --git a/dd-java-agent/instrumentation/spark/spark-common/src/main/java/datadog/trace/instrumentation/spark/SparkLauncherAdvice.java b/dd-java-agent/instrumentation/spark/spark-common/src/main/java/datadog/trace/instrumentation/spark/SparkLauncherAdvice.java index e245a4a16c4..cfd14c65a81 100644 --- a/dd-java-agent/instrumentation/spark/spark-common/src/main/java/datadog/trace/instrumentation/spark/SparkLauncherAdvice.java +++ b/dd-java-agent/instrumentation/spark/spark-common/src/main/java/datadog/trace/instrumentation/spark/SparkLauncherAdvice.java @@ -1,5 +1,8 @@ package datadog.trace.instrumentation.spark; +import datadog.trace.api.DDTags; +import datadog.trace.api.sampling.PrioritySampling; +import datadog.trace.api.sampling.SamplingMechanism; import datadog.trace.bootstrap.instrumentation.api.AgentSpan; import datadog.trace.bootstrap.instrumentation.api.AgentTracer; import net.bytebuddy.asm.Advice; @@ -23,14 +26,8 @@ private static synchronized void createLauncherSpan(String resource) { AgentTracer.TracerAPI tracer = AgentTracer.get(); AgentSpan span = - tracer - .buildSpan("spark.launcher") - .withSpanType("spark") - .withResourceName(resource) - .start(); - span.setSamplingPriority( - datadog.trace.api.sampling.PrioritySampling.USER_KEEP, - datadog.trace.api.sampling.SamplingMechanism.DATA_JOBS); + tracer.buildSpan("spark.launcher").withSpanType("spark").withResourceName(resource).start(); + span.setSamplingPriority(PrioritySampling.USER_KEEP, SamplingMechanism.DATA_JOBS); launcherSpan = span; if (!shutdownHookRegistered) { @@ -56,7 +53,7 @@ static synchronized void finishLauncherSpan(int exitCode) { } if (exitCode != 0) { span.setError(true); - span.setTag("error.type", "Launcher process failed with exit code " + exitCode); + span.setTag(DDTags.ERROR_TYPE, "Spark Launcher Failed with exit code " + exitCode); } span.finish(); launcherSpan = null; @@ -71,7 +68,6 @@ public static void exit( if (throwable != null) { AgentSpan span = launcherSpan; if (span != null) { - span.setError(true); span.addThrowable(throwable); span.finish(); launcherSpan = null; @@ -97,7 +93,6 @@ public static void exit(@Advice.Thrown Throwable throwable) { if (throwable != null) { AgentSpan span = launcherSpan; if (span != null) { - span.setError(true); span.addThrowable(throwable); span.finish(); launcherSpan = null; @@ -124,7 +119,7 @@ public void stateChanged(SparkAppHandle handle) { || state == SparkAppHandle.State.KILLED || state == SparkAppHandle.State.LOST) { span.setError(true); - span.setTag("error.type", "Spark application " + state); + span.setTag(DDTags.ERROR_TYPE, "Spark Application " + state); } } } diff --git a/dd-java-agent/instrumentation/spark/spark-common/src/main/java/datadog/trace/instrumentation/spark/SparkLauncherInstrumentation.java b/dd-java-agent/instrumentation/spark/spark-common/src/main/java/datadog/trace/instrumentation/spark/SparkLauncherInstrumentation.java index 669643912ea..861a37175f4 100644 --- a/dd-java-agent/instrumentation/spark/spark-common/src/main/java/datadog/trace/instrumentation/spark/SparkLauncherInstrumentation.java +++ b/dd-java-agent/instrumentation/spark/spark-common/src/main/java/datadog/trace/instrumentation/spark/SparkLauncherInstrumentation.java @@ -30,8 +30,7 @@ public String instrumentedType() { @Override public String[] helperClassNames() { return new String[] { - packageName + ".SparkLauncherAdvice", - packageName + ".SparkLauncherAdvice$AppHandleListener", + packageName + ".SparkLauncherAdvice", packageName + ".SparkLauncherAdvice$AppHandleListener", }; } diff --git a/dd-java-agent/instrumentation/spark/spark-common/src/testFixtures/groovy/datadog/trace/instrumentation/spark/AbstractSparkTest.groovy b/dd-java-agent/instrumentation/spark/spark-common/src/testFixtures/groovy/datadog/trace/instrumentation/spark/AbstractSparkTest.groovy index 5280d43e49c..93b7502abf3 100644 --- a/dd-java-agent/instrumentation/spark/spark-common/src/testFixtures/groovy/datadog/trace/instrumentation/spark/AbstractSparkTest.groovy +++ b/dd-java-agent/instrumentation/spark/spark-common/src/testFixtures/groovy/datadog/trace/instrumentation/spark/AbstractSparkTest.groovy @@ -810,7 +810,7 @@ abstract class AbstractSparkTest extends InstrumentationSpecification { spanType "spark" resourceName "SparkLauncher.startApplication" errored true - assert span.tags["error.type"] == "Launcher process failed with exit code 1" + assert span.tags["error.type"] == "Spark Launcher Failed with exit code 1" } } } From edbea75c384dcc14c6a15d58c02d10d519e4220a Mon Sep 17 00:00:00 2001 From: Adrien Boitreaud Date: Thu, 19 Feb 2026 10:37:50 +0100 Subject: [PATCH 3/6] Fix tess --- .../instrumentation/spark/AbstractSparkTest.groovy | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/dd-java-agent/instrumentation/spark/spark-common/src/testFixtures/groovy/datadog/trace/instrumentation/spark/AbstractSparkTest.groovy b/dd-java-agent/instrumentation/spark/spark-common/src/testFixtures/groovy/datadog/trace/instrumentation/spark/AbstractSparkTest.groovy index 93b7502abf3..e89800d2c8f 100644 --- a/dd-java-agent/instrumentation/spark/spark-common/src/testFixtures/groovy/datadog/trace/instrumentation/spark/AbstractSparkTest.groovy +++ b/dd-java-agent/instrumentation/spark/spark-common/src/testFixtures/groovy/datadog/trace/instrumentation/spark/AbstractSparkTest.groovy @@ -789,15 +789,15 @@ abstract class AbstractSparkTest extends InstrumentationSpecification { when: // Directly invoke the advice to simulate what would happen when startApplication is called def tracer = datadog.trace.bootstrap.instrumentation.api.AgentTracer.get() - def span = tracer + def launcherSpan = tracer .buildSpan("spark.launcher") .withSpanType("spark") .withResourceName("SparkLauncher.startApplication") .start() - span.setSamplingPriority( + launcherSpan.setSamplingPriority( datadog.trace.api.sampling.PrioritySampling.USER_KEEP, datadog.trace.api.sampling.SamplingMechanism.DATA_JOBS) - SparkLauncherAdvice.launcherSpan = span + SparkLauncherAdvice.launcherSpan = launcherSpan // Simulate a non-zero exit finishing the launcher span SparkLauncherAdvice.finishLauncherSpan(1) @@ -822,15 +822,15 @@ abstract class AbstractSparkTest extends InstrumentationSpecification { when: def tracer = datadog.trace.bootstrap.instrumentation.api.AgentTracer.get() - def span = tracer + def launcherSpan = tracer .buildSpan("spark.launcher") .withSpanType("spark") .withResourceName("SparkLauncher.launch") .start() - span.setSamplingPriority( + launcherSpan.setSamplingPriority( datadog.trace.api.sampling.PrioritySampling.USER_KEEP, datadog.trace.api.sampling.SamplingMechanism.DATA_JOBS) - SparkLauncherAdvice.launcherSpan = span + SparkLauncherAdvice.launcherSpan = launcherSpan // Simulate a successful exit SparkLauncherAdvice.finishLauncherSpan(0) From 9794da840c3f21ed0b96f90e7e78c9ccfa6873d3 Mon Sep 17 00:00:00 2001 From: Adrien Boitreaud Date: Thu, 19 Feb 2026 12:22:37 +0100 Subject: [PATCH 4/6] move test to the right /test dir --- .../spark/spark-common/build.gradle | 4 ++ .../spark/SparkLauncherTest.groovy | 72 +++++++++++++++++++ .../spark/AbstractSparkTest.groovy | 67 ----------------- 3 files changed, 76 insertions(+), 67 deletions(-) create mode 100644 dd-java-agent/instrumentation/spark/spark-common/src/test/groovy/datadog/trace/instrumentation/spark/SparkLauncherTest.groovy diff --git a/dd-java-agent/instrumentation/spark/spark-common/build.gradle b/dd-java-agent/instrumentation/spark/spark-common/build.gradle index 724bfec3f67..f19ebd38b4a 100644 --- a/dd-java-agent/instrumentation/spark/spark-common/build.gradle +++ b/dd-java-agent/instrumentation/spark/spark-common/build.gradle @@ -26,4 +26,8 @@ dependencies { testFixturesCompileOnly(libs.bundles.groovy) testFixturesCompileOnly(libs.bundles.spock) + + testImplementation project(':dd-java-agent:instrumentation-testing') + testImplementation group: 'org.apache.spark', name: 'spark-launcher_2.12', version: '2.4.0' } + diff --git a/dd-java-agent/instrumentation/spark/spark-common/src/test/groovy/datadog/trace/instrumentation/spark/SparkLauncherTest.groovy b/dd-java-agent/instrumentation/spark/spark-common/src/test/groovy/datadog/trace/instrumentation/spark/SparkLauncherTest.groovy new file mode 100644 index 00000000000..cf1ffcf25ed --- /dev/null +++ b/dd-java-agent/instrumentation/spark/spark-common/src/test/groovy/datadog/trace/instrumentation/spark/SparkLauncherTest.groovy @@ -0,0 +1,72 @@ +package datadog.trace.instrumentation.spark + +import datadog.trace.agent.test.InstrumentationSpecification +import datadog.trace.api.sampling.PrioritySampling +import datadog.trace.api.sampling.SamplingMechanism +import datadog.trace.bootstrap.instrumentation.api.AgentTracer + +class SparkLauncherTest extends InstrumentationSpecification { + + def "generate spark.launcher span from startApplication"() { + setup: + SparkLauncherAdvice.launcherSpan = null + + when: + def tracer = AgentTracer.get() + def launcherSpan = tracer + .buildSpan("spark.launcher") + .withSpanType("spark") + .withResourceName("SparkLauncher.startApplication") + .start() + launcherSpan.setSamplingPriority( + PrioritySampling.USER_KEEP, + SamplingMechanism.DATA_JOBS) + SparkLauncherAdvice.launcherSpan = launcherSpan + + SparkLauncherAdvice.finishLauncherSpan(1) + + then: + assertTraces(1) { + trace(1) { + span { + operationName "spark.launcher" + spanType "spark" + resourceName "SparkLauncher.startApplication" + errored true + assert span.tags["error.type"] == "Spark Launcher Failed with exit code 1" + } + } + } + } + + def "generate spark.launcher span with successful exit"() { + setup: + SparkLauncherAdvice.launcherSpan = null + + when: + def tracer = AgentTracer.get() + def launcherSpan = tracer + .buildSpan("spark.launcher") + .withSpanType("spark") + .withResourceName("SparkLauncher.launch") + .start() + launcherSpan.setSamplingPriority( + PrioritySampling.USER_KEEP, + SamplingMechanism.DATA_JOBS) + SparkLauncherAdvice.launcherSpan = launcherSpan + + SparkLauncherAdvice.finishLauncherSpan(0) + + then: + assertTraces(1) { + trace(1) { + span { + operationName "spark.launcher" + spanType "spark" + resourceName "SparkLauncher.launch" + errored false + } + } + } + } +} diff --git a/dd-java-agent/instrumentation/spark/spark-common/src/testFixtures/groovy/datadog/trace/instrumentation/spark/AbstractSparkTest.groovy b/dd-java-agent/instrumentation/spark/spark-common/src/testFixtures/groovy/datadog/trace/instrumentation/spark/AbstractSparkTest.groovy index e89800d2c8f..c39f42f473c 100644 --- a/dd-java-agent/instrumentation/spark/spark-common/src/testFixtures/groovy/datadog/trace/instrumentation/spark/AbstractSparkTest.groovy +++ b/dd-java-agent/instrumentation/spark/spark-common/src/testFixtures/groovy/datadog/trace/instrumentation/spark/AbstractSparkTest.groovy @@ -780,71 +780,4 @@ abstract class AbstractSparkTest extends InstrumentationSpecification { } } } - - def "generate spark.launcher span from startApplication"() { - setup: - // Reset any previous launcher span - SparkLauncherAdvice.launcherSpan = null - - when: - // Directly invoke the advice to simulate what would happen when startApplication is called - def tracer = datadog.trace.bootstrap.instrumentation.api.AgentTracer.get() - def launcherSpan = tracer - .buildSpan("spark.launcher") - .withSpanType("spark") - .withResourceName("SparkLauncher.startApplication") - .start() - launcherSpan.setSamplingPriority( - datadog.trace.api.sampling.PrioritySampling.USER_KEEP, - datadog.trace.api.sampling.SamplingMechanism.DATA_JOBS) - SparkLauncherAdvice.launcherSpan = launcherSpan - - // Simulate a non-zero exit finishing the launcher span - SparkLauncherAdvice.finishLauncherSpan(1) - - then: - assertTraces(1) { - trace(1) { - span { - operationName "spark.launcher" - spanType "spark" - resourceName "SparkLauncher.startApplication" - errored true - assert span.tags["error.type"] == "Spark Launcher Failed with exit code 1" - } - } - } - } - - def "generate spark.launcher span with successful exit"() { - setup: - SparkLauncherAdvice.launcherSpan = null - - when: - def tracer = datadog.trace.bootstrap.instrumentation.api.AgentTracer.get() - def launcherSpan = tracer - .buildSpan("spark.launcher") - .withSpanType("spark") - .withResourceName("SparkLauncher.launch") - .start() - launcherSpan.setSamplingPriority( - datadog.trace.api.sampling.PrioritySampling.USER_KEEP, - datadog.trace.api.sampling.SamplingMechanism.DATA_JOBS) - SparkLauncherAdvice.launcherSpan = launcherSpan - - // Simulate a successful exit - SparkLauncherAdvice.finishLauncherSpan(0) - - then: - assertTraces(1) { - trace(1) { - span { - operationName "spark.launcher" - spanType "spark" - resourceName "SparkLauncher.launch" - errored false - } - } - } - } } From bf992601073d913a52f26e9de407f29179c10efd Mon Sep 17 00:00:00 2001 From: Adrien Boitreaud Date: Thu, 19 Feb 2026 17:15:05 +0100 Subject: [PATCH 5/6] advice should be public --- .../trace/instrumentation/spark/SparkLauncherAdvice.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/dd-java-agent/instrumentation/spark/spark-common/src/main/java/datadog/trace/instrumentation/spark/SparkLauncherAdvice.java b/dd-java-agent/instrumentation/spark/spark-common/src/main/java/datadog/trace/instrumentation/spark/SparkLauncherAdvice.java index cfd14c65a81..e183140e4a9 100644 --- a/dd-java-agent/instrumentation/spark/spark-common/src/main/java/datadog/trace/instrumentation/spark/SparkLauncherAdvice.java +++ b/dd-java-agent/instrumentation/spark/spark-common/src/main/java/datadog/trace/instrumentation/spark/SparkLauncherAdvice.java @@ -10,16 +10,16 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -class SparkLauncherAdvice { +public class SparkLauncherAdvice { private static final Logger log = LoggerFactory.getLogger(SparkLauncherAdvice.class); /** The launcher span, accessible from SparkExitAdvice via reflection. */ - static volatile AgentSpan launcherSpan; + public static volatile AgentSpan launcherSpan; private static volatile boolean shutdownHookRegistered = false; - private static synchronized void createLauncherSpan(String resource) { + public static synchronized void createLauncherSpan(String resource) { if (launcherSpan != null) { return; } @@ -46,7 +46,7 @@ private static synchronized void createLauncherSpan(String resource) { } } - static synchronized void finishLauncherSpan(int exitCode) { + public static synchronized void finishLauncherSpan(int exitCode) { AgentSpan span = launcherSpan; if (span == null) { return; From 74326e0291793932aeb557c625b3854e4b848619 Mon Sep 17 00:00:00 2001 From: Adrien Boitreaud Date: Thu, 19 Feb 2026 18:44:44 +0100 Subject: [PATCH 6/6] finish launcher span with error via RunMainAdvice --- .../spark/AbstractSparkInstrumentation.java | 11 +++++++++++ .../instrumentation/spark/SparkLauncherAdvice.java | 14 +++++++++++++- 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/dd-java-agent/instrumentation/spark/spark-common/src/main/java/datadog/trace/instrumentation/spark/AbstractSparkInstrumentation.java b/dd-java-agent/instrumentation/spark/spark-common/src/main/java/datadog/trace/instrumentation/spark/AbstractSparkInstrumentation.java index 80c71a4f64b..fe777e7f276 100644 --- a/dd-java-agent/instrumentation/spark/spark-common/src/main/java/datadog/trace/instrumentation/spark/AbstractSparkInstrumentation.java +++ b/dd-java-agent/instrumentation/spark/spark-common/src/main/java/datadog/trace/instrumentation/spark/AbstractSparkInstrumentation.java @@ -106,6 +106,17 @@ public static void exit(@Advice.Thrown Throwable throwable) { if (AbstractDatadogSparkListener.listener != null) { AbstractDatadogSparkListener.listener.finishApplication( System.currentTimeMillis(), throwable, 0, null); + } else { + try { + Class adviceClass = + Thread.currentThread() + .getContextClassLoader() + .loadClass("datadog.trace.instrumentation.spark.SparkLauncherAdvice"); + java.lang.reflect.Method finishMethod = + adviceClass.getMethod("finishLauncherSpan", Throwable.class); + finishMethod.invoke(null, throwable); + } catch (Exception ignored) { + } } } } diff --git a/dd-java-agent/instrumentation/spark/spark-common/src/main/java/datadog/trace/instrumentation/spark/SparkLauncherAdvice.java b/dd-java-agent/instrumentation/spark/spark-common/src/main/java/datadog/trace/instrumentation/spark/SparkLauncherAdvice.java index e183140e4a9..302de156ae6 100644 --- a/dd-java-agent/instrumentation/spark/spark-common/src/main/java/datadog/trace/instrumentation/spark/SparkLauncherAdvice.java +++ b/dd-java-agent/instrumentation/spark/spark-common/src/main/java/datadog/trace/instrumentation/spark/SparkLauncherAdvice.java @@ -26,7 +26,7 @@ public static synchronized void createLauncherSpan(String resource) { AgentTracer.TracerAPI tracer = AgentTracer.get(); AgentSpan span = - tracer.buildSpan("spark.launcher").withSpanType("spark").withResourceName(resource).start(); + tracer.buildSpan("spark.launcher.launch").withSpanType("spark").withResourceName(resource).start(); span.setSamplingPriority(PrioritySampling.USER_KEEP, SamplingMechanism.DATA_JOBS); launcherSpan = span; @@ -59,6 +59,18 @@ public static synchronized void finishLauncherSpan(int exitCode) { launcherSpan = null; } + public static synchronized void finishLauncherSpan(Throwable throwable) { + AgentSpan span = launcherSpan; + if (span == null) { + return; + } + if (throwable != null) { + span.addThrowable(throwable); + } + span.finish(); + launcherSpan = null; + } + public static class StartApplicationAdvice { @Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class) public static void exit(