diff --git a/pom.xml b/pom.xml
index b8e8d6ef..eb11abb6 100644
--- a/pom.xml
+++ b/pom.xml
@@ -149,6 +149,12 @@
parsson
1.1.7
+
+
+ org.jenkins-ci.plugins.workflow
+ workflow-step-api
+ 707.v76364b_2b_6818
+
@@ -377,6 +383,10 @@
org.jenkins-ci.plugins.workflow
workflow-multibranch
+
+ org.jenkins-ci.plugins.workflow
+ workflow-step-api
+
org.jenkinsci.plugins
pipeline-model-definition
diff --git a/src/main/java/io/jenkins/plugins/opentelemetry/init/StepExecutionInstrumentationInitializer.java b/src/main/java/io/jenkins/plugins/opentelemetry/init/StepExecutionInstrumentationInitializer.java
index 76581299..df0270a5 100644
--- a/src/main/java/io/jenkins/plugins/opentelemetry/init/StepExecutionInstrumentationInitializer.java
+++ b/src/main/java/io/jenkins/plugins/opentelemetry/init/StepExecutionInstrumentationInitializer.java
@@ -6,48 +6,30 @@
package io.jenkins.plugins.opentelemetry.init;
import hudson.Extension;
-import hudson.util.ClassLoaderSanityThreadFactory;
-import hudson.util.DaemonThreadFactory;
-import hudson.util.NamingThreadFactory;
-import io.jenkins.plugins.opentelemetry.api.OpenTelemetryLifecycleListener;
import io.opentelemetry.context.Context;
-import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties;
-import java.lang.reflect.Field;
-import java.util.Arrays;
-import java.util.Optional;
import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
import java.util.logging.Level;
import java.util.logging.Logger;
-import javax.annotation.Nonnull;
import org.jenkinsci.plugins.workflow.steps.SynchronousNonBlockingStepExecution;
+/**
+ * Initializes the instrumentation for {@link SynchronousNonBlockingStepExecution} by augmenting the
+ * {@link ExecutorService} to ensure that the OpenTelemetry context is propagated correctly.
+ */
@Extension
-public class StepExecutionInstrumentationInitializer implements OpenTelemetryLifecycleListener {
+public class StepExecutionInstrumentationInitializer
+ implements SynchronousNonBlockingStepExecution.ExecutorServiceAugmentor {
static final Logger logger = Logger.getLogger(StepExecutionInstrumentationInitializer.class.getName());
- @Override
- public void afterConfiguration(@Nonnull ConfigProperties configProperties) {
- try {
- logger.log(
- Level.FINE, () -> "Instrumenting " + SynchronousNonBlockingStepExecution.class.getName() + "...");
- Class synchronousNonBlockingStepExecutionClass =
- SynchronousNonBlockingStepExecution.class;
- Arrays.stream(synchronousNonBlockingStepExecutionClass.getDeclaredFields())
- .forEach(field -> logger.log(Level.FINE, () -> "Field: " + field.getName()));
- Field executorServiceField = synchronousNonBlockingStepExecutionClass.getDeclaredField("executorService");
- executorServiceField.setAccessible(true);
- ExecutorService executorService = (ExecutorService) Optional.ofNullable(executorServiceField.get(null))
- .orElseGet(() -> Executors.newCachedThreadPool(new NamingThreadFactory(
- new ClassLoaderSanityThreadFactory(new DaemonThreadFactory()),
- "org.jenkinsci.plugins.workflow.steps.SynchronousNonBlockingStepExecution")));
- ExecutorService instrumentedExecutorService = Context.taskWrapping(executorService);
- executorServiceField.set(null, instrumentedExecutorService);
-
- // org.jenkinsci.plugins.workflow.cps.CpsThreadGroup.runner
- } catch (NoSuchFieldException | IllegalAccessException e) {
- throw new RuntimeException(e);
- }
+ /**
+ * Augment the provided ExecutorService to wrap it with OpenTelemetry context propagation
+ * @param executorService the ExecutorService to augment
+ * @return the augmented ExecutorService
+ * @see SynchronousNonBlockingStepExecution#getExecutorService()
+ */
+ public ExecutorService augment(ExecutorService executorService) {
+ logger.log(Level.FINE, () -> "Instrumenting " + SynchronousNonBlockingStepExecution.class.getName() + "...");
+ return Context.taskWrapping(executorService);
}
}
diff --git a/src/test/java/io/jenkins/plugins/opentelemetry/JenkinsOtelPluginNoConfigurationTest.java b/src/test/java/io/jenkins/plugins/opentelemetry/JenkinsOtelPluginNoConfigurationTest.java
new file mode 100644
index 00000000..1e47aafe
--- /dev/null
+++ b/src/test/java/io/jenkins/plugins/opentelemetry/JenkinsOtelPluginNoConfigurationTest.java
@@ -0,0 +1,78 @@
+package io.jenkins.plugins.opentelemetry;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.hasSize;
+import static org.hamcrest.Matchers.nullValue;
+
+import hudson.model.Result;
+import io.jenkins.plugins.opentelemetry.init.StepExecutionInstrumentationInitializer;
+import io.opentelemetry.sdk.common.CompletableResultCode;
+import io.opentelemetry.sdk.testing.exporter.InMemorySpanExporterProvider;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.TimeUnit;
+import org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition;
+import org.jenkinsci.plugins.workflow.job.WorkflowJob;
+import org.junit.Rule;
+import org.junit.Test;
+import org.jvnet.hudson.test.BuildWatcher;
+import org.jvnet.hudson.test.JenkinsRule;
+
+public class JenkinsOtelPluginNoConfigurationTest {
+
+ @Rule
+ public JenkinsRule j = new JenkinsRule();
+
+ @Rule
+ public BuildWatcher buildWatcher = new BuildWatcher();
+
+ /**
+ * Test that the StepExecutionInstrumentationInitializer does nothing when configuration is not set.
+ * This test is similar to {@link JenkinsOtelPluginIntegrationTest#testSpanContextPropagationSynchronousNonBlockingTestStep()}
+ */
+ @Test
+ public void testNoOpWhenNotConfigured() throws Exception {
+
+ String pipelineScript =
+ """
+ node() {
+ stage('ze-stage1') {
+ echo message: 'hello'
+ spanContextPropagationSynchronousNonBlockingTestStep()
+ }
+ }""";
+ j.createOnlineSlave();
+ final String jobName = "test-SpanContextPropagationSynchronousTestStep";
+ WorkflowJob pipeline = j.createProject(WorkflowJob.class, jobName);
+ pipeline.setDefinition(new CpsFlowDefinition(pipelineScript, true));
+ j.assertBuildStatus(Result.SUCCESS, pipeline.scheduleBuild2(0));
+
+ CompletableResultCode result = JenkinsControllerOpenTelemetry.get()
+ .getOpenTelemetrySdk()
+ .getSdkTracerProvider()
+ .forceFlush();
+ result.join(1, TimeUnit.SECONDS);
+
+ // without specific configuration, no spans should be exported
+ assertThat(InMemorySpanExporterProvider.LAST_CREATED_INSTANCE, nullValue());
+ }
+
+ /**
+ * Make sure a standard pipeline with synchronous non-blocking steps works with {@link StepExecutionInstrumentationInitializer#augment(ExecutorService)}
+ */
+ @Test
+ public void testStandardPipeline() throws Exception {
+ j.createOnlineSlave();
+ WorkflowJob pipeline = j.createProject(WorkflowJob.class);
+ pipeline.setDefinition(new CpsFlowDefinition(
+ """
+ node {
+ writeFile(file: 'file', text: 'Hello, World!')
+ archiveArtifacts('file')
+ }
+ """,
+ true));
+
+ var build = j.assertBuildStatus(Result.SUCCESS, pipeline.scheduleBuild2(0));
+ assertThat(build.getArtifacts(), hasSize(1));
+ }
+}
diff --git a/src/test/java/io/jenkins/plugins/opentelemetry/init/StepExecutionInstrumentationInitializerTest.java b/src/test/java/io/jenkins/plugins/opentelemetry/init/StepExecutionInstrumentationInitializerTest.java
deleted file mode 100644
index 2928c745..00000000
--- a/src/test/java/io/jenkins/plugins/opentelemetry/init/StepExecutionInstrumentationInitializerTest.java
+++ /dev/null
@@ -1,21 +0,0 @@
-/*
- * Copyright The Original Author or Authors
- * SPDX-License-Identifier: Apache-2.0
- */
-
-package io.jenkins.plugins.opentelemetry.init;
-
-import io.opentelemetry.sdk.autoconfigure.spi.internal.DefaultConfigProperties;
-import java.util.Collections;
-import org.junit.Test;
-
-public class StepExecutionInstrumentationInitializerTest {
-
- @Test
- public void testAfterConfiguration() {
- StepExecutionInstrumentationInitializer stepExecutionInstrumentationInitializer =
- new StepExecutionInstrumentationInitializer();
- stepExecutionInstrumentationInitializer.afterConfiguration(
- DefaultConfigProperties.createFromMap(Collections.emptyMap()));
- }
-}