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())); - } -}