From adeb34be3029cbd94b86e12cbad82b66913c3cac Mon Sep 17 00:00:00 2001 From: Tobias Peter Date: Tue, 19 Sep 2023 16:47:32 +0000 Subject: [PATCH 1/2] Store step/stage result as attribute on corresponding spans Unlike otel.status_code, which can only be OK or ERROR, the step result can also be UNSTABLE, NOT_BUILT (for skipped steps), ABORTED, etc. The result of a stage is calculated from the error and warning actions attached to the FlowNodes between the stage's StepStartNode and StepEndNode, using org.jenkinsci.plugins.workflow.pipelinegraphanalysis.StatusAndTiming (which is the implementation that ultimately also provides stage results for BlueOcean). --- docs/job-traces.md | 1 + pom.xml | 4 ++ .../job/MonitoringPipelineListener.java | 40 ++++++++++++++----- .../job/jenkins/AbstractPipelineListener.java | 8 ++-- ...raphListenerAdapterToPipelineListener.java | 24 +++++------ .../job/jenkins/PipelineListener.java | 8 ++-- .../JenkinsOtelSemanticAttributes.java | 7 +++- 7 files changed, 61 insertions(+), 31 deletions(-) diff --git a/docs/job-traces.md b/docs/job-traces.md index 5d95d49ef..5fddb27e8 100644 --- a/docs/job-traces.md +++ b/docs/job-traces.md @@ -75,6 +75,7 @@ Attributes reported on the span of pipeline steps: | jenkins.pipeline.step.name | Step name (user friendly) | String | | jenkins.pipeline.step.type | Step name | String | | jenkins.pipeline.step.id | Step id | String | +| jenkins.pipeline.step.result | Step result | Enum (`ABORTED`, `FAILURE`, `NOT_EXECUTED`, `PAUSED_PENDING_INPUT`, `QUEUED`, `SUCCESS`, `UNSTABLE`; see [GenericStatus](https://javadoc.jenkins.io/plugin/pipeline-graph-analysis/org/jenkinsci/plugins/workflow/pipelinegraphanalysis/GenericStatus.html)) | | jenkins.pipeline.step.plugin.name | Jenkins plugin for that particular step | String | | jenkins.pipeline.step.plugin.version| Jenkins plugin version | String | | jenkins.pipeline.step.agent.label | Labels attached to the agent | String | diff --git a/pom.xml b/pom.xml index 149af2480..d28fc1e23 100644 --- a/pom.xml +++ b/pom.xml @@ -255,6 +255,10 @@ org.jenkins-ci.plugins pipeline-stage-step + + org.jenkins-ci.plugins + pipeline-graph-analysis + org.jenkins-ci.plugins scm-api 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 9070badf1..7b5b0f54c 100644 --- a/src/main/java/io/jenkins/plugins/opentelemetry/job/MonitoringPipelineListener.java +++ b/src/main/java/io/jenkins/plugins/opentelemetry/job/MonitoringPipelineListener.java @@ -46,6 +46,8 @@ import org.jenkinsci.plugins.workflow.flow.StepListener; import org.jenkinsci.plugins.workflow.graph.FlowNode; import org.jenkinsci.plugins.workflow.job.WorkflowRun; +import org.jenkinsci.plugins.workflow.pipelinegraphanalysis.GenericStatus; +import org.jenkinsci.plugins.workflow.pipelinegraphanalysis.StatusAndTiming; import org.jenkinsci.plugins.workflow.steps.*; import edu.umd.cs.findbugs.annotations.NonNull; @@ -129,7 +131,7 @@ public void onStartNodeStep(@NonNull StepStartNode stepStartNode, @Nullable Stri @Override public void onAfterStartNodeStep(@NonNull StepStartNode stepStartNode, @Nullable String nodeLabel, @NonNull WorkflowRun run) { // end the JenkinsOtelSemanticAttributes.AGENT_ALLOCATE span - endCurrentSpan(stepStartNode, run); + endCurrentSpan(stepStartNode, run, null); } @Override @@ -156,13 +158,17 @@ public void onStartStageStep(@NonNull StepStartNode stepStartNode, @NonNull Stri } @Override - public void onEndNodeStep(@NonNull StepEndNode node, @NonNull String nodeName, @NonNull WorkflowRun run) { - endCurrentSpan(node, run); + public void onEndNodeStep(@NonNull StepEndNode node, @NonNull String nodeName, 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 onEndStageStep(@NonNull StepEndNode node, @NonNull String stageName, @NonNull WorkflowRun run) { - endCurrentSpan(node, run); + public void onEndStageStep(@NonNull StepEndNode node, @NonNull String stageName, FlowNode nextNode, @NonNull WorkflowRun run) { + StepStartNode stageStartNode = node.getStartNode(); + GenericStatus stageStatus = StatusAndTiming.computeChunkStatus2(run, null, stageStartNode, node, nextNode); + endCurrentSpan(node, run, stageStatus); } protected List getStepHandlers() { @@ -212,12 +218,13 @@ public void onAtomicStep(@NonNull StepAtomNode node, @NonNull WorkflowRun run) { } @Override - public void onAfterAtomicStep(@NonNull StepAtomNode node, @NonNull WorkflowRun run) { + public void onAfterAtomicStep(@NonNull StepAtomNode node, FlowNode nextNode, @NonNull WorkflowRun run) { if (isIgnoredStep(node.getDescriptor())){ LOGGER.log(Level.FINE, () -> run.getFullDisplayName() + " - don't end span for step '" + node.getDisplayFunctionName() + "'"); return; } - endCurrentSpan(node, run); + GenericStatus stageStatus = StatusAndTiming.computeChunkStatus2(run, null, node, node, nextNode); + endCurrentSpan(node, run, stageStatus); } private boolean isIgnoredStep(@Nullable StepDescriptor stepDescriptor) { @@ -289,17 +296,21 @@ public void onStartParallelStepBranch(@NonNull StepStartNode stepStartNode, @Non } @Override - public void onEndParallelStepBranch(@NonNull StepEndNode node, @NonNull String branchName, @NonNull WorkflowRun run) { - endCurrentSpan(node, run); + public void onEndParallelStepBranch(@NonNull StepEndNode node, @NonNull String branchName, FlowNode nextNode, @NonNull WorkflowRun run) { + StepStartNode parallelStartNode = node.getStartNode(); + GenericStatus parallelStatus = StatusAndTiming.computeChunkStatus2(run, null, parallelStartNode, node, nextNode); + endCurrentSpan(node, run, parallelStatus); } - private void endCurrentSpan(FlowNode node, WorkflowRun run) { + private void endCurrentSpan(FlowNode node, WorkflowRun run, GenericStatus status) { try (Scope ignored = setupContext(run, node)) { verifyNotNull(ignored, "%s - No span found for node %s", run, node); Span span = getTracerService().getSpan(run, node); + ErrorAction errorAction = node.getError(); if (errorAction == null) { + if (status == null) status = GenericStatus.SUCCESS; span.setStatus(StatusCode.OK); } else { Throwable throwable = errorAction.getError(); @@ -307,6 +318,8 @@ private void endCurrentSpan(FlowNode node, WorkflowRun run) { FlowInterruptedException interruptedException = (FlowInterruptedException) throwable; List causesOfInterruption = interruptedException.getCauses(); + if (status == null) status = GenericStatus.fromResult(interruptedException.getResult()); + List causeDescriptions = causesOfInterruption.stream().map(cause -> cause.getClass().getSimpleName() + ": " + cause.getShortDescription()).collect(Collectors.toList()); span.setAttribute(JenkinsOtelSemanticAttributes.JENKINS_STEP_INTERRUPTION_CAUSES, causeDescriptions); @@ -326,10 +339,17 @@ private void endCurrentSpan(FlowNode node, WorkflowRun run) { span.setStatus(StatusCode.ERROR, statusDescription); } } else { + if (status == null) status = GenericStatus.FAILURE; span.recordException(throwable); span.setStatus(StatusCode.ERROR, throwable.getMessage()); } } + + if (status != null) { + status = StatusAndTiming.coerceStatusApi(status, StatusAndTiming.API_V2); + span.setAttribute(JenkinsOtelSemanticAttributes.JENKINS_STEP_RESULT, status.toString()); + } + span.end(); LOGGER.log(Level.FINE, () -> run.getFullDisplayName() + " - < " + node.getDisplayFunctionName() + " - end " + OtelUtils.toDebugString(span)); 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 0d6bcfa63..1bb89d91b 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 @@ -36,12 +36,12 @@ public void onAfterStartNodeStep(@NonNull StepStartNode stepStartNode, @Nullable } @Override - public void onEndNodeStep(@NonNull StepEndNode nodeStepEndNode, @NonNull String nodeName, @NonNull WorkflowRun run) { + public void onEndNodeStep(@NonNull StepEndNode nodeStepEndNode, @NonNull String nodeName, FlowNode nextNode, @NonNull WorkflowRun run) { } @Override - public void onEndStageStep(@NonNull StepEndNode stageStepEndNode, @NonNull String stageName, @NonNull WorkflowRun run) { + public void onEndStageStep(@NonNull StepEndNode stageStepEndNode, @NonNull String stageName, FlowNode nextNode, @NonNull WorkflowRun run) { } @@ -51,7 +51,7 @@ public void onAtomicStep(@NonNull StepAtomNode node, @NonNull WorkflowRun run) { } @Override - public void onAfterAtomicStep(@NonNull StepAtomNode stepAtomNode, @NonNull WorkflowRun run) { + public void onAfterAtomicStep(@NonNull StepAtomNode stepAtomNode, FlowNode nextNode, @NonNull WorkflowRun run) { } @@ -61,7 +61,7 @@ public void onStartParallelStepBranch(@NonNull StepStartNode stepStartNode, @Non } @Override - public void onEndParallelStepBranch(@NonNull StepEndNode stepStepNode, @NonNull String branchName, @NonNull WorkflowRun run) { + public void onEndParallelStepBranch(@NonNull StepEndNode stepStepNode, @NonNull String branchName, FlowNode nextNode, @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 6bb42a6dd..ed400dbcb 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 @@ -55,18 +55,18 @@ private void processPreviousNodes(FlowNode node, WorkflowRun run) { log(Level.FINE, () -> run.getFullDisplayName() + " - Process previous node " + PipelineNodeUtil.getDetailedDebugString(previousNode) + " of node " + PipelineNodeUtil.getDetailedDebugString(node)); if (previousNode instanceof StepAtomNode) { StepAtomNode stepAtomNode = (StepAtomNode) previousNode; - fireOnAfterAtomicStep(stepAtomNode, run); + fireOnAfterAtomicStep(stepAtomNode, node, run); } else if (isBeforeEndExecutorNodeStep(previousNode)) { String nodeName = PipelineNodeUtil.getDisplayName(((StepEndNode) previousNode).getStartNode()); - fireOnAfterEndNodeStep((StepEndNode) previousNode, nodeName, run); + fireOnAfterEndNodeStep((StepEndNode) previousNode, nodeName, node, run); } else if (isBeforeEndStageStep(previousNode)) { String stageName = PipelineNodeUtil.getDisplayName(((StepEndNode) previousNode).getStartNode()); - fireOnAfterEndStageStep((StepEndNode) previousNode, stageName, run); + fireOnAfterEndStageStep((StepEndNode) previousNode, stageName, node, run); } else if (isBeforeEndParallelBranch(previousNode)) { StepEndNode endParallelBranchNode = (StepEndNode) previousNode; StepStartNode beginParallelBranch = endParallelBranchNode.getStartNode(); ThreadNameAction persistentAction = verifyNotNull(beginParallelBranch.getPersistentAction(ThreadNameAction.class), "Null ThreadNameAction on %s", beginParallelBranch); - fireOnAfterEndParallelStepBranch(endParallelBranchNode, persistentAction.getThreadName(), run); + fireOnAfterEndParallelStepBranch(endParallelBranchNode, persistentAction.getThreadName(), node, run); } else { log(Level.FINE, () -> "Ignore previous node " + PipelineNodeUtil.getDetailedDebugString(previousNode)); } @@ -129,11 +129,11 @@ private void fireOnBeforeStartParallelStepBranch(@NonNull StepStartNode node, @N } } - private void fireOnAfterEndParallelStepBranch(@NonNull StepEndNode node, @NonNull String branchName, @NonNull WorkflowRun run) { + private void fireOnAfterEndParallelStepBranch(@NonNull StepEndNode node, @NonNull String branchName, FlowNode nextNode, @NonNull WorkflowRun run) { for (PipelineListener pipelineListener : PipelineListener.all()) { log(Level.FINE, () -> "onAfterEndParallelStepBranch(branchName: " + branchName + ", node[name:" + node.getDisplayName() + ", id: " + node.getId() + "]): " + pipelineListener.toString()); try { - pipelineListener.onEndParallelStepBranch(node, branchName, run); + pipelineListener.onEndParallelStepBranch(node, branchName, nextNode, run); } catch (RuntimeException e) { LOGGER.log(Level.WARNING, e, () -> "Exception invoking `onAfterEndParallelStepBranch` on " + pipelineListener); } @@ -157,11 +157,11 @@ private void logFlowNodeDetails(@NonNull FlowNode node, @NonNull WorkflowRun run }); } - public void fireOnAfterAtomicStep(@NonNull StepAtomNode stepAtomNode, @NonNull WorkflowRun run) { + public void fireOnAfterAtomicStep(@NonNull StepAtomNode stepAtomNode, FlowNode nextNode, @NonNull WorkflowRun run) { for (PipelineListener pipelineListener : PipelineListener.all()) { log(() -> "onAfterAtomicStep(" + stepAtomNode.getDisplayName() + "): " + pipelineListener.toString()); try { - pipelineListener.onAfterAtomicStep(stepAtomNode, run); + pipelineListener.onAfterAtomicStep(stepAtomNode, nextNode, run); } catch (RuntimeException e) { LOGGER.log(Level.WARNING, e, () -> "Exception invoking `onAfterAtomicStep` on " + pipelineListener); } @@ -190,22 +190,22 @@ public void fireOnStartPipeline(@NonNull FlowStartNode node, @NonNull WorkflowRu } } - public void fireOnAfterEndNodeStep(@NonNull StepEndNode node, @NonNull String nodeName, @NonNull WorkflowRun run) { + public void fireOnAfterEndNodeStep(@NonNull StepEndNode node, @NonNull String nodeName, FlowNode nextNode, @NonNull WorkflowRun run) { for (PipelineListener pipelineListener : PipelineListener.all()) { log(() -> "onAfterEndNodeStep(" + node.getDisplayName() + "): " + pipelineListener.toString() + (node.getError() != null ? ("error: " + node.getError().getError()) : "")); try { - pipelineListener.onEndNodeStep(node, nodeName, run); + pipelineListener.onEndNodeStep(node, nodeName, nextNode, run); } catch (RuntimeException e) { LOGGER.log(Level.WARNING, e, () -> "Exception invoking `onAfterEndNodeStep` on " + pipelineListener); } } } - public void fireOnAfterEndStageStep(@NonNull StepEndNode node, @NonNull String stageName, @NonNull WorkflowRun run) { + public void fireOnAfterEndStageStep(@NonNull StepEndNode node, @NonNull String stageName, FlowNode nextNode, @NonNull WorkflowRun run) { for (PipelineListener pipelineListener : PipelineListener.all()) { log(() -> "onAfterEndStageStep(" + node.getDisplayName() + "): " + pipelineListener.toString() + (node.getError() != null ? ("error: " + node.getError().getError()) : "")); try { - pipelineListener.onEndStageStep(node, stageName, run); + pipelineListener.onEndStageStep(node, stageName, nextNode, run); } catch (RuntimeException e) { LOGGER.log(Level.WARNING, e, () -> "Exception invoking `onAfterEndStageStep` on " + pipelineListener); } 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 5ba4e4863..ea8175011 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 @@ -41,7 +41,7 @@ static List all() { /** * Just after the `node` step ends */ - void onEndNodeStep(@NonNull StepEndNode nodeStepEndNode, @NonNull String nodeName, @NonNull WorkflowRun run); + void onEndNodeStep(@NonNull StepEndNode nodeStepEndNode, @NonNull String nodeName, FlowNode nextNode, @NonNull WorkflowRun run); /** * Just before the `stage`step starts @@ -51,7 +51,7 @@ static List all() { /** * Just after the `stage` step ends */ - void onEndStageStep(@NonNull StepEndNode stageStepEndNode, @NonNull String stageName, @NonNull WorkflowRun run); + void onEndStageStep(@NonNull StepEndNode stageStepEndNode, @NonNull String stageName, FlowNode nextNode, @NonNull WorkflowRun run); /** * Just before the `parallel` branch starts @@ -61,7 +61,7 @@ static List all() { /** * Just before the `parallel` branch ends */ - void onEndParallelStepBranch(@NonNull StepEndNode stepStepNode, @NonNull String branchName, @NonNull WorkflowRun run); + void onEndParallelStepBranch(@NonNull StepEndNode stepStepNode, @NonNull String branchName, FlowNode nextNode, @NonNull WorkflowRun run); /** * Just before the atomic step starts @@ -71,7 +71,7 @@ static List all() { /** * Just after the atomic step */ - void onAfterAtomicStep(@NonNull StepAtomNode stepAtomNode, @NonNull WorkflowRun run); + void onAfterAtomicStep(@NonNull StepAtomNode stepAtomNode, FlowNode nextNode, @NonNull WorkflowRun run); /** * Just after the pipeline ends diff --git a/src/main/java/io/jenkins/plugins/opentelemetry/semconv/JenkinsOtelSemanticAttributes.java b/src/main/java/io/jenkins/plugins/opentelemetry/semconv/JenkinsOtelSemanticAttributes.java index f8d770962..c1950a0dd 100644 --- a/src/main/java/io/jenkins/plugins/opentelemetry/semconv/JenkinsOtelSemanticAttributes.java +++ b/src/main/java/io/jenkins/plugins/opentelemetry/semconv/JenkinsOtelSemanticAttributes.java @@ -75,6 +75,11 @@ public final class JenkinsOtelSemanticAttributes { * @see org.jenkinsci.plugins.workflow.graph.FlowNode#getId() */ public static final AttributeKey JENKINS_STEP_ID = AttributeKey.stringKey("jenkins.pipeline.step.id"); + /** + * @see org.jenkinsci.plugins.workflow.pipelinegraphanalysis.GenericStatus + * @see org.jenkinsci.plugins.workflow.pipelinegraphanalysis.StatusAndTiming#computeChunkStatus2(org.jenkinsci.plugins.workflow.job.WorkflowRun,org.jenkinsci.plugins.workflow.graph.FlowNode,org.jenkinsci.plugins.workflow.graph.FlowNode,org.jenkinsci.plugins.workflow.graph.FlowNode,org.jenkinsci.plugins.workflow.graph.FlowNode) + */ + public static final AttributeKey JENKINS_STEP_RESULT = AttributeKey.stringKey("jenkins.pipeline.step.result"); /** * @see PluginWrapper#getShortName() */ @@ -97,7 +102,7 @@ public final class JenkinsOtelSemanticAttributes { public static final String JENKINS = "jenkins"; /** - * As {@link Jenkins.MasterComputer#getName()} return "", choose another name + * As {@link Jenkins.MasterComputer#getName()} returns "", choose another name * * @see Jenkins.MasterComputer#getName() */ From b413e59f1fb916c0db8da5b1aab4e471a5547e9e Mon Sep 17 00:00:00 2001 From: Tobias Peter Date: Tue, 7 Nov 2023 14:24:18 +0100 Subject: [PATCH 2/2] Add test for step/stage result span attributes --- .../job/step/StepResultTest.java | 139 ++++++++++++++++++ 1 file changed, 139 insertions(+) create mode 100644 src/test/java/io/jenkins/plugins/opentelemetry/job/step/StepResultTest.java diff --git a/src/test/java/io/jenkins/plugins/opentelemetry/job/step/StepResultTest.java b/src/test/java/io/jenkins/plugins/opentelemetry/job/step/StepResultTest.java new file mode 100644 index 000000000..74835bd33 --- /dev/null +++ b/src/test/java/io/jenkins/plugins/opentelemetry/job/step/StepResultTest.java @@ -0,0 +1,139 @@ +/* + * 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.Node; +import hudson.model.Result; +import io.jenkins.plugins.opentelemetry.BaseIntegrationTest; +import io.jenkins.plugins.opentelemetry.semconv.JenkinsOtelSemanticAttributes; +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.sdk.trace.data.SpanData; +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.job.WorkflowRun; +import org.junit.Test; + +import static org.junit.Assume.assumeFalse; + +public class StepResultTest extends BaseIntegrationTest { + + @Test + public void testSimplePipelineWithWithStepResults() throws Exception { + assumeFalse(SystemUtils.IS_OS_WINDOWS); + // BEFORE + + String pipelineScript = "def xsh(cmd) {if (isUnix()) {sh cmd} else {bat cmd}};\n" + + "node() {\n" + + " stage('build') {\n" + + " unstable('stage unstable')\n" + + " }\n" + + " stage('parallel') {\n" + + " catchError(stageResult: 'UNSTABLE') { // otherwise, the timeout stage would never run\n" + + " parallel (\n" + + " first: { xsh (label: 'parallel-first', script: 'exit 1') },\n" + + " second: { xsh (label: 'parallel-second', script: 'exit 0') },\n" + + " )\n" + + " }\n" + + " }\n" + + " stage('skipped') {\n" + + " org.jenkinsci.plugins.pipeline.modeldefinition.Utils.markStageSkippedForConditional('skipped');\n" + + " }\n" + + " stage('timeout') {\n" + + " timeout(time: 1, unit: 'MILLISECONDS') {\n" + + " xsh (label: 'sleep', script: 'sleep 1')\n" + + " }\n" + + " }\n" + + "}"; + final Node agent = jenkinsRule.createOnlineSlave(); + + final String jobName = "test-simple-pipeline-with-step-results" + jobNameSuffix.incrementAndGet(); + WorkflowJob pipeline = jenkinsRule.createProject(WorkflowJob.class, jobName); + pipeline.setDefinition(new CpsFlowDefinition(pipelineScript, true)); + WorkflowRun build = jenkinsRule.assertBuildStatus(Result.ABORTED, pipeline.scheduleBuild2(0)); + + String rootSpanName = JenkinsOtelSemanticAttributes.CI_PIPELINE_RUN_ROOT_SPAN_NAME_PREFIX + jobName; + + final Tree spans = getGeneratedSpans(); + + checkChainOfSpans(spans, "Phase: Start", rootSpanName); + checkChainOfSpans(spans, JenkinsOtelSemanticAttributes.AGENT_ALLOCATION_UI, JenkinsOtelSemanticAttributes.AGENT_UI, "Phase: Run", rootSpanName); + checkChainOfSpans(spans, "unstable", "Stage: build", JenkinsOtelSemanticAttributes.AGENT_UI, "Phase: Run", rootSpanName); + checkChainOfSpans(spans, "parallel-first", "Parallel branch: first", "Stage: parallel", JenkinsOtelSemanticAttributes.AGENT_UI, "Phase: Run", rootSpanName); + checkChainOfSpans(spans, "parallel-second", "Parallel branch: second", "Stage: parallel", JenkinsOtelSemanticAttributes.AGENT_UI, "Phase: Run", rootSpanName); + checkChainOfSpans(spans, "Stage: skipped", JenkinsOtelSemanticAttributes.AGENT_UI, "Phase: Run", rootSpanName); + checkChainOfSpans(spans, "Stage: timeout", JenkinsOtelSemanticAttributes.AGENT_UI, "Phase: Run", rootSpanName); + checkChainOfSpans(spans, "Phase: Finalise", rootSpanName); + + // Note: pipeline root span is not a step/stage, so it does not get a JenkinsOtelSemanticAttributes.JENKINS_STEP_RESULT attribute (just like it doesn't get any JenkinsOtelSemanticAttributes at all at the moment) + // Neither are any of the 3 "Phase" spans. + + { // node span: 'ABORTED' (because timeout stage is aborted) + SpanData actualSpanData = spans.breadthFirstStream().filter(sdw -> JenkinsOtelSemanticAttributes.AGENT_UI.equals(sdw.spanData.getName())).findFirst().get().spanData; + String actualStageResult = actualSpanData.getAttributes().get(JenkinsOtelSemanticAttributes.JENKINS_STEP_RESULT); + MatcherAssert.assertThat(actualStageResult, CoreMatchers.is("ABORTED")); + } + + { // node allocation span: 'SUCCESS' + SpanData actualSpanData = spans.breadthFirstStream().filter(sdw -> JenkinsOtelSemanticAttributes.AGENT_ALLOCATION_UI.equals(sdw.spanData.getName())).findFirst().get().spanData; + String actualStageResult = actualSpanData.getAttributes().get(JenkinsOtelSemanticAttributes.JENKINS_STEP_RESULT); + MatcherAssert.assertThat(actualStageResult, CoreMatchers.is("SUCCESS")); + } + + { // stage 'build': 'UNSTABLE' + SpanData actualSpanData = spans.breadthFirstStream().filter(sdw -> "Stage: build".equals(sdw.spanData.getName())).findFirst().get().spanData; + String actualStageResult = actualSpanData.getAttributes().get(JenkinsOtelSemanticAttributes.JENKINS_STEP_RESULT); + MatcherAssert.assertThat(actualStageResult, CoreMatchers.is("UNSTABLE")); + } + + { // stage 'parallel': 'UNSTABLE' (because catchError caught parallel-first's FAILURE and set stageResult to UNSTABLE) + SpanData actualSpanData = spans.breadthFirstStream().filter(sdw -> "Stage: parallel".equals(sdw.spanData.getName())).findFirst().get().spanData; + String actualStageResult = actualSpanData.getAttributes().get(JenkinsOtelSemanticAttributes.JENKINS_STEP_RESULT); + MatcherAssert.assertThat(actualStageResult, CoreMatchers.is("UNSTABLE")); + } + + { // parallel node 'first': 'FAILURE' + SpanData actualSpanData = spans.breadthFirstStream().filter(sdw -> "Parallel branch: first".equals(sdw.spanData.getName())).findFirst().get().spanData; + String actualStepResult = actualSpanData.getAttributes().get(JenkinsOtelSemanticAttributes.JENKINS_STEP_RESULT); + MatcherAssert.assertThat(actualStepResult, CoreMatchers.is("FAILURE")); + } + + { // xsh node 'parallel-first': 'FAILURE' + SpanData actualSpanData = spans.breadthFirstStream().filter(sdw -> "parallel-first".equals(sdw.spanData.getName())).findFirst().get().spanData; + String actualStepResult = actualSpanData.getAttributes().get(JenkinsOtelSemanticAttributes.JENKINS_STEP_RESULT); + MatcherAssert.assertThat(actualStepResult, CoreMatchers.is("FAILURE")); + } + + { // parallel node 'second': 'SUCCESS' + SpanData actualSpanData = spans.breadthFirstStream().filter(sdw -> "Parallel branch: second".equals(sdw.spanData.getName())).findFirst().get().spanData; + String actualStepResult = actualSpanData.getAttributes().get(JenkinsOtelSemanticAttributes.JENKINS_STEP_RESULT); + MatcherAssert.assertThat(actualStepResult, CoreMatchers.is("SUCCESS")); + } + + { // xsh node 'parallel-second': 'SUCCESS' + SpanData actualSpanData = spans.breadthFirstStream().filter(sdw -> "parallel-second".equals(sdw.spanData.getName())).findFirst().get().spanData; + String actualStepResult = actualSpanData.getAttributes().get(JenkinsOtelSemanticAttributes.JENKINS_STEP_RESULT); + MatcherAssert.assertThat(actualStepResult, CoreMatchers.is("SUCCESS")); + } + + { // stage 'skipped': 'NOT_EXECUTED' + SpanData actualSpanData = spans.breadthFirstStream().filter(sdw -> "Stage: skipped".equals(sdw.spanData.getName())).findFirst().get().spanData; + String actualStageResult = actualSpanData.getAttributes().get(JenkinsOtelSemanticAttributes.JENKINS_STEP_RESULT); + MatcherAssert.assertThat(actualStageResult, CoreMatchers.is("NOT_EXECUTED")); + } + + { // stage 'timeout': 'ABORTED' + SpanData actualSpanData = spans.breadthFirstStream().filter(sdw -> "Stage: timeout".equals(sdw.spanData.getName())).findFirst().get().spanData; + String actualStageResult = actualSpanData.getAttributes().get(JenkinsOtelSemanticAttributes.JENKINS_STEP_RESULT); + MatcherAssert.assertThat(actualStageResult, CoreMatchers.is("ABORTED")); + } + + MatcherAssert.assertThat(spans.cardinality(), CoreMatchers.is(15L)); + } +} \ No newline at end of file