diff --git a/src/main/java/io/jenkins/plugins/opentelemetry/init/StepExecutionInstrumentationInitializer.java b/src/main/java/io/jenkins/plugins/opentelemetry/init/StepExecutionInstrumentationInitializer.java new file mode 100644 index 000000000..e97541a31 --- /dev/null +++ b/src/main/java/io/jenkins/plugins/opentelemetry/init/StepExecutionInstrumentationInitializer.java @@ -0,0 +1,48 @@ +/* + * Copyright The Original Author or Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +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 org.jenkinsci.plugins.workflow.steps.SynchronousNonBlockingStepExecution; + +import javax.annotation.Nonnull; +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; + +@Extension +public class StepExecutionInstrumentationInitializer implements OpenTelemetryLifecycleListener { + + final static 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); + } + } +} diff --git a/src/main/java/io/jenkins/plugins/opentelemetry/job/MonitoringAction.java b/src/main/java/io/jenkins/plugins/opentelemetry/job/MonitoringAction.java index 6b536ae11..2cf9cfae8 100644 --- a/src/main/java/io/jenkins/plugins/opentelemetry/job/MonitoringAction.java +++ b/src/main/java/io/jenkins/plugins/opentelemetry/job/MonitoringAction.java @@ -50,7 +50,7 @@ public class MonitoringAction extends AbstractMonitoringAction implements Action private transient Run run; public MonitoringAction(Span span) { - super(span); + super(span, Collections.emptyList()); this.rootSpanName = super.getSpanName(); this.rootContext = super.getW3cTraceContext(); } diff --git a/src/main/java/io/jenkins/plugins/opentelemetry/job/MonitoringPipelineListener.java b/src/main/java/io/jenkins/plugins/opentelemetry/job/MonitoringPipelineListener.java index 73085ceb8..27feb317d 100644 --- a/src/main/java/io/jenkins/plugins/opentelemetry/job/MonitoringPipelineListener.java +++ b/src/main/java/io/jenkins/plugins/opentelemetry/job/MonitoringPipelineListener.java @@ -200,38 +200,37 @@ public void onAtomicStep(@NonNull StepAtomNode node, @NonNull WorkflowRun run) { LOGGER.log(Level.FINE, () -> run.getFullDisplayName() + " - don't create span for step '" + node.getDisplayFunctionName() + "'"); return; } - try (Scope ignored = setupContext(run, node)) { - verifyNotNull(ignored, "%s - No span found for node %s", run, node); + Scope encapsulatingNodeScope = setupContext(run, node); - String principal = Objects.toString(node.getExecution().getAuthentication().getPrincipal(), "#null#"); - LOGGER.log(Level.FINE, () -> node.getDisplayFunctionName() + " - principal: " + principal); + verifyNotNull(encapsulatingNodeScope, "%s - No span found for node %s", run, node); - StepHandler stepHandler = getStepHandlers().stream().filter(sh -> sh.canCreateSpanBuilder(node, run)).findFirst() - .orElseThrow((Supplier) () -> - new IllegalStateException("No StepHandler found for node " + node.getClass() + " - " + node + " on " + run)); - SpanBuilder spanBuilder = stepHandler.createSpanBuilder(node, run, getTracer()); + String principal = Objects.toString(node.getExecution().getAuthentication().getPrincipal(), "#null#"); - String stepType = getStepType(node, node.getDescriptor(), JenkinsOtelSemanticAttributes.STEP_NAME); - JenkinsOpenTelemetryPluginConfiguration.StepPlugin stepPlugin = JenkinsOpenTelemetryPluginConfiguration.get().findStepPluginOrDefault(stepType, node); + StepHandler stepHandler = getStepHandlers().stream().filter(sh -> sh.canCreateSpanBuilder(node, run)).findFirst() + .orElseThrow((Supplier) () -> + new IllegalStateException("No StepHandler found for node " + node.getClass() + " - " + node + " on " + run)); + SpanBuilder spanBuilder = stepHandler.createSpanBuilder(node, run, getTracer()); - spanBuilder - .setParent(Context.current()) // TODO can we remove this call? - .setAttribute(JenkinsOtelSemanticAttributes.JENKINS_STEP_TYPE, stepType) - .setAttribute(JenkinsOtelSemanticAttributes.JENKINS_STEP_ID, node.getId()) - .setAttribute(JenkinsOtelSemanticAttributes.JENKINS_STEP_NAME, getStepName(node, JenkinsOtelSemanticAttributes.STEP_NAME)) - .setAttribute(JenkinsOtelSemanticAttributes.CI_PIPELINE_RUN_USER, principal) - .setAttribute(JenkinsOtelSemanticAttributes.JENKINS_STEP_PLUGIN_NAME, stepPlugin.getName()) - .setAttribute(JenkinsOtelSemanticAttributes.JENKINS_STEP_PLUGIN_VERSION, stepPlugin.getVersion()); + String stepType = getStepType(node, node.getDescriptor(), JenkinsOtelSemanticAttributes.STEP_NAME); + JenkinsOpenTelemetryPluginConfiguration.StepPlugin stepPlugin = JenkinsOpenTelemetryPluginConfiguration.get().findStepPluginOrDefault(stepType, node); - Span atomicStepSpan = spanBuilder.startSpan(); - LOGGER.log(Level.FINE, () -> run.getFullDisplayName() + " - > " + node.getDisplayFunctionName() + " - begin " + OtelUtils.toDebugString(atomicStepSpan)); - try (Scope ignored2 = atomicStepSpan.makeCurrent()) { - stepHandler.afterSpanCreated(node, run); - } - getTracerService().putSpan(run, atomicStepSpan, node); - } + spanBuilder + .setAttribute(JenkinsOtelSemanticAttributes.JENKINS_STEP_TYPE, stepType) + .setAttribute(JenkinsOtelSemanticAttributes.JENKINS_STEP_ID, node.getId()) + .setAttribute(JenkinsOtelSemanticAttributes.JENKINS_STEP_NAME, getStepName(node, JenkinsOtelSemanticAttributes.STEP_NAME)) + .setAttribute(JenkinsOtelSemanticAttributes.CI_PIPELINE_RUN_USER, principal) + .setAttribute(JenkinsOtelSemanticAttributes.JENKINS_STEP_PLUGIN_NAME, stepPlugin.getName()) + .setAttribute(JenkinsOtelSemanticAttributes.JENKINS_STEP_PLUGIN_VERSION, stepPlugin.getVersion()); + + Span atomicStepSpan = spanBuilder.startSpan(); + LOGGER.log(Level.FINE, () -> run.getFullDisplayName() + " - > " + node.getDisplayFunctionName() + " - begin " + OtelUtils.toDebugString(atomicStepSpan)); + Scope atomicStepScope = atomicStepSpan.makeCurrent(); + stepHandler.afterSpanCreated(node, run); + + getTracerService().putSpanAndScopes(run, atomicStepSpan, node, Arrays.asList(encapsulatingNodeScope, atomicStepScope)); } + @Override public void onAfterAtomicStep(@NonNull StepAtomNode node, FlowNode nextNode, @NonNull WorkflowRun run) { if (isIgnoredStep(node.getDescriptor())){ @@ -371,7 +370,7 @@ private void endCurrentSpan(FlowNode node, WorkflowRun run, GenericStatus status span.end(); LOGGER.log(Level.FINE, () -> run.getFullDisplayName() + " - < " + node.getDisplayFunctionName() + " - end " + OtelUtils.toDebugString(span)); - getTracerService().removePipelineStepSpan(run, node, span); + getTracerService().removePipelineStepSpanAndCloseAssociatedScopes(run, node, span); } } diff --git a/src/main/java/io/jenkins/plugins/opentelemetry/job/OtelTraceService.java b/src/main/java/io/jenkins/plugins/opentelemetry/job/OtelTraceService.java index 61029ff61..d19081184 100644 --- a/src/main/java/io/jenkins/plugins/opentelemetry/job/OtelTraceService.java +++ b/src/main/java/io/jenkins/plugins/opentelemetry/job/OtelTraceService.java @@ -23,6 +23,7 @@ import io.jenkins.plugins.opentelemetry.semconv.JenkinsOtelSemanticAttributes; import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.api.trace.Span; +import io.opentelemetry.context.Scope; import org.jenkinsci.plugins.workflow.cps.nodes.StepEndNode; import org.jenkinsci.plugins.workflow.cps.nodes.StepStartNode; import org.jenkinsci.plugins.workflow.flow.FlowExecution; @@ -155,7 +156,7 @@ private Iterable getAncestors(@NonNull final FlowNode flowNode) { return ancestors; } - public void removePipelineStepSpan(@NonNull WorkflowRun run, @NonNull FlowNode flowNode, @NonNull Span span) { + public void removePipelineStepSpanAndCloseAssociatedScopes(@NonNull WorkflowRun run, @NonNull FlowNode flowNode, @NonNull Span span) { FlowNode startSpanNode; if (flowNode instanceof AtomNode) { startSpanNode = flowNode; @@ -183,7 +184,7 @@ public void removePipelineStepSpan(@NonNull WorkflowRun run, @NonNull FlowNode f .filter(flowNodeMonitoringAction -> Objects.equals(flowNodeMonitoringAction.getSpanId(), span.getSpanContext().getSpanId())) .findFirst() .ifPresentOrElse( - FlowNodeMonitoringAction::purgeSpan, + FlowNodeMonitoringAction::purgeSpanAndCloseAssociatedScopes, () -> { String msg = "span not found to be purged: " + OtelUtils.toDebugString(span) + " ending " + OtelUtils.toDebugString(startSpanNode) + " in " + run; @@ -204,20 +205,20 @@ public void removeBuildStepSpan(@NonNull AbstractBuild build, @NonNull BuildStep .reverse() .stream() .filter(buildStepMonitoringAction -> Objects.equals(buildStepMonitoringAction.getSpanId(), span.getSpanContext().getSpanId())) - .findFirst().ifPresentOrElse(BuildStepMonitoringAction::purgeSpan, () -> { + .findFirst().ifPresentOrElse(BuildStepMonitoringAction::purgeSpanAndCloseAssociatedScopes, () -> { throw new IllegalStateException("span not found to be purged: " + span + " for " + buildStep); }); } public void purgeRun(@NonNull Run run) { - run.getActions(OtelMonitoringAction.class).forEach(OtelMonitoringAction::purgeSpan); + run.getActions(OtelMonitoringAction.class).forEach(OtelMonitoringAction::purgeSpanAndCloseAssociatedScopes); // TODO verify we don't need this cleanup if (run instanceof WorkflowRun) { WorkflowRun workflowRun = (WorkflowRun) run; List flowNodesHeads = Optional.ofNullable(workflowRun.getExecution()).map(FlowExecution::getCurrentHeads).orElse(Collections.emptyList()); ForkScanner scanner = new ForkScanner(); scanner.setup(flowNodesHeads); - StreamSupport.stream(scanner.spliterator(), false).forEach(flowNode -> flowNode.getActions(OtelMonitoringAction.class).forEach(OtelMonitoringAction::purgeSpan)); + StreamSupport.stream(scanner.spliterator(), false).forEach(flowNode -> flowNode.getActions(OtelMonitoringAction.class).forEach(OtelMonitoringAction::purgeSpanAndCloseAssociatedScopes)); } } @@ -260,6 +261,14 @@ public void putSpan(@NonNull Run run, @NonNull Span span, @NonNull FlowNode flow OtelUtils.toDebugString(flowNode) + ", " + OtelUtils.toDebugString(span) + ")"); } + public void putSpanAndScopes(@NonNull Run run, @NonNull Span span, @NonNull FlowNode flowNode, List scopes) { + // FYI for agent allocation, we have 2 FlowNodeMonitoringAction to track the agent allocation duration + flowNode.addAction(new FlowNodeMonitoringAction(span, scopes)); + + LOGGER.log(Level.FINE, () -> "putSpan(" + run.getFullDisplayName() + ", " + + OtelUtils.toDebugString(flowNode) + ", " + OtelUtils.toDebugString(span) + ")"); + } + private void setAttributesToSpan(@NonNull Span span, OpenTelemetryAttributesAction openTelemetryAttributesAction) { if (openTelemetryAttributesAction == null) { return; diff --git a/src/main/java/io/jenkins/plugins/opentelemetry/job/action/AbstractInvisibleMonitoringAction.java b/src/main/java/io/jenkins/plugins/opentelemetry/job/action/AbstractInvisibleMonitoringAction.java index 1b240fc56..03989e24f 100644 --- a/src/main/java/io/jenkins/plugins/opentelemetry/job/action/AbstractInvisibleMonitoringAction.java +++ b/src/main/java/io/jenkins/plugins/opentelemetry/job/action/AbstractInvisibleMonitoringAction.java @@ -6,11 +6,19 @@ package io.jenkins.plugins.opentelemetry.job.action; import io.opentelemetry.api.trace.Span; +import io.opentelemetry.context.Scope; + +import java.util.Collections; +import java.util.List; public abstract class AbstractInvisibleMonitoringAction extends AbstractMonitoringAction { public AbstractInvisibleMonitoringAction(Span span) { - super(span); + super(span, Collections.emptyList()); + } + + public AbstractInvisibleMonitoringAction(Span span, List scopes) { + super(span, scopes); } @Override diff --git a/src/main/java/io/jenkins/plugins/opentelemetry/job/action/AbstractMonitoringAction.java b/src/main/java/io/jenkins/plugins/opentelemetry/job/action/AbstractMonitoringAction.java index 2ecfbb30e..6aca166bd 100644 --- a/src/main/java/io/jenkins/plugins/opentelemetry/job/action/AbstractMonitoringAction.java +++ b/src/main/java/io/jenkins/plugins/opentelemetry/job/action/AbstractMonitoringAction.java @@ -5,6 +5,7 @@ package io.jenkins.plugins.opentelemetry.job.action; +import com.google.common.collect.ImmutableList; import edu.umd.cs.findbugs.annotations.CheckForNull; import edu.umd.cs.findbugs.annotations.NonNull; import hudson.model.Action; @@ -17,6 +18,7 @@ import java.util.Collections; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Optional; import java.util.logging.Level; @@ -25,14 +27,20 @@ public abstract class AbstractMonitoringAction implements Action, OtelMonitoringAction { private final static Logger LOGGER = Logger.getLogger(AbstractMonitoringAction.class.getName()); - private transient Span span; + transient SpanAndScopes spanAndScopes; + + final String traceId; final String spanId; protected String spanName; protected Map w3cTraceContext; - public AbstractMonitoringAction(Span span) { - this.span = span; + /** + * @param span span of this action + * @param scopes scope and underlying scopes associated with the span. + */ + public AbstractMonitoringAction(Span span, List scopes) { + this.spanAndScopes = new SpanAndScopes(span, scopes, Thread.currentThread().getName()); this.traceId = span.getSpanContext().getTraceId(); this.spanId = span.getSpanContext().getSpanId(); this.spanName = span instanceof ReadWriteSpan ? ((ReadWriteSpan) span).getName() : null; // when tracer is no-op, span is NOT a ReadWriteSpan @@ -41,6 +49,8 @@ public AbstractMonitoringAction(Span span) { W3CTraceContextPropagator.getInstance().inject(Context.current(), w3cTraceContext, (carrier, key, value) -> carrier.put(key, value)); this.w3cTraceContext = w3cTraceContext; } + + LOGGER.log(Level.FINE, () -> "Span " + getSpanName() + ", thread=" + spanAndScopes.scopeStartThreadName + " opened " + spanAndScopes.scopes.size() + " scopes"); } public String getSpanName() { @@ -55,7 +65,7 @@ public Map getW3cTraceContext() { @Override @CheckForNull public Span getSpan() { - return span; + return spanAndScopes.span; } public String getTraceId() { @@ -67,9 +77,14 @@ public String getSpanId() { } @Override - public void purgeSpan() { - LOGGER.log(Level.FINE, () -> "Purge span='" + spanName + "', spanId=" + spanId + ", traceId=" + traceId + ": " + (span == null ? "#null#" : "purged")); - this.span = null; + public void purgeSpanAndCloseAssociatedScopes() { + LOGGER.log(Level.FINE, () -> "Purge span='" + spanName + "', spanId=" + spanId + ", traceId=" + traceId + ": " + spanAndScopes); + Optional.ofNullable(spanAndScopes) + .map(spanAndScopes -> spanAndScopes.scopes) + .map(ImmutableList::copyOf) + .map(ImmutableList::reverse) + .ifPresent(scopes -> scopes.forEach(Scope::close)); + this.spanAndScopes = null; } @Override @@ -85,8 +100,40 @@ public String toString() { public boolean hasEnded() { return Optional - .ofNullable(span).map(s -> s instanceof ReadableSpan ? (ReadableSpan) s : null) // cast to ReadableSpan + .ofNullable(spanAndScopes) + .map(sac -> sac.span) + .filter(s -> s instanceof ReadableSpan) + .map(s -> (ReadableSpan) s) .map(ReadableSpan::hasEnded) .orElse(true); } + + /** + * Scopes associated with the span and the underlying scopes instantiated to create the span. + * Underlying scopes can be the scope of the underlying wrapping pipeline step (eg a `stage` step). + * Thread name when the scope was opened. Used for debugging, to identify potential leaks. + */ + static class SpanAndScopes { + @NonNull + final Span span; + @NonNull + final List scopes; + @NonNull + final String scopeStartThreadName; + + public SpanAndScopes(@NonNull Span span, @NonNull List scopes, @NonNull String scopeStartThreadName) { + this.span = span; + this.scopes = scopes; + this.scopeStartThreadName = scopeStartThreadName; + } + + @Override + public String toString() { + return "SpanAndScopes{" + + "span=" + span + + ", scopes=" + scopes.size() + + ", scopeStartThreadName='" + scopeStartThreadName + '\'' + + '}'; + } + } } diff --git a/src/main/java/io/jenkins/plugins/opentelemetry/job/action/FlowNodeMonitoringAction.java b/src/main/java/io/jenkins/plugins/opentelemetry/job/action/FlowNodeMonitoringAction.java index 37cdaa89d..8cc36bfe4 100644 --- a/src/main/java/io/jenkins/plugins/opentelemetry/job/action/FlowNodeMonitoringAction.java +++ b/src/main/java/io/jenkins/plugins/opentelemetry/job/action/FlowNodeMonitoringAction.java @@ -6,12 +6,20 @@ package io.jenkins.plugins.opentelemetry.job.action; import io.opentelemetry.api.trace.Span; +import io.opentelemetry.context.Scope; + +import java.util.List; /** * Span reference associate with a {@link org.jenkinsci.plugins.workflow.graph.FlowNode} */ public class FlowNodeMonitoringAction extends AbstractInvisibleMonitoringAction { + public FlowNodeMonitoringAction(Span span) { super(span); } + + public FlowNodeMonitoringAction(Span span, List scopes) { + super(span, scopes); + } } diff --git a/src/main/java/io/jenkins/plugins/opentelemetry/job/action/OtelMonitoringAction.java b/src/main/java/io/jenkins/plugins/opentelemetry/job/action/OtelMonitoringAction.java index aed284966..92ad7c5db 100644 --- a/src/main/java/io/jenkins/plugins/opentelemetry/job/action/OtelMonitoringAction.java +++ b/src/main/java/io/jenkins/plugins/opentelemetry/job/action/OtelMonitoringAction.java @@ -22,7 +22,7 @@ public interface OtelMonitoringAction extends Action { @CheckForNull Span getSpan(); - void purgeSpan(); + void purgeSpanAndCloseAssociatedScopes(); /** * @return {@code true} if the associated {@link Span} has ended diff --git a/src/test/java/io/jenkins/plugins/opentelemetry/JenkinsOtelPluginIntegrationTest.java b/src/test/java/io/jenkins/plugins/opentelemetry/JenkinsOtelPluginIntegrationTest.java index e4d65748d..99f6de1d7 100644 --- a/src/test/java/io/jenkins/plugins/opentelemetry/JenkinsOtelPluginIntegrationTest.java +++ b/src/test/java/io/jenkins/plugins/opentelemetry/JenkinsOtelPluginIntegrationTest.java @@ -11,14 +11,18 @@ import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; +import hudson.ExtensionList; +import io.jenkins.plugins.opentelemetry.job.step.SpanContextPropagationSynchronousTestStep; import org.apache.commons.lang3.SystemUtils; import org.hamcrest.CoreMatchers; import org.hamcrest.MatcherAssert; import org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition; import org.jenkinsci.plugins.workflow.job.WorkflowJob; +import org.jenkinsci.plugins.workflow.steps.EchoStep; import org.junit.Ignore; import org.junit.Test; import org.jvnet.hudson.test.recipes.WithPlugin; @@ -512,4 +516,54 @@ public void testFailFastParallelScriptedPipelineWithException() throws Exception MatcherAssert.assertThat(failingBranchSpanData.getStatus().getStatusCode(), CoreMatchers.is(StatusCode.ERROR)); MatcherAssert.assertThat(failingBranchSpanData.getStatus().getDescription(), CoreMatchers.is("the failure that will cause the interruption of other branches")); } + + @Test + public void testSpanContextPropagationSynchronousTestStep() throws Exception { + Set.of(EchoStep.class, EchoStep.DescriptorImpl.class, SpanContextPropagationSynchronousTestStep.class).forEach(c -> System.out.println(c + " -> " +ExtensionList.lookup(c))); + + + String pipelineScript = + "node() {\n" + + " stage('ze-stage1') {\n" + + " echo message: 'hello'\n" + + " spanContextPropagationSynchronousTestStep()\n" + + " }\n" + + "}"; + jenkinsRule.createOnlineSlave(); + + final String jobName = "test-SpanContextPropagationSynchronousTestStep-" + jobNameSuffix.incrementAndGet(); + WorkflowJob pipeline = jenkinsRule.createProject(WorkflowJob.class, jobName); + pipeline.setDefinition(new CpsFlowDefinition(pipelineScript, true)); + jenkinsRule.assertBuildStatus(Result.SUCCESS, pipeline.scheduleBuild2(0)); + + String rootSpanName = JenkinsOtelSemanticAttributes.CI_PIPELINE_RUN_ROOT_SPAN_NAME_PREFIX + jobName; + + final Tree spans = getGeneratedSpans(); + checkChainOfSpans(spans,"SpanContextPropagationTestStep.execution", "spanContextPropagationSynchronousTestStep", "Stage: ze-stage1", JenkinsOtelSemanticAttributes.AGENT_UI, "Phase: Run"); + } + @Test + public void testSpanContextPropagationSynchronousNonBlockingTestStep() throws Exception { + Set.of(EchoStep.class, EchoStep.DescriptorImpl.class, SpanContextPropagationSynchronousTestStep.class).forEach(c -> System.out.println(c + " -> " +ExtensionList.lookup(c))); + + + String pipelineScript = + "node() {\n" + + " stage('ze-stage1') {\n" + + " echo message: 'hello'\n" + + " spanContextPropagationSynchronousNonBlockingTestStep()\n" + + " }\n" + + "}"; + jenkinsRule.createOnlineSlave(); + + final String jobName = "test-SpanContextPropagationSynchronousTestStep-" + jobNameSuffix.incrementAndGet(); + WorkflowJob pipeline = jenkinsRule.createProject(WorkflowJob.class, jobName); + pipeline.setDefinition(new CpsFlowDefinition(pipelineScript, true)); + jenkinsRule.assertBuildStatus(Result.SUCCESS, pipeline.scheduleBuild2(0)); + + String rootSpanName = JenkinsOtelSemanticAttributes.CI_PIPELINE_RUN_ROOT_SPAN_NAME_PREFIX + jobName; + + final Tree spans = getGeneratedSpans(); + checkChainOfSpans(spans,"SpanContextPropagationSynchronousNonBlockingTestStep.execution", "spanContextPropagationSynchronousNonBlockingTestStep", "Stage: ze-stage1", JenkinsOtelSemanticAttributes.AGENT_UI, "Phase: Run"); + } + } diff --git a/src/test/java/io/jenkins/plugins/opentelemetry/init/StepExecutionInstrumentationInitializerTest.java b/src/test/java/io/jenkins/plugins/opentelemetry/init/StepExecutionInstrumentationInitializerTest.java new file mode 100644 index 000000000..2816d956c --- /dev/null +++ b/src/test/java/io/jenkins/plugins/opentelemetry/init/StepExecutionInstrumentationInitializerTest.java @@ -0,0 +1,19 @@ +/* + * Copyright The Original Author or Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.jenkins.plugins.opentelemetry.init; + +import io.jenkins.plugins.opentelemetry.opentelemetry.autoconfigure.ConfigPropertiesUtils; +import org.junit.Test; + +public class StepExecutionInstrumentationInitializerTest { + + @Test + public void testAfterConfiguration() { + StepExecutionInstrumentationInitializer stepExecutionInstrumentationInitializer = new StepExecutionInstrumentationInitializer(); + stepExecutionInstrumentationInitializer.afterConfiguration(ConfigPropertiesUtils.emptyConfig()); + } + +} \ No newline at end of file diff --git a/src/test/java/io/jenkins/plugins/opentelemetry/job/step/SpanContextPropagationSynchronousNonBlockingTestStep.java b/src/test/java/io/jenkins/plugins/opentelemetry/job/step/SpanContextPropagationSynchronousNonBlockingTestStep.java new file mode 100644 index 000000000..3ea55d8fe --- /dev/null +++ b/src/test/java/io/jenkins/plugins/opentelemetry/job/step/SpanContextPropagationSynchronousNonBlockingTestStep.java @@ -0,0 +1,85 @@ +/* + * Copyright The Original Author or Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.jenkins.plugins.opentelemetry.job.step; + +import hudson.Extension; +import hudson.model.TaskListener; +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.context.Scope; +import org.jenkinsci.plugins.workflow.steps.Step; +import org.jenkinsci.plugins.workflow.steps.StepContext; +import org.jenkinsci.plugins.workflow.steps.StepDescriptor; +import org.jenkinsci.plugins.workflow.steps.SynchronousNonBlockingStepExecution; +import org.jenkinsci.plugins.workflow.steps.SynchronousNonBlockingStepExecution; +import org.kohsuke.stapler.DataBoundConstructor; + +import javax.annotation.Nonnull; +import java.io.Serializable; +import java.util.Collections; +import java.util.Objects; +import java.util.Set; +import java.util.logging.Logger; + +public class SpanContextPropagationSynchronousNonBlockingTestStep extends Step implements Serializable { + private static final long serialVersionUID = 1L; + + private final static Logger logger = Logger.getLogger(SpanContextPropagationSynchronousNonBlockingTestStep.class.getName()); + transient OpenTelemetry openTelemetry = GlobalOpenTelemetry.get(); + transient Tracer tracer = openTelemetry.getTracer("io.jenkins.opentelemetry.test"); + + + @DataBoundConstructor + public SpanContextPropagationSynchronousNonBlockingTestStep() { + } + + @Override + public StepExecution start(StepContext context) throws Exception { + return new StepExecution(context); + } + + @Extension + public static class DescriptorImpl extends StepDescriptor { + @Override + public Set> getRequiredContext() { + return Collections.singleton(TaskListener.class); + } + + @Override + public String getFunctionName() { + return "spanContextPropagationSynchronousNonBlockingTestStep"; + } + + @Nonnull + @Override + public String getDisplayName() { + return "spanContextPropagationSynchronousNonBlockingTestStep"; + } + } + + private class StepExecution extends SynchronousNonBlockingStepExecution { + public StepExecution(StepContext context) { + super(context); + } + + @Override + protected Void run() throws Exception { + + Span span = tracer.spanBuilder("SpanContextPropagationSynchronousNonBlockingTestStep.execution").startSpan(); + try (Scope ctx = span.makeCurrent()) { + TaskListener taskListener = Objects.requireNonNull(getContext().get(TaskListener.class)); + taskListener.getLogger().println(getClass().getName()); + } finally { + span.end(); + } + return null; + } + + private static final long serialVersionUID = 1L; + } +} diff --git a/src/test/java/io/jenkins/plugins/opentelemetry/job/step/SpanContextPropagationSynchronousTestStep.java b/src/test/java/io/jenkins/plugins/opentelemetry/job/step/SpanContextPropagationSynchronousTestStep.java new file mode 100644 index 000000000..9c04f733b --- /dev/null +++ b/src/test/java/io/jenkins/plugins/opentelemetry/job/step/SpanContextPropagationSynchronousTestStep.java @@ -0,0 +1,81 @@ +/* + * Copyright The Original Author or Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.jenkins.plugins.opentelemetry.job.step; + +import hudson.Extension; +import hudson.model.TaskListener; +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.context.Scope; +import org.jenkinsci.plugins.workflow.steps.Step; +import org.jenkinsci.plugins.workflow.steps.StepContext; +import org.jenkinsci.plugins.workflow.steps.StepDescriptor; +import org.jenkinsci.plugins.workflow.steps.SynchronousStepExecution; +import org.kohsuke.stapler.DataBoundConstructor; + +import javax.annotation.Nonnull; +import java.util.Collections; +import java.util.Objects; +import java.util.Set; +import java.util.logging.Logger; + +public class SpanContextPropagationSynchronousTestStep extends Step { + private final static Logger logger = Logger.getLogger(SpanContextPropagationSynchronousTestStep.class.getName()); + transient OpenTelemetry openTelemetry = GlobalOpenTelemetry.get(); + transient Tracer tracer = openTelemetry.getTracer("io.jenkins.opentelemetry.test"); + + + @DataBoundConstructor + public SpanContextPropagationSynchronousTestStep() { + } + + @Override + public StepExecution start(StepContext context) throws Exception { + return new StepExecution(context); + } + + @Extension + public static class DescriptorImpl extends StepDescriptor { + @Override + public Set> getRequiredContext() { + return Collections.singleton(TaskListener.class); + } + + @Override + public String getFunctionName() { + return "spanContextPropagationSynchronousTestStep"; + } + + @Nonnull + @Override + public String getDisplayName() { + return "spanContextPropagationSynchronousTestStep"; + } + } + + private class StepExecution extends SynchronousStepExecution { + public StepExecution(StepContext context) { + super(context); + } + + @Override + protected Void run() throws Exception { + + Span span = tracer.spanBuilder("SpanContextPropagationTestStep.execution").startSpan(); + try (Scope ctx = span.makeCurrent()) { + TaskListener taskListener = Objects.requireNonNull(getContext().get(TaskListener.class)); + taskListener.getLogger().println(getClass().getName()); + } finally { + span.end(); + } + return null; + } + + private static final long serialVersionUID = 1L; + } +}