Skip to content

Commit

Permalink
Merge pull request #956 from xBis7/with-new-span-dev
Browse files Browse the repository at this point in the history
Capability to manually create spans
  • Loading branch information
kuisathaverat authored Oct 28, 2024
2 parents e546d41 + 5cd6b24 commit 3b27ca9
Show file tree
Hide file tree
Showing 15 changed files with 1,085 additions and 3 deletions.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
46 changes: 45 additions & 1 deletion docs/job-traces.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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:
Expand Down
5 changes: 5 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -457,6 +457,11 @@
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.github.rutledgepaulv</groupId>
<artifactId>prune</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -31,6 +33,10 @@ public class OpenTelemetryAttributesAction extends InvisibleAction implements Se
private transient Map<AttributeKey<?>, Object> attributes;

private transient Set<String> 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<String> inheritanceAllowedSpanIdList;

@NonNull
public Map<AttributeKey<?>, Object> getAttributes() {
Expand All @@ -52,6 +58,27 @@ public boolean isNotYetAppliedToSpan(String spanId) {
return appliedToSpans.add(spanId);
}

public void addSpanIdToInheritanceAllowedList(String spanId) {
if (inheritanceAllowedSpanIdList == null) {

Check warning on line 62 in src/main/java/io/jenkins/plugins/opentelemetry/OpenTelemetryAttributesAction.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 62 is only partially covered, one branch is missing
inheritanceAllowedSpanIdList = new ArrayList<>();
}
inheritanceAllowedSpanIdList.add(spanId);
}

public boolean inheritanceAllowedSpanIdListIsEmpty() {
if (inheritanceAllowedSpanIdList == null) {
return true;
}
return inheritanceAllowedSpanIdList.isEmpty();
}

public boolean isSpanIdAllowedToInheritAttributes(String spanId) {
if (inheritanceAllowedSpanIdList == null) {

Check warning on line 76 in src/main/java/io/jenkins/plugins/opentelemetry/OpenTelemetryAttributesAction.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 76 is only partially covered, one branch is missing
return false;

Check warning on line 77 in src/main/java/io/jenkins/plugins/opentelemetry/OpenTelemetryAttributesAction.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 77 is not covered by tests
}
return inheritanceAllowedSpanIdList.contains(spanId);
}

@Override
public String toString() {
return "OpenTelemetryAttributesAction{" +
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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) {

Check warning on line 278 in src/main/java/io/jenkins/plugins/opentelemetry/job/MonitoringPipelineListener.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 278 is only partially covered, one branch is missing
Descriptor<? extends Describable> d = SymbolLookup.get().findDescriptor(Describable.class, describable.getSymbol());
return d.getDisplayName();

Check warning on line 280 in src/main/java/io/jenkins/plugins/opentelemetry/job/MonitoringPipelineListener.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered lines

Lines 279-280 are not covered by tests
}
return stepDescriptor.getDisplayName();
}

private String getStepType(@NonNull FlowNode node, @Nullable StepDescriptor stepDescriptor, @NonNull String type) {
if (stepDescriptor == null) {
return type;
Expand Down Expand Up @@ -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<String, Object> arguments = ArgumentsAction.getFilteredArguments(stepStartNode);
// Argument 'label'.
final String spanLabelArgument = (String) arguments.getOrDefault("label", stepName);
final String spanLabel = Strings.isNullOrEmpty(spanLabelArgument) ? stepName : spanLabelArgument;

Check warning on line 405 in src/main/java/io/jenkins/plugins/opentelemetry/job/MonitoringPipelineListener.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 405 is only partially covered, one branch is missing
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<?>) {

Check warning on line 417 in src/main/java/io/jenkins/plugins/opentelemetry/job/MonitoringPipelineListener.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 417 is only partially covered, one branch is missing
// Filter the list items and cast to SpanAttribute.
List<SpanAttribute> 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);

Check warning on line 436 in src/main/java/io/jenkins/plugins/opentelemetry/job/MonitoringPipelineListener.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered lines

Lines 433-436 are not covered by tests
}

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 {
Expand Down Expand Up @@ -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.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,
// 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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {

}

Check warning on line 56 in src/main/java/io/jenkins/plugins/opentelemetry/job/jenkins/AbstractPipelineListener.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered lines

Lines 51-56 are not covered by tests

@Override
public void onAtomicStep(@NonNull StepAtomNode node, @NonNull WorkflowRun run) {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
Expand Down Expand Up @@ -98,6 +100,8 @@ private void processCurrentNode(FlowNode node, WorkflowRun run) {
final Map<String, Object> 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);
}
Expand Down Expand Up @@ -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);

Check warning on line 225 in src/main/java/io/jenkins/plugins/opentelemetry/job/jenkins/GraphListenerAdapterToPipelineListener.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered lines

Lines 224-225 are not covered by tests
}
}
}

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()) : ""));

Check warning on line 232 in src/main/java/io/jenkins/plugins/opentelemetry/job/jenkins/GraphListenerAdapterToPipelineListener.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 232 is not covered by tests
try {
pipelineListener.onEndWithNewSpanStep(node, nextNode, run);
} catch (RuntimeException e) {
LOGGER.log(Level.WARNING, e, () -> "Exception invoking `onAfterEndWithNewSpanStep` on " + pipelineListener);

Check warning on line 236 in src/main/java/io/jenkins/plugins/opentelemetry/job/jenkins/GraphListenerAdapterToPipelineListener.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered lines

Lines 235-236 are not covered by tests
}
}
}

public void fireOnBeforeAtomicStep(@NonNull StepAtomNode node, @NonNull WorkflowRun run) {
for (PipelineListener pipelineListener : PipelineListener.all()) {
log(() -> "onBeforeAtomicStep(" + node.getDisplayName() + "): " + pipelineListener.toString());
Expand Down Expand Up @@ -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<String> message) {
log(Level.FINE, message);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,16 @@ static List<PipelineListener> 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
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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) {

Check warning on line 63 in src/main/java/io/jenkins/plugins/opentelemetry/job/jenkins/PipelineNodeUtil.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 63 is only partially covered, one branch is missing
return false;

Check warning on line 64 in src/main/java/io/jenkins/plugins/opentelemetry/job/jenkins/PipelineNodeUtil.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 64 is not covered by tests
}

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}
*/
Expand Down
Loading

0 comments on commit 3b27ca9

Please sign in to comment.