From 7ed23a3b2433b3509bdc6fe9ac108ebb5fe25520 Mon Sep 17 00:00:00 2001 From: Christos Bisias Date: Mon, 23 Sep 2024 19:57:35 +0300 Subject: [PATCH 1/4] Capability to manually create spans --- docs/job-traces.md | 46 ++- pom.xml | 5 + .../OpenTelemetryAttributesAction.java | 27 ++ .../job/MonitoringPipelineListener.java | 86 +++++ .../job/jenkins/AbstractPipelineListener.java | 10 + ...raphListenerAdapterToPipelineListener.java | 30 ++ .../job/jenkins/PipelineListener.java | 10 + .../job/jenkins/PipelineNodeUtil.java | 17 + .../job/step/SpanAttributeStepExecution.java | 21 +- .../job/step/WithNewSpanStep.java | 83 +++++ .../opentelemetry/BaseIntegrationTest.java | 48 +++ .../job/MonitoringPipelineListenerTest.java | 306 ++++++++++++++++++ .../job/step/WithNewSpanStepTest.java | 218 +++++++++++++ .../sdk/testing/trace/SpanMock.java | 181 +++++++++++ 14 files changed, 1085 insertions(+), 3 deletions(-) create mode 100644 src/main/java/io/jenkins/plugins/opentelemetry/job/step/WithNewSpanStep.java create mode 100644 src/test/java/io/jenkins/plugins/opentelemetry/job/MonitoringPipelineListenerTest.java create mode 100644 src/test/java/io/jenkins/plugins/opentelemetry/job/step/WithNewSpanStepTest.java create mode 100644 src/test/java/io/opentelemetry/sdk/testing/trace/SpanMock.java diff --git a/docs/job-traces.md b/docs/job-traces.md index b9cfe1336..fa268dfc2 100644 --- a/docs/job-traces.md +++ b/docs/job-traces.md @@ -1,7 +1,7 @@ # Traces of Jobs and Pipeline Builds -## Traces and Spans Attributes +## Traces, Spans and Span Attributes The Jenkins OpenTelemetry integration collects comprehensive contextual attributes of the jobs and pipelines builds to: * Provide build executions details in order to be an alternative to the Jenkins GUI if desired @@ -58,6 +58,50 @@ pipeline { } ```` +### Custom spans + +Custom spans can be defined using the `withNewSpan` step, which accepts the following parameters +* `label` + * the label of the span + * the value is a `string` +* `attributes` + * a list of attributes, defined the same way as in the `withSpanAttributes` step + * ```groovy + attributes: ([ + spanAttribute(key: 'modules', value: '2'), + spanAttribute(key: 'command', value: 'mvn clean install') + ]) + ``` +* `setAttributesOnlyOnParent` + * flag used to define whether to inherit the provided attributes to the children spans or not + * `true` by default, all user-defined attributes for a span are passed on to children spans + * the value is a boolean, `true` or `false` + +Example definitions: + +* All parameters provided + ```groovy + stage('build') { + withNewSpan(label: 'custom-build-span', attributes: ([ + spanAttribute(key: 'modules', value: '2'), + spanAttribute(key: 'command', value: 'mvn clean install') + ]), setAttributesOnlyOnParent: true) { + sh './build-module1.sh' + sh './build-module2.sh' + } + } + ``` + +* Only the `label` parameter is required, all others are optional. + ```groovy + stage('build') { + withNewSpan(label: 'custom-build-span') { + sh './build-module1.sh' + sh './build-module2.sh' + } + } + ``` + ### Pipeline, freestyle, and matrix project build spans Attributes reported on the root span of Jenkins job and pipeline builds: diff --git a/pom.xml b/pom.xml index ab6e8a240..1fa693265 100644 --- a/pom.xml +++ b/pom.xml @@ -457,6 +457,11 @@ junit test + + org.mockito + mockito-core + test + com.github.rutledgepaulv prune diff --git a/src/main/java/io/jenkins/plugins/opentelemetry/OpenTelemetryAttributesAction.java b/src/main/java/io/jenkins/plugins/opentelemetry/OpenTelemetryAttributesAction.java index 13d099dc6..9fe415e54 100644 --- a/src/main/java/io/jenkins/plugins/opentelemetry/OpenTelemetryAttributesAction.java +++ b/src/main/java/io/jenkins/plugins/opentelemetry/OpenTelemetryAttributesAction.java @@ -12,8 +12,10 @@ import edu.umd.cs.findbugs.annotations.NonNull; import java.io.Serializable; +import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; +import java.util.List; import java.util.Map; import java.util.Set; import java.util.logging.Logger; @@ -31,6 +33,10 @@ public class OpenTelemetryAttributesAction extends InvisibleAction implements Se private transient Map, Object> attributes; private transient Set appliedToSpans; + // If the list has any values, then only the spans on the list will get attributes. + // If the list is empty, then there is no restriction. + // Used to control attribute inheritance to children spans. + private transient List allowedSpanIdList; @NonNull public Map, Object> getAttributes() { @@ -52,6 +58,27 @@ public boolean isNotYetAppliedToSpan(String spanId) { return appliedToSpans.add(spanId); } + public void addSpanIdToAllowedList(String spanId) { + if (allowedSpanIdList == null) { + allowedSpanIdList = new ArrayList<>(); + } + allowedSpanIdList.add(spanId); + } + + public boolean spanIdAllowedListIsEmpty() { + if (allowedSpanIdList == null) { + return true; + } + return allowedSpanIdList.isEmpty(); + } + + public boolean isSpanIdAllowed(String spanId) { + if (allowedSpanIdList == null) { + return false; + } + return allowedSpanIdList.contains(spanId); + } + @Override public String toString() { return "OpenTelemetryAttributesAction{" + 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 27feb317d..dd3dd8070 100644 --- a/src/main/java/io/jenkins/plugins/opentelemetry/job/MonitoringPipelineListener.java +++ b/src/main/java/io/jenkins/plugins/opentelemetry/job/MonitoringPipelineListener.java @@ -5,6 +5,7 @@ package io.jenkins.plugins.opentelemetry.job; +import com.google.common.base.Strings; import com.google.errorprone.annotations.MustBeClosed; import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; @@ -22,6 +23,7 @@ import io.jenkins.plugins.opentelemetry.job.jenkins.AbstractPipelineListener; import io.jenkins.plugins.opentelemetry.job.jenkins.PipelineListener; import io.jenkins.plugins.opentelemetry.job.step.SetSpanAttributesStep; +import io.jenkins.plugins.opentelemetry.job.step.SpanAttribute; import io.jenkins.plugins.opentelemetry.job.step.StepHandler; import io.jenkins.plugins.opentelemetry.job.step.WithSpanAttributeStep; import io.jenkins.plugins.opentelemetry.job.step.WithSpanAttributesStep; @@ -267,6 +269,19 @@ private String getStepName(@NonNull StepAtomNode node, @NonNull String name) { return stepDescriptor.getDisplayName(); } + private String getStepName(@NonNull StepStartNode node, @NonNull String name) { + StepDescriptor stepDescriptor = node.getDescriptor(); + if (stepDescriptor == null) { + return name; + } + UninstantiatedDescribable describable = getUninstantiatedDescribableOrNull(node, stepDescriptor); + if (describable != null) { + Descriptor d = SymbolLookup.get().findDescriptor(Describable.class, describable.getSymbol()); + return d.getDisplayName(); + } + return stepDescriptor.getDisplayName(); + } + private String getStepType(@NonNull FlowNode node, @Nullable StepDescriptor stepDescriptor, @NonNull String type) { if (stepDescriptor == null) { return type; @@ -374,6 +389,66 @@ private void endCurrentSpan(FlowNode node, WorkflowRun run, GenericStatus status } } + @Override + public void onStartWithNewSpanStep(@NonNull StepStartNode stepStartNode, @NonNull WorkflowRun run) { + try (Scope ignored = setupContext(run, stepStartNode)) { + verifyNotNull(ignored, "%s - No span found for node %s", run, stepStartNode); + + String stepName = getStepName(stepStartNode, "withNewSpan"); + String stepType = getStepType(stepStartNode, stepStartNode.getDescriptor(), "step"); + JenkinsOpenTelemetryPluginConfiguration.StepPlugin stepPlugin = JenkinsOpenTelemetryPluginConfiguration.get().findStepPluginOrDefault(stepType, stepStartNode); + + // Get the arguments. + final Map arguments = ArgumentsAction.getFilteredArguments(stepStartNode); + // Argument 'label'. + final String spanLabelArgument = (String) arguments.getOrDefault("label", stepName); + final String spanLabel = Strings.isNullOrEmpty(spanLabelArgument) ? stepName : spanLabelArgument; + SpanBuilder spanBuilder = getTracer().spanBuilder(spanLabel) + .setParent(Context.current()) + .setAttribute(JenkinsOtelSemanticAttributes.JENKINS_STEP_TYPE, stepType) + .setAttribute(JenkinsOtelSemanticAttributes.JENKINS_STEP_ID, stepStartNode.getId()) + .setAttribute(JenkinsOtelSemanticAttributes.JENKINS_STEP_NAME, stepName) + .setAttribute(JenkinsOtelSemanticAttributes.JENKINS_STEP_PLUGIN_NAME, stepPlugin.getName()) + .setAttribute(JenkinsOtelSemanticAttributes.JENKINS_STEP_PLUGIN_VERSION, stepPlugin.getVersion()); + + // Populate the attributes if any 'attributes' argument was passed to the 'withNewSpan' step. + try { + Object attributesObj = arguments.getOrDefault("attributes", Collections.emptyList()); + if (attributesObj instanceof List) { + // Filter the list items and cast to SpanAttribute. + List attributes = ((List) attributesObj).stream() + .filter(item -> item instanceof SpanAttribute) + .map(item -> (SpanAttribute) item) + .collect(Collectors.toList()); + + for (SpanAttribute attribute : attributes) { + // Set the attributeType in case it's not there. + attributes.forEach(SpanAttribute::setDefaultType); + // attributeKey is null, call convert() to set the appropriate key value + // and convert the attribute value. + attribute.convert(); + spanBuilder.setAttribute(attribute.getAttributeKey(), attribute.getConvertedValue()); + } + } else { + LOGGER.log(Level.WARNING, "Attributes are in an unexpected format: " + attributesObj.getClass().getSimpleName()); + } + } catch (ClassCastException cce) { + LOGGER.log(Level.WARNING, run.getFullDisplayName() + " failure to gather the attributes for the 'withNewSpan' step.", cce); + } + + Span newSpan = spanBuilder.startSpan(); + LOGGER.log(Level.FINE, () -> run.getFullDisplayName() + " - > " + stepStartNode.getDisplayFunctionName() + " - begin " + OtelUtils.toDebugString(newSpan)); + getTracerService().putSpan(run, newSpan, stepStartNode); + } + } + + @Override + public void onEndWithNewSpanStep(@NonNull StepEndNode node, FlowNode nextNode, @NonNull WorkflowRun run) { + StepStartNode nodeStartNode = node.getStartNode(); + GenericStatus nodeStatus = StatusAndTiming.computeChunkStatus2(run, null, nodeStartNode, node, nextNode); + endCurrentSpan(node, run, nodeStatus); + } + @Override public void notifyOfNewStep(@NonNull Step step, @NonNull StepContext context) { try { @@ -416,6 +491,17 @@ private void setAttributesToSpan(@NonNull Span span, OpenTelemetryAttributesActi if (openTelemetryAttributesAction == null) { return; } + + // If the list is empty, ignore this check. + if (!openTelemetryAttributesAction.spanIdAllowedListIsEmpty() && + !openTelemetryAttributesAction.isSpanIdAllowed(span.getSpanContext().getSpanId())) { + // If the list isn't empty, then the attributes shouldn't be set on children spans. + // Attributes should only be set on Ids from the list. + // If there are Ids on the list but the provided Id isn't part of them, + // don't set attributes on the span. + return; + } + if (!openTelemetryAttributesAction.isNotYetAppliedToSpan(span.getSpanContext().getSpanId())) { // Do not reapply attributes, if previously applied. // This is important for overriding of attributes to work in an intuitive manner. diff --git a/src/main/java/io/jenkins/plugins/opentelemetry/job/jenkins/AbstractPipelineListener.java b/src/main/java/io/jenkins/plugins/opentelemetry/job/jenkins/AbstractPipelineListener.java index 1bb89d91b..756052558 100644 --- a/src/main/java/io/jenkins/plugins/opentelemetry/job/jenkins/AbstractPipelineListener.java +++ b/src/main/java/io/jenkins/plugins/opentelemetry/job/jenkins/AbstractPipelineListener.java @@ -45,6 +45,16 @@ public void onEndStageStep(@NonNull StepEndNode stageStepEndNode, @NonNull Strin } + @Override + public void onStartWithNewSpanStep(@NonNull StepStartNode stepStartNode, @NonNull WorkflowRun run) { + + } + + @Override + public void onEndWithNewSpanStep(@NonNull StepEndNode nodeStepEndNode, FlowNode nextNode, @NonNull WorkflowRun run) { + + } + @Override public void onAtomicStep(@NonNull StepAtomNode node, @NonNull WorkflowRun run) { diff --git a/src/main/java/io/jenkins/plugins/opentelemetry/job/jenkins/GraphListenerAdapterToPipelineListener.java b/src/main/java/io/jenkins/plugins/opentelemetry/job/jenkins/GraphListenerAdapterToPipelineListener.java index b8fba7d2b..7b5ae9069 100644 --- a/src/main/java/io/jenkins/plugins/opentelemetry/job/jenkins/GraphListenerAdapterToPipelineListener.java +++ b/src/main/java/io/jenkins/plugins/opentelemetry/job/jenkins/GraphListenerAdapterToPipelineListener.java @@ -67,6 +67,8 @@ private void processPreviousNodes(FlowNode node, WorkflowRun run) { StepStartNode beginParallelBranch = endParallelBranchNode.getStartNode(); ThreadNameAction persistentAction = verifyNotNull(beginParallelBranch.getPersistentAction(ThreadNameAction.class), "Null ThreadNameAction on %s", beginParallelBranch); fireOnAfterEndParallelStepBranch(endParallelBranchNode, persistentAction.getThreadName(), node, run); + } else if (isBeforeEndWithNewSpanStep(previousNode)) { + fireOnAfterEndWithNewSpanStep((StepEndNode) previousNode, node, run); } else { log(Level.FINE, () -> "Ignore previous node " + PipelineNodeUtil.getDetailedDebugString(previousNode)); } @@ -98,6 +100,8 @@ private void processCurrentNode(FlowNode node, WorkflowRun run) { final Map arguments = ArgumentsAction.getFilteredArguments(node); String label = Objects.toString(arguments.get("label"), null); fireOnAfterStartNodeStep((StepStartNode) node, label, run); + } else if (PipelineNodeUtil.isStartWithNewSpan(node)) { + fireOnBeforeWithNewSpanStep((StepStartNode) node, run); } else { logFlowNodeDetails(node, run); } @@ -212,6 +216,28 @@ public void fireOnAfterEndStageStep(@NonNull StepEndNode node, @NonNull String s } } + public void fireOnBeforeWithNewSpanStep(@NonNull StepStartNode node, @NonNull WorkflowRun run) { + for (PipelineListener pipelineListener : PipelineListener.all()) { + log(() -> "onBeforeWithNewSpanStep(" + node.getDisplayName() + "): " + pipelineListener.toString()); + try { + pipelineListener.onStartWithNewSpanStep(node, run); + } catch (RuntimeException e) { + LOGGER.log(Level.WARNING, e, () -> "Exception invoking `onBeforeWithNewSpanStep` on " + pipelineListener); + } + } + } + + public void fireOnAfterEndWithNewSpanStep(@NonNull StepEndNode node, FlowNode nextNode, @NonNull WorkflowRun run) { + for (PipelineListener pipelineListener : PipelineListener.all()) { + log(() -> "onAfterEndWithNewSpanStep(" + node.getDisplayName() + "): " + pipelineListener.toString() + (node.getError() != null ? ("error: " + node.getError().getError()) : "")); + try { + pipelineListener.onEndWithNewSpanStep(node, nextNode, run); + } catch (RuntimeException e) { + LOGGER.log(Level.WARNING, e, () -> "Exception invoking `onAfterEndWithNewSpanStep` on " + pipelineListener); + } + } + } + public void fireOnBeforeAtomicStep(@NonNull StepAtomNode node, @NonNull WorkflowRun run) { for (PipelineListener pipelineListener : PipelineListener.all()) { log(() -> "onBeforeAtomicStep(" + node.getDisplayName() + "): " + pipelineListener.toString()); @@ -268,6 +294,10 @@ private boolean isBeforeEndParallelBranch(@NonNull FlowNode node) { return (node instanceof StepEndNode) && PipelineNodeUtil.isStartParallelBranch(((StepEndNode) node).getStartNode()); } + private boolean isBeforeEndWithNewSpanStep(@NonNull FlowNode node) { + return (node instanceof StepEndNode) && PipelineNodeUtil.isStartWithNewSpan(((StepEndNode) node).getStartNode()); + } + protected void log(@NonNull Supplier message) { log(Level.FINE, message); } diff --git a/src/main/java/io/jenkins/plugins/opentelemetry/job/jenkins/PipelineListener.java b/src/main/java/io/jenkins/plugins/opentelemetry/job/jenkins/PipelineListener.java index ea8175011..2d17b3f97 100644 --- a/src/main/java/io/jenkins/plugins/opentelemetry/job/jenkins/PipelineListener.java +++ b/src/main/java/io/jenkins/plugins/opentelemetry/job/jenkins/PipelineListener.java @@ -63,6 +63,16 @@ static List all() { */ void onEndParallelStepBranch(@NonNull StepEndNode stepStepNode, @NonNull String branchName, FlowNode nextNode, @NonNull WorkflowRun run); + /** + * Just before the `withNewSpan` step starts + */ + void onStartWithNewSpanStep(@NonNull StepStartNode stepStartNode, @NonNull WorkflowRun run); + + /** + * Just before the `withNewSpan` step ends + */ + void onEndWithNewSpanStep(@NonNull StepEndNode nodeStepEndNode, FlowNode nextNode, @NonNull WorkflowRun run); + /** * Just before the atomic step starts */ diff --git a/src/main/java/io/jenkins/plugins/opentelemetry/job/jenkins/PipelineNodeUtil.java b/src/main/java/io/jenkins/plugins/opentelemetry/job/jenkins/PipelineNodeUtil.java index 0740193f6..f9e6c420c 100644 --- a/src/main/java/io/jenkins/plugins/opentelemetry/job/jenkins/PipelineNodeUtil.java +++ b/src/main/java/io/jenkins/plugins/opentelemetry/job/jenkins/PipelineNodeUtil.java @@ -8,10 +8,12 @@ import com.google.common.collect.Iterables; import hudson.model.Action; import hudson.model.Queue; +import io.jenkins.plugins.opentelemetry.job.step.WithNewSpanStep; import org.apache.commons.lang.StringUtils; import org.jenkinsci.plugins.pipeline.StageStatus; import org.jenkinsci.plugins.pipeline.SyntheticStage; import org.jenkinsci.plugins.workflow.actions.*; +import org.jenkinsci.plugins.workflow.cps.actions.ArgumentsActionImpl; import org.jenkinsci.plugins.workflow.cps.nodes.StepEndNode; import org.jenkinsci.plugins.workflow.cps.nodes.StepStartNode; import org.jenkinsci.plugins.workflow.cps.steps.ParallelStep; @@ -57,6 +59,21 @@ public static boolean isStartStage(FlowNode node) { return node.getAction(LabelAction.class) != null; } + public static boolean isStartWithNewSpan(FlowNode node) { + if (node == null) { + return false; + } + + if (!(node instanceof StepStartNode)) { + return false; + } + StepStartNode stepStartNode = (StepStartNode) node; + if (!(stepStartNode.getDescriptor() instanceof WithNewSpanStep.DescriptorImpl)) { + return false; + } + return node.getAction(ArgumentsActionImpl.class) != null; + } + /** * copy of {@code io.jenkins.blueocean.rest.impl.pipeline.PipelineNodeUtil} */ diff --git a/src/main/java/io/jenkins/plugins/opentelemetry/job/step/SpanAttributeStepExecution.java b/src/main/java/io/jenkins/plugins/opentelemetry/job/step/SpanAttributeStepExecution.java index cf38f446a..ea00cb04a 100644 --- a/src/main/java/io/jenkins/plugins/opentelemetry/job/step/SpanAttributeStepExecution.java +++ b/src/main/java/io/jenkins/plugins/opentelemetry/job/step/SpanAttributeStepExecution.java @@ -30,10 +30,20 @@ public class SpanAttributeStepExecution extends GeneralNonBlockingStepExecution private final boolean setOnChildren; + private final boolean setAttributesOnlyOnParent; + public SpanAttributeStepExecution(List spanAttributes, boolean setOnChildren, StepContext context) { super(context); this.spanAttributes = spanAttributes; this.setOnChildren = setOnChildren; + this.setAttributesOnlyOnParent = false; + } + + public SpanAttributeStepExecution(List spanAttributes, boolean setOnChildren, StepContext context, boolean setAttributesOnlyOnParent) { + super(context); + this.spanAttributes = spanAttributes; + this.setOnChildren = setOnChildren; + this.setAttributesOnlyOnParent = setAttributesOnlyOnParent; } @Override @@ -58,6 +68,7 @@ protected Void run() throws Exception { OtelTraceService otelTraceService = ExtensionList.lookupSingleton(OtelTraceService.class); Run run = getContext().get(Run.class); FlowNode flowNode = getContext().get(FlowNode.class); + String currentSpanId = otelTraceService.getSpan(run, flowNode).getSpanContext().getSpanId(); spanAttributes.forEach(spanAttribute -> { switch (spanAttribute.getTarget()) { case PIPELINE_ROOT_SPAN: @@ -100,7 +111,7 @@ protected Void run() throws Exception { }); getContext() .newBodyInvoker() - .withContext(mergeAttributes(getContext(), spanAttributes)) + .withContext(mergeAttributes(getContext(), spanAttributes, currentSpanId)) .withCallback(NopCallback.INSTANCE) .start(); } @@ -117,7 +128,7 @@ private void addAttributeToRunAction(Actionable actionable, AttributeKey attribu openTelemetryAttributesAction.getAttributes().put(attributeKey, convertedValue); } - private OpenTelemetryAttributesAction mergeAttributes(StepContext context, List spanAttributes) throws IOException, InterruptedException { + private OpenTelemetryAttributesAction mergeAttributes(StepContext context, List spanAttributes, String currentSpanId) throws IOException, InterruptedException { OpenTelemetryAttributesAction existingAttributes = context.get(OpenTelemetryAttributesAction.class); OpenTelemetryAttributesAction resultingAttributes = new OpenTelemetryAttributesAction(); if (existingAttributes != null) { @@ -126,6 +137,12 @@ private OpenTelemetryAttributesAction mergeAttributes(StepContext context, List< spanAttributes.forEach(spanAttribute -> resultingAttributes.getAttributes().put(spanAttribute.getAttributeKey(), spanAttribute.getConvertedValue()) ); + + if (setAttributesOnlyOnParent) { + // If the flag is set to true, then only the current span will get the attributes. + // This will prevent any children from inheriting the attributes of the parent span. + resultingAttributes.addSpanIdToAllowedList(currentSpanId); + } return resultingAttributes; } diff --git a/src/main/java/io/jenkins/plugins/opentelemetry/job/step/WithNewSpanStep.java b/src/main/java/io/jenkins/plugins/opentelemetry/job/step/WithNewSpanStep.java new file mode 100644 index 000000000..b40c7d911 --- /dev/null +++ b/src/main/java/io/jenkins/plugins/opentelemetry/job/step/WithNewSpanStep.java @@ -0,0 +1,83 @@ +/* + * 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 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.StepExecution; +import org.kohsuke.stapler.DataBoundConstructor; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Set; + +public class WithNewSpanStep extends Step { + + private final String label; + private final List attributes; + private final boolean setAttributesOnlyOnParent; + + @DataBoundConstructor + public WithNewSpanStep(String label, List attributes, Boolean setAttributesOnlyOnParent) { + this.label = label; + // Allow empty attributes. + this.attributes = attributes == null ? new ArrayList<>() : attributes; + // Set to 'false', if no value is provided. + this.setAttributesOnlyOnParent = setAttributesOnlyOnParent != null && setAttributesOnlyOnParent; + } + + public String getLabel() { + return label; + } + + public List getAttributes() { + return attributes; + } + + public boolean isSetAttributesOnlyOnParent() { + return setAttributesOnlyOnParent; + } + + @Override + public DescriptorImpl getDescriptor() { + return (DescriptorImpl)super.getDescriptor(); + } + + @Override + public StepExecution start(StepContext context) throws Exception { + // Set AttributeType for any provided attributes, to avoid an exception if null. + attributes.forEach(SpanAttribute::setDefaultType); + + return new SpanAttributeStepExecution(attributes, context.hasBody(), context, setAttributesOnlyOnParent); + } + + @Extension + public static class DescriptorImpl extends StepDescriptor { + @Override + public String getFunctionName() { + return "withNewSpan"; + } + + @Override + public boolean takesImplicitBlockArgument() { + return true; + } + + @Override + public String getDisplayName() { + return "Step with a new user-defined Span"; + } + + @Override + public Set> getRequiredContext() { + return Collections.singleton(TaskListener.class); + } + } +} diff --git a/src/test/java/io/jenkins/plugins/opentelemetry/BaseIntegrationTest.java b/src/test/java/io/jenkins/plugins/opentelemetry/BaseIntegrationTest.java index 2e6193f5b..9127eb54b 100644 --- a/src/test/java/io/jenkins/plugins/opentelemetry/BaseIntegrationTest.java +++ b/src/test/java/io/jenkins/plugins/opentelemetry/BaseIntegrationTest.java @@ -41,9 +41,12 @@ import org.jvnet.hudson.test.BuildWatcher; import edu.umd.cs.findbugs.annotations.NonNull; + +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Comparator; +import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; @@ -101,6 +104,15 @@ public static void beforeClass() throws Exception { jenkinsRule.waitUntilNoActivity(); LOGGER.log(Level.INFO, "Jenkins started"); + // Update all sites to reload available plugins. + jenkinsRule.jenkins.getUpdateCenter().updateAllSites(); + + // install() returns a Future for every plugin. Call get() on the Future so that this line blocks + // until the operation is finished and the future is available. + jenkinsRule.jenkins.getPluginManager().install( + Collections.singletonList("durable-task"), true).get(0).get(); + Assert.assertNotNull(jenkinsRule.jenkins.getPluginManager().getPlugin("durable-task")); + ExtensionList jenkinsOpenTelemetries = jenkinsRule.getInstance().getExtensionList(JenkinsControllerOpenTelemetry.class); verify(jenkinsOpenTelemetries.size() == 1, "Number of jenkinsControllerOpenTelemetrys: %s", jenkinsOpenTelemetries.size()); jenkinsControllerOpenTelemetry = jenkinsOpenTelemetries.get(0); @@ -253,6 +265,42 @@ protected void assertBuildStepMetadata(Tree spans, String stepN MatcherAssert.assertThat(attributes.get(JenkinsOtelSemanticAttributes.JENKINS_STEP_PLUGIN_VERSION), CoreMatchers.is(CoreMatchers.notNullValue())); } + /** + * This method is used for testing that the correct spans and their children have been generated. + * It returns a map, + * - key: span name + * - value: list of children span names + */ + protected Map> getSpanMapWithChildrenFromTree(Tree spansTree) { + Map> spansTreeMap = new HashMap<>(); + // Stream all the nodes. + spansTree.breadthFirstStreamNodes().forEach(node -> { + String parentSpanName = node.getData().spanData.getName(); + List childrenSpanNames = new ArrayList<>(); + + // Get the children and for each one, store the name. + node.getChildren().forEach(child -> childrenSpanNames.add(child.getData().spanData.getName())); + + // Put the span and its children on the map. + spansTreeMap.put(parentSpanName, childrenSpanNames); + }); + return spansTreeMap; + } + + /** + * This method is used for testing a span's data like attributes. + * It returns a map, + * - key: span name + * - value: SpanData + */ + protected Map getSpanDataMapFromTree(Tree spansTree) { + Map spansTreeMap = new HashMap<>(); + // Stream all the nodes. + spansTree.breadthFirstStreamNodes().forEach(node -> + spansTreeMap.put(node.getData().spanData.getName(), node.getData().spanData)); + return spansTreeMap; + } + // https://github.com/jenkinsci/workflow-multibranch-plugin/blob/master/src/test/java/org/jenkinsci/plugins/workflow/multibranch/WorkflowMultiBranchProjectTest.java @NonNull public static WorkflowJob scheduleAndFindBranchProject(@NonNull WorkflowMultiBranchProject mp, @NonNull String name) throws Exception { diff --git a/src/test/java/io/jenkins/plugins/opentelemetry/job/MonitoringPipelineListenerTest.java b/src/test/java/io/jenkins/plugins/opentelemetry/job/MonitoringPipelineListenerTest.java new file mode 100644 index 000000000..80527697d --- /dev/null +++ b/src/test/java/io/jenkins/plugins/opentelemetry/job/MonitoringPipelineListenerTest.java @@ -0,0 +1,306 @@ +/* + * Copyright The Original Author or Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.jenkins.plugins.opentelemetry.job; + +import hudson.ExtensionList; +import hudson.model.Computer; +import io.jenkins.plugins.opentelemetry.JenkinsControllerOpenTelemetry; +import io.jenkins.plugins.opentelemetry.OpenTelemetryAttributesAction; +import io.jenkins.plugins.opentelemetry.job.step.SpanAttribute; +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.ImplicitContextKeyed; +import io.opentelemetry.context.Scope; +import io.opentelemetry.sdk.testing.trace.SpanBuilderMock; +import io.opentelemetry.sdk.testing.trace.SpanMock; +import io.opentelemetry.sdk.testing.trace.TracerMock; +import jenkins.model.Jenkins; +import org.jenkinsci.plugins.workflow.actions.ArgumentsAction; +import org.jenkinsci.plugins.workflow.cps.nodes.StepStartNode; +import org.jenkinsci.plugins.workflow.graph.FlowNode; +import org.jenkinsci.plugins.workflow.job.WorkflowRun; +import org.jenkinsci.plugins.workflow.steps.Step; +import org.jenkinsci.plugins.workflow.steps.StepContext; +import org.jetbrains.annotations.NotNull; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.ClassRule; +import org.junit.Test; + +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Suite; +import org.jvnet.hudson.test.JenkinsRule; +import org.mockito.MockedStatic; +import org.mockito.Mockito; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static com.google.common.base.Verify.verify; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.when; + +/** + * Unit tests for {@link MonitoringPipelineListener}. + * Using subclasses so that the non-parameterized tests + * don't have to be run once for every parameter. + */ +@RunWith(Suite.class) +@Suite.SuiteClasses({ + MonitoringPipelineListenerTest.ParamTests.class, + MonitoringPipelineListenerTest.NonParamTests.class +}) +public class MonitoringPipelineListenerTest { + + @ClassRule + public static final JenkinsRule jenkinsRule = new JenkinsRule(); + + private static final StepContext stepContext = Mockito.mock(StepContext.class); + private static final OtelTraceService otelTraceService = Mockito.mock(OtelTraceService.class); + + private static MonitoringPipelineListener monitoringPipelineListener; + private static WorkflowRun workflowRun; + + @BeforeClass + public static void commonSetup() throws IOException, InterruptedException { + // Jenkins must have been initialized. + Assert.assertNotNull(Jenkins.getInstanceOrNull()); + + workflowRun = Mockito.mock(WorkflowRun.class); + + Mockito.when(stepContext.get(WorkflowRun.class)).thenReturn(workflowRun); + } + + @RunWith(Parameterized.class) + public static class ParamTests { + + private static final String START_NODE_ROOT_SPAN_NAME = "root-span"; + private static final String WITH_NEW_SPAN_NAME = "with-new-span"; + SpanBuilderMock spanBuilderMock = Mockito.spy(new SpanBuilderMock(WITH_NEW_SPAN_NAME)); + + @Parameterized.Parameter(0) + public String attributeKeyName; + + @Parameterized.Parameter(1) + public Object attributeKeyObj; + + @Parameterized.Parameter(2) + public Object attributeValueObj; + + @Parameterized.Parameters + public static Collection argumentsActionScenarios() { + return Arrays.asList(new Object[][] { + { "with.new.span.boolean", AttributeKey.booleanKey("with.new.span.boolean"), true }, + { "with.new.span.string", AttributeKey.stringKey("with.new.span.string"), "true" }, + { "with.new.span.long", AttributeKey.longKey("with.new.span.long"), 2L }, + { "with.new.span.double", AttributeKey.doubleKey("with.new.span.double"), 2.22 }, + }); + } + + @Before + public void setup() { + ExtensionList jenkinsOpenTelemetries = jenkinsRule.getInstance().getExtensionList(JenkinsControllerOpenTelemetry.class); + verify(jenkinsOpenTelemetries.size() == 1, "Number of jenkinsControllerOpenTelemetrys: %s", jenkinsOpenTelemetries.size()); + JenkinsControllerOpenTelemetry jenkinsControllerOpenTelemetry = Mockito.spy(jenkinsOpenTelemetries.get(0)); + + Tracer tracer = Mockito.spy(new TracerMock()); + + Mockito.when(tracer.spanBuilder(WITH_NEW_SPAN_NAME)).thenReturn(spanBuilderMock); + + Mockito.when(jenkinsControllerOpenTelemetry.getDefaultTracer()).thenReturn(tracer); + + monitoringPipelineListener = new MonitoringPipelineListener(); + monitoringPipelineListener.jenkinsControllerOpenTelemetry = jenkinsControllerOpenTelemetry; + + Assert.assertNull(monitoringPipelineListener.getTracer()); + // postConstruct() calls the getDefaultTracer() method which needs to be stubbed in advance before using the tracer. + // Manually invoke the postConstruct() method to re-apply the @PostConstruct logic. + monitoringPipelineListener.postConstruct(); + + Assert.assertNotNull(monitoringPipelineListener.getTracer()); + + monitoringPipelineListener.setOpenTelemetryTracerService(otelTraceService); + } + + @Test + public void testOnStartWithNewSpanStep() { + + StepStartNode stepStartNode = Mockito.mock(StepStartNode.class); + ArgumentsAction action = new ArgumentsAction() { + @NotNull + @Override + protected Map getArgumentsInternal() { + Map map = new HashMap<>(); + map.put("label", WITH_NEW_SPAN_NAME); + List spanAttributes = new ArrayList<>(); + SpanAttribute spanAttribute = new SpanAttribute(attributeKeyName, attributeValueObj, null, null); + spanAttributes.add(spanAttribute); + map.put("attributes", spanAttributes); + return map; + } + }; + Mockito.when(stepStartNode.getPersistentAction(ArgumentsAction.class)).thenReturn(action); + + Scope scope = Mockito.mock(Scope.class); + + SpanMock rootSpan = Mockito.spy(new SpanMock(START_NODE_ROOT_SPAN_NAME)); + + Mockito.when(rootSpan.makeCurrent()).thenReturn(scope); + + Mockito.when(otelTraceService.getSpan(workflowRun)).thenReturn(rootSpan); + Mockito.when(otelTraceService.getSpan(workflowRun, stepStartNode)).thenReturn(rootSpan); + + monitoringPipelineListener.setOpenTelemetryTracerService(otelTraceService); + Assert.assertNotNull(monitoringPipelineListener.getTracerService().getSpan(workflowRun)); + + SpanMock newSpan = Mockito.spy(new SpanMock(WITH_NEW_SPAN_NAME)); + Mockito.when(monitoringPipelineListener.getTracer().spanBuilder(WITH_NEW_SPAN_NAME).startSpan()).thenReturn(newSpan); + + try (MockedStatic mockedStaticSpan = mockStatic(Span.class); + MockedStatic mockedStaticContext = mockStatic(Context.class)) { + // Span.current() should return the mocked span. + mockedStaticSpan.when(Span::current).thenReturn(rootSpan); + Assert.assertEquals(rootSpan, Span.current()); + + mockedStaticSpan.when(() -> Span.fromContext(any())).thenReturn(rootSpan); + + Context context = Mockito.mock(Context.class); + when(context.with(any(ImplicitContextKeyed.class))).thenReturn(context); + mockedStaticContext.when(Context::current).thenReturn(context); + + // The span builder shouldn't have any attributes. + Assert.assertEquals(0, spanBuilderMock.getAttributes().size()); + Assert.assertFalse(spanBuilderMock.getAttributes().containsKey(attributeKeyObj)); + + monitoringPipelineListener.onStartWithNewSpanStep(stepStartNode, workflowRun); + + // After the onStartWithNewSpanStep() call, the spanBuilder should contain the attribute. + Assert.assertTrue(spanBuilderMock.getAttributes().containsKey(attributeKeyObj)); + Assert.assertEquals(attributeValueObj, spanBuilderMock.getAttributes().get(attributeKeyObj)); + } + } + } + + public static class NonParamTests { + + private static final String TEST_SPAN_NAME = "test-span"; + private static final String SH_STEP_SPAN_NAME = "sh-span"; + private static final FlowNode flowNode = Mockito.mock(FlowNode.class); + private static SpanMock testSpan; + + @Before + public void setup() throws IOException, InterruptedException { + testSpan = new SpanMock(TEST_SPAN_NAME); + testSpan.setAttribute("caller.name", "testuser"); + + Mockito.when(stepContext.get(FlowNode.class)).thenReturn(flowNode); + + Mockito.when(otelTraceService.getSpan(workflowRun)).thenReturn(testSpan); + Mockito.when(otelTraceService.getSpan(workflowRun, flowNode)).thenReturn(testSpan); + } + + @After + public void cleanup() { + testSpan.end(); + } + + private void setupAttributesActionStubs(List allowedIds) throws IOException, InterruptedException { + Computer computer = Mockito.mock(Computer.class); + + Mockito.when(stepContext.get(Computer.class)).thenReturn(computer); + + // Computer AttributesAction stub. + OpenTelemetryAttributesAction otelComputerAttributesAction = new OpenTelemetryAttributesAction(); + otelComputerAttributesAction.getAttributes().put(AttributeKey.stringKey("attribute.from.computer.action.applied"), "true"); + Mockito.when(computer.getAction(OpenTelemetryAttributesAction.class)).thenReturn(otelComputerAttributesAction); + + // Child AttributesAction stub. + OpenTelemetryAttributesAction otelChildAttributesAction = new OpenTelemetryAttributesAction(); + + for (String id : allowedIds) { + otelChildAttributesAction.addSpanIdToAllowedList(id); + } + + otelChildAttributesAction.getAttributes().put(AttributeKey.stringKey("attribute.from.child.action.applied"), "true"); + Mockito.when(stepContext.get(OpenTelemetryAttributesAction.class)).thenReturn(otelChildAttributesAction); + } + + @Test + public void testSetAttributesToSpan() throws IOException, InterruptedException { + // Pass an empty list. All spans are allowed to get attributes. + setupAttributesActionStubs(new ArrayList<>()); + + try (MockedStatic mockedStatic = mockStatic(Span.class)) { + // Span.current() should return the mocked span. + mockedStatic.when(Span::current).thenReturn(testSpan); + Assert.assertEquals(testSpan, Span.current()); + + // The span should contain only 1 attribute. + Assert.assertEquals(1, testSpan.getAttributes().keySet().size()); + Assert.assertTrue(testSpan.getAttributes().containsKey(AttributeKey.stringKey("caller.name"))); + Assert.assertEquals("testuser", testSpan.getAttributes().get(AttributeKey.stringKey("caller.name"))); + + Step step = Mockito.mock(Step.class); + monitoringPipelineListener.notifyOfNewStep(step, stepContext); + + // The span should now contain the computer and child action attributes as well. + Assert.assertEquals(3, testSpan.getAttributes().keySet().size()); + Assert.assertTrue(testSpan.getAttributes().containsKey(AttributeKey.stringKey("caller.name"))); + Assert.assertEquals("testuser", testSpan.getAttributes().get(AttributeKey.stringKey("caller.name"))); + + for (String component : Arrays.asList("computer", "child")) { + AttributeKey attributeKey = AttributeKey.stringKey("attribute.from." + component + ".action.applied"); + Assert.assertTrue(testSpan.getAttributes().containsKey(attributeKey)); + Assert.assertEquals("true", testSpan.getAttributes().get(attributeKey)); + } + } + } + + @Test + public void testSetAttributesToSpanWithNotAllowedSpanId() throws IOException, InterruptedException { + String testSpanId = testSpan.getSpanContext().getSpanId(); + setupAttributesActionStubs(Collections.singletonList(testSpanId)); + + SpanMock shSpan = new SpanMock(SH_STEP_SPAN_NAME); + + Assert.assertNotEquals(testSpan.getSpanContext().getSpanId(), shSpan.getSpanContext().getSpanId()); + + try (MockedStatic mockedStatic = mockStatic(Span.class)) { + // Span.current() should return the mocked span. + mockedStatic.when(Span::current).thenReturn(shSpan); + Assert.assertEquals(shSpan, Span.current()); + + // The span doesn't have any attributes. + Assert.assertEquals(0, shSpan.getAttributes().keySet().size()); + + Step step = Mockito.mock(Step.class); + monitoringPipelineListener.notifyOfNewStep(step, stepContext); + + // The span should now contain only the computer action attribute. + // The computer action allowedSpanIdList is empty while + // the child action allowedSpanIdList has an id. + // The id on the list is different from the id of the current span. + Assert.assertEquals(1, shSpan.getAttributes().keySet().size()); + + AttributeKey attributeKey = AttributeKey.stringKey("attribute.from.computer.action.applied"); + Assert.assertTrue(shSpan.getAttributes().containsKey(attributeKey)); + Assert.assertEquals("true", shSpan.getAttributes().get(attributeKey)); + } + } + } +} diff --git a/src/test/java/io/jenkins/plugins/opentelemetry/job/step/WithNewSpanStepTest.java b/src/test/java/io/jenkins/plugins/opentelemetry/job/step/WithNewSpanStepTest.java new file mode 100644 index 000000000..42760454c --- /dev/null +++ b/src/test/java/io/jenkins/plugins/opentelemetry/job/step/WithNewSpanStepTest.java @@ -0,0 +1,218 @@ +/* + * Copyright The Original Author or Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.jenkins.plugins.opentelemetry.job.step; + +import com.github.rutledgepaulv.prune.Tree; +import hudson.model.Result; +import io.jenkins.plugins.opentelemetry.BaseIntegrationTest; +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.sdk.trace.data.SpanData; +import org.apache.commons.lang3.SystemUtils; +import org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition; +import org.jenkinsci.plugins.workflow.job.WorkflowJob; +import org.junit.Assert; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +import static org.junit.Assume.assumeFalse; + +public class WithNewSpanStepTest extends BaseIntegrationTest { + + private static WorkflowJob pipeline; + + @BeforeClass + public static void setUp() throws Exception { + assumeFalse(SystemUtils.IS_OS_WINDOWS); + + jenkinsRule.createOnlineSlave(); + + final String jobName = "test-pipeline-spans" + jobNameSuffix.incrementAndGet(); + pipeline = jenkinsRule.createProject(WorkflowJob.class, jobName); + } + + @Test + public void testLibCallWithUserDefinedSpan() throws Exception { + String pipelineScript = + "def xsh(cmd) {if (isUnix()) {sh cmd} else {bat cmd}};\n" + + "\n" + + "def runBuilds() {\n" + + " withNewSpan(label: 'run-builds') {\n" + + " xsh (label: 'build-mod1', script: 'echo building-module-1') \n" + + " xsh (label: 'build-mod2', script: 'echo building-module-2') \n" + + " }\n" + + "}\n" + + "\n" + + "node {\n" + + " stage('build') {\n" + + " runBuilds()" + + " }\n" + + "}"; + + pipeline.setDefinition(new CpsFlowDefinition(pipelineScript, true)); + jenkinsRule.assertBuildStatus(Result.SUCCESS, pipeline.scheduleBuild2(0)); + + final Tree spansTree = getGeneratedSpans(); + Map> spansTreeMap = getSpanMapWithChildrenFromTree(spansTree); + + // Check that the new spans exist. + Assert.assertNotNull(spansTreeMap.get("run-builds")); + Assert.assertNotNull(spansTreeMap.get("build-mod1")); + Assert.assertNotNull(spansTreeMap.get("build-mod2")); + + // Check span children. + Assert.assertTrue(spansTreeMap.get("run-builds").containsAll(Arrays.asList("build-mod1", "build-mod2"))); + Assert.assertEquals(2, spansTreeMap.get("run-builds").size()); + } + + @Test + public void testUserDefinedSpanWithAttributes() throws Exception { + String pipelineScript = + "def xsh(cmd) {if (isUnix()) {sh cmd} else {bat cmd}};\n" + + "node {\n" + + " stage('build') {\n" + + " withNewSpan(label: 'run-builds', attributes: ([\n" + + " spanAttribute(key: 'modules-num', value: '2'),\n" + + " spanAttribute(key: 'command', value: 'build')\n" + + " ])) {\n" + + " xsh (label: 'build-mod1', script: 'echo building-module-1') \n" + + " echo 'building-module-2'\n" + + " }\n" + + " }\n" + + "}"; + + pipeline.setDefinition(new CpsFlowDefinition(pipelineScript, true)); + jenkinsRule.assertBuildStatus(Result.SUCCESS, pipeline.scheduleBuild2(0)); + + final Tree spansTree = getGeneratedSpans(); + Map> spansTreeMap = getSpanMapWithChildrenFromTree(spansTree); + Map spansDataMap = getSpanDataMapFromTree(spansTree); + + // Check that the new spans exist. + Assert.assertNotNull(spansTreeMap.get("run-builds")); + Assert.assertNotNull(spansTreeMap.get("build-mod1")); + + // Check span children. + Assert.assertTrue(spansTreeMap.get("run-builds").contains("build-mod1")); + Assert.assertEquals(1, spansTreeMap.get("run-builds").size()); + + // Check user-defined span attributes. + Attributes parentAttributes = spansDataMap.get("run-builds").getAttributes(); + Assert.assertEquals("2", parentAttributes.get(AttributeKey.stringKey("modules-num"))); + Assert.assertEquals("build", parentAttributes.get(AttributeKey.stringKey("command"))); + + // Span attributes are also passed to children. + Attributes childAttributes = spansDataMap.get("build-mod1").getAttributes(); + Assert.assertEquals("2", childAttributes.get(AttributeKey.stringKey("modules-num"))); + Assert.assertEquals("build", childAttributes.get(AttributeKey.stringKey("command"))); + } + + @Test + public void testUserDefinedSpanWithChildren() throws Exception { + String pipelineScript = + "def xsh(cmd) {if (isUnix()) {sh cmd} else {bat cmd}};\n" + + "node {\n" + + " stage('build') {\n" + + " withNewSpan(label: 'run-builds') {\n" + + " xsh (label: 'build-mod1', script: 'echo building-module-1') \n" + + " xsh (label: 'build-mod2', script: 'echo building-module-2') \n" + + " }\n" + + " }\n" + + "}"; + + pipeline.setDefinition(new CpsFlowDefinition(pipelineScript, true)); + jenkinsRule.assertBuildStatus(Result.SUCCESS, pipeline.scheduleBuild2(0)); + + final Tree spansTree = getGeneratedSpans(); + Map> spansTreeMap = getSpanMapWithChildrenFromTree(spansTree); + + // Use a non-existent span name. + Assert.assertNull(spansTreeMap.get("non-existent-span")); + + // Check that the new spans exist. + Assert.assertNotNull(spansTreeMap.get("run-builds")); + Assert.assertNotNull(spansTreeMap.get("build-mod1")); + Assert.assertNotNull(spansTreeMap.get("build-mod2")); + + // Check span children. + Assert.assertTrue(spansTreeMap.get("run-builds").containsAll(Arrays.asList("build-mod1", "build-mod2"))); + Assert.assertFalse(spansTreeMap.get("run-builds").containsAll(Arrays.asList("build-mod1", "non-existent"))); + Assert.assertEquals(2, spansTreeMap.get("run-builds").size()); + } + + @Test + public void testUserDefinedSpanWithNoChildren() throws Exception { + String pipelineScript = + "def xsh(cmd) {if (isUnix()) {sh cmd} else {bat cmd}};\n" + + "node {\n" + + " stage('build') {\n" + + " withNewSpan(label: 'run-builds') {\n" + + " echo 'building-module-1'\n" + + " echo 'building-module-2'\n" + + " }\n" + + " }\n" + + "}"; + + pipeline.setDefinition(new CpsFlowDefinition(pipelineScript, true)); + jenkinsRule.assertBuildStatus(Result.SUCCESS, pipeline.scheduleBuild2(0)); + + final Tree spansTree = getGeneratedSpans(); + Map> spansTreeMap = getSpanMapWithChildrenFromTree(spansTree); + + // Check that the new spans exist. + Assert.assertNotNull(spansTreeMap.get("run-builds")); + + // Check span children. + Assert.assertTrue(spansTreeMap.get("run-builds").isEmpty()); + Assert.assertEquals(0, spansTreeMap.get("run-builds").size()); + } + + @Test + public void testUserDefinedSpanWithAttributesNotPassedOnToChildren() throws Exception { + String pipelineScript = + "def xsh(cmd) {if (isUnix()) {sh cmd} else {bat cmd}};\n" + + "node {\n" + + " stage('build') {\n" + + " withNewSpan(label: 'run-builds', attributes: ([\n" + + " spanAttribute(key: 'modules-num', value: '2'),\n" + + " spanAttribute(key: 'command', value: 'build')\n" + + " ]), setAttributesOnlyOnParent: true) {\n" + + " xsh (label: 'build-mod1', script: 'echo building-module-1') \n" + + " echo 'building-module-2'\n" + + " }\n" + + " }\n" + + "}"; + + pipeline.setDefinition(new CpsFlowDefinition(pipelineScript, true)); + jenkinsRule.assertBuildStatus(Result.SUCCESS, pipeline.scheduleBuild2(0)); + + final Tree spansTree = getGeneratedSpans(); + Map> spansTreeMap = getSpanMapWithChildrenFromTree(spansTree); + Map spansDataMap = getSpanDataMapFromTree(spansTree); + + // Check that the new spans exist. + Assert.assertNotNull(spansTreeMap.get("run-builds")); + + // Check span children. + Assert.assertTrue(spansTreeMap.get("run-builds").contains("build-mod1")); + Assert.assertEquals(1, spansTreeMap.get("run-builds").size()); + + // Check user-defined span attributes. + Attributes parentAttributes = spansDataMap.get("run-builds").getAttributes(); + Assert.assertEquals("2", parentAttributes.get(AttributeKey.stringKey("modules-num"))); + Assert.assertEquals("build", parentAttributes.get(AttributeKey.stringKey("command"))); + + // Span attributes are NOT passed to children. + Attributes childAttributes = spansDataMap.get("build-mod1").getAttributes(); + Assert.assertNull(childAttributes.get(AttributeKey.stringKey("modules-num"))); + Assert.assertNull(childAttributes.get(AttributeKey.stringKey("command"))); + } + +} diff --git a/src/test/java/io/opentelemetry/sdk/testing/trace/SpanMock.java b/src/test/java/io/opentelemetry/sdk/testing/trace/SpanMock.java new file mode 100644 index 000000000..2ef309aa0 --- /dev/null +++ b/src/test/java/io/opentelemetry/sdk/testing/trace/SpanMock.java @@ -0,0 +1,181 @@ +/* + * Copyright The Original Author or Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.testing.trace; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.api.trace.StatusCode; +import io.opentelemetry.context.Context; +import org.mockito.Mockito; + +import java.time.Instant; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +/** + * Mock class for {@link Span}. It exposes the attributes for unit testing. + */ +public class SpanMock implements Span { + + private final String spanName; + private final Span delegate; + private final Map, Object> attributesMap = new HashMap<>(); + + public SpanMock(String spanName) { + this.spanName = spanName; + this.delegate = new SpanBuilderMock(spanName).startSpan(); + } + + public Map, Object> getAttributes() { + return attributesMap; + } + + @Override + public Span setAttribute(String key, String value) { + attributesMap.put(AttributeKey.stringKey(key), value); + return delegate.setAttribute(key, value); + } + + @Override + public Span setAttribute(String key, long value) { + attributesMap.put(AttributeKey.stringKey(key), value); + return delegate.setAttribute(key, value); + } + + @Override + public Span setAttribute(String key, double value) { + attributesMap.put(AttributeKey.stringKey(key), value); + return delegate.setAttribute(key, value); + } + + @Override + public Span setAttribute(String key, boolean value) { + attributesMap.put(AttributeKey.stringKey(key), value); + return delegate.setAttribute(key, value); + } + + @Override + public Span setAttribute(AttributeKey key, int value) { + attributesMap.put(key, value); + return delegate.setAttribute(key, value); + } + + @Override + public Span setAllAttributes(Attributes attributes) { + if (!attributes.isEmpty()) { + attributes.forEach(attributesMap::put); + } + return delegate.setAllAttributes(attributes); + } + + @Override + public Span addEvent(String name) { + return delegate.addEvent(name); + } + + @Override + public Span addEvent(String name, long timestamp, TimeUnit unit) { + return delegate.addEvent(name, timestamp, unit); + } + + @Override + public Span addEvent(String name, Instant timestamp) { + return delegate.addEvent(name, timestamp); + } + + @Override + public Span addEvent(String name, Attributes attributes, Instant timestamp) { + return delegate.addEvent(name, attributes, timestamp); + } + + @Override + public Span setStatus(StatusCode statusCode) { + return delegate.setStatus(statusCode); + } + + @Override + public Span recordException(Throwable exception) { + return delegate.recordException(exception); + } + + @Override + public Span addLink(SpanContext spanContext) { + return delegate.addLink(spanContext); + } + + @Override + public Span addLink(SpanContext spanContext, Attributes attributes) { + return delegate.addLink(spanContext, attributes); + } + + @Override + public void end(Instant timestamp) { + delegate.end(timestamp); + } + + @Override + public Context storeInContext(Context context) { + return delegate.storeInContext(context); + } + + @Override + public Span setAttribute(AttributeKey attributeKey, T t) { + attributesMap.put(attributeKey, t); + return delegate.setAttribute(attributeKey, t); + } + + @Override + public Span addEvent(String s, Attributes attributes) { + return delegate.addEvent(s, attributes); + } + + @Override + public Span addEvent(String s, Attributes attributes, long l, TimeUnit timeUnit) { + return delegate.addEvent(s, attributes, l, timeUnit); + } + + @Override + public Span setStatus(StatusCode statusCode, String s) { + return delegate.setStatus(statusCode, s); + } + + @Override + public Span recordException(Throwable throwable, Attributes attributes) { + return delegate.recordException(throwable, attributes); + } + + @Override + public Span updateName(String s) { + return delegate.updateName(s); + } + + @Override + public void end() { + delegate.end(); + } + + @Override + public void end(long l, TimeUnit timeUnit) { + delegate.end(l, timeUnit); + } + + @Override + public SpanContext getSpanContext() { + // The span id is always 0000000000000000. + // Spy the object and stub it to provide a new id. + SpanContext spanContext = Mockito.spy(delegate.getSpanContext()); + Mockito.when(spanContext.getSpanId()).thenReturn("spanId-" + spanName); + return spanContext; + } + + @Override + public boolean isRecording() { + return delegate.isRecording(); + } +} From 64529734236354daa8c7f75c95fe9816588af82e Mon Sep 17 00:00:00 2001 From: Christos Bisias Date: Wed, 25 Sep 2024 16:02:31 +0300 Subject: [PATCH 2/4] rename allowedSpanIdList to inheritanceAllowedSpanIdList --- .../OpenTelemetryAttributesAction.java | 22 +++++++++---------- .../job/MonitoringPipelineListener.java | 4 ++-- .../job/step/SpanAttributeStepExecution.java | 2 +- .../job/MonitoringPipelineListenerTest.java | 2 +- 4 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/main/java/io/jenkins/plugins/opentelemetry/OpenTelemetryAttributesAction.java b/src/main/java/io/jenkins/plugins/opentelemetry/OpenTelemetryAttributesAction.java index 9fe415e54..9bfe6d4c8 100644 --- a/src/main/java/io/jenkins/plugins/opentelemetry/OpenTelemetryAttributesAction.java +++ b/src/main/java/io/jenkins/plugins/opentelemetry/OpenTelemetryAttributesAction.java @@ -36,7 +36,7 @@ public class OpenTelemetryAttributesAction extends InvisibleAction implements Se // If the list has any values, then only the spans on the list will get attributes. // If the list is empty, then there is no restriction. // Used to control attribute inheritance to children spans. - private transient List allowedSpanIdList; + private transient List inheritanceAllowedSpanIdList; @NonNull public Map, Object> getAttributes() { @@ -58,25 +58,25 @@ public boolean isNotYetAppliedToSpan(String spanId) { return appliedToSpans.add(spanId); } - public void addSpanIdToAllowedList(String spanId) { - if (allowedSpanIdList == null) { - allowedSpanIdList = new ArrayList<>(); + public void addSpanIdToInheritanceAllowedList(String spanId) { + if (inheritanceAllowedSpanIdList == null) { + inheritanceAllowedSpanIdList = new ArrayList<>(); } - allowedSpanIdList.add(spanId); + inheritanceAllowedSpanIdList.add(spanId); } - public boolean spanIdAllowedListIsEmpty() { - if (allowedSpanIdList == null) { + public boolean inheritanceAllowedSpanIdListIsEmpty() { + if (inheritanceAllowedSpanIdList == null) { return true; } - return allowedSpanIdList.isEmpty(); + return inheritanceAllowedSpanIdList.isEmpty(); } - public boolean isSpanIdAllowed(String spanId) { - if (allowedSpanIdList == null) { + public boolean isSpanIdAllowedToInheritAttributes(String spanId) { + if (inheritanceAllowedSpanIdList == null) { return false; } - return allowedSpanIdList.contains(spanId); + return inheritanceAllowedSpanIdList.contains(spanId); } @Override 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 dd3dd8070..4c3f9c6c8 100644 --- a/src/main/java/io/jenkins/plugins/opentelemetry/job/MonitoringPipelineListener.java +++ b/src/main/java/io/jenkins/plugins/opentelemetry/job/MonitoringPipelineListener.java @@ -493,8 +493,8 @@ private void setAttributesToSpan(@NonNull Span span, OpenTelemetryAttributesActi } // If the list is empty, ignore this check. - if (!openTelemetryAttributesAction.spanIdAllowedListIsEmpty() && - !openTelemetryAttributesAction.isSpanIdAllowed(span.getSpanContext().getSpanId())) { + if (!openTelemetryAttributesAction.inheritanceAllowedSpanIdListIsEmpty() && + !openTelemetryAttributesAction.isSpanIdAllowedToInheritAttributes(span.getSpanContext().getSpanId())) { // If the list isn't empty, then the attributes shouldn't be set on children spans. // Attributes should only be set on Ids from the list. // If there are Ids on the list but the provided Id isn't part of them, diff --git a/src/main/java/io/jenkins/plugins/opentelemetry/job/step/SpanAttributeStepExecution.java b/src/main/java/io/jenkins/plugins/opentelemetry/job/step/SpanAttributeStepExecution.java index ea00cb04a..3874b51d2 100644 --- a/src/main/java/io/jenkins/plugins/opentelemetry/job/step/SpanAttributeStepExecution.java +++ b/src/main/java/io/jenkins/plugins/opentelemetry/job/step/SpanAttributeStepExecution.java @@ -141,7 +141,7 @@ private OpenTelemetryAttributesAction mergeAttributes(StepContext context, List< if (setAttributesOnlyOnParent) { // If the flag is set to true, then only the current span will get the attributes. // This will prevent any children from inheriting the attributes of the parent span. - resultingAttributes.addSpanIdToAllowedList(currentSpanId); + resultingAttributes.addSpanIdToInheritanceAllowedList(currentSpanId); } return resultingAttributes; } diff --git a/src/test/java/io/jenkins/plugins/opentelemetry/job/MonitoringPipelineListenerTest.java b/src/test/java/io/jenkins/plugins/opentelemetry/job/MonitoringPipelineListenerTest.java index 80527697d..0be05f1af 100644 --- a/src/test/java/io/jenkins/plugins/opentelemetry/job/MonitoringPipelineListenerTest.java +++ b/src/test/java/io/jenkins/plugins/opentelemetry/job/MonitoringPipelineListenerTest.java @@ -233,7 +233,7 @@ private void setupAttributesActionStubs(List allowedIds) throws IOExcept OpenTelemetryAttributesAction otelChildAttributesAction = new OpenTelemetryAttributesAction(); for (String id : allowedIds) { - otelChildAttributesAction.addSpanIdToAllowedList(id); + otelChildAttributesAction.addSpanIdToInheritanceAllowedList(id); } otelChildAttributesAction.getAttributes().put(AttributeKey.stringKey("attribute.from.child.action.applied"), "true"); From 7123b65a46de9e2f41cc94821f80ae089867594b Mon Sep 17 00:00:00 2001 From: Christos Bisias Date: Wed, 25 Sep 2024 16:25:40 +0300 Subject: [PATCH 3/4] test cleanup --- .../job/MonitoringPipelineListenerTest.java | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/test/java/io/jenkins/plugins/opentelemetry/job/MonitoringPipelineListenerTest.java b/src/test/java/io/jenkins/plugins/opentelemetry/job/MonitoringPipelineListenerTest.java index 0be05f1af..3e39382ee 100644 --- a/src/test/java/io/jenkins/plugins/opentelemetry/job/MonitoringPipelineListenerTest.java +++ b/src/test/java/io/jenkins/plugins/opentelemetry/job/MonitoringPipelineListenerTest.java @@ -72,17 +72,13 @@ public class MonitoringPipelineListenerTest { private static final StepContext stepContext = Mockito.mock(StepContext.class); private static final OtelTraceService otelTraceService = Mockito.mock(OtelTraceService.class); - - private static MonitoringPipelineListener monitoringPipelineListener; - private static WorkflowRun workflowRun; + private static final WorkflowRun workflowRun = Mockito.mock(WorkflowRun.class); @BeforeClass public static void commonSetup() throws IOException, InterruptedException { // Jenkins must have been initialized. Assert.assertNotNull(Jenkins.getInstanceOrNull()); - workflowRun = Mockito.mock(WorkflowRun.class); - Mockito.when(stepContext.get(WorkflowRun.class)).thenReturn(workflowRun); } @@ -91,7 +87,8 @@ public static class ParamTests { private static final String START_NODE_ROOT_SPAN_NAME = "root-span"; private static final String WITH_NEW_SPAN_NAME = "with-new-span"; - SpanBuilderMock spanBuilderMock = Mockito.spy(new SpanBuilderMock(WITH_NEW_SPAN_NAME)); + private final SpanBuilderMock spanBuilderMock = Mockito.spy(new SpanBuilderMock(WITH_NEW_SPAN_NAME)); + private MonitoringPipelineListener monitoringPipelineListener; @Parameterized.Parameter(0) public String attributeKeyName; @@ -200,11 +197,14 @@ public static class NonParamTests { private static final String TEST_SPAN_NAME = "test-span"; private static final String SH_STEP_SPAN_NAME = "sh-span"; - private static final FlowNode flowNode = Mockito.mock(FlowNode.class); - private static SpanMock testSpan; + private final FlowNode flowNode = Mockito.mock(FlowNode.class); + private final MonitoringPipelineListener monitoringPipelineListener = new MonitoringPipelineListener(); + private SpanMock testSpan; @Before public void setup() throws IOException, InterruptedException { + monitoringPipelineListener.setOpenTelemetryTracerService(otelTraceService); + testSpan = new SpanMock(TEST_SPAN_NAME); testSpan.setAttribute("caller.name", "testuser"); From cbc7a79933448166f4519f0a28d015ca041407f2 Mon Sep 17 00:00:00 2001 From: Christos Bisias Date: Wed, 25 Sep 2024 16:51:46 +0300 Subject: [PATCH 4/4] testing png --- ...jenkins-opentelemetry-jaeger-withNewSpan.png | Bin 0 -> 88957 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 docs/images/jenkins-opentelemetry-jaeger-withNewSpan.png diff --git a/docs/images/jenkins-opentelemetry-jaeger-withNewSpan.png b/docs/images/jenkins-opentelemetry-jaeger-withNewSpan.png new file mode 100644 index 0000000000000000000000000000000000000000..f36dd978d32370206ad5f0084937d2dd97205fa1 GIT binary patch literal 88957 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