diff --git a/docs/monitoring-metrics.md b/docs/monitoring-metrics.md
index 76c7f3843..5985bbbef 100644
--- a/docs/monitoring-metrics.md
+++ b/docs/monitoring-metrics.md
@@ -24,7 +24,26 @@ or APIs ([here](https://www.elastic.co/guide/en/kibana/current/dashboard-import-
|------------------------------------------------|----------------------------------|
| | |
-## Jenkins Health Metrics
+## Build Duration
+
+**⚠️ In order to control metrics cardinality, the `ci.pipeline.run.duration` metrics are enabled by default
+aggregating the durations of all the jobs/pipelines under the umbrella `ci.pipeline.id=#other#`.
+To enable per job/pipeline metrics, use the allow and deny list setting the configuration parameters
+`otel.instrumentation.jenkins.run.metric.duration.allow_list` and `otel.instrumentation.jenkins.run.metric.duration.deny_list`.**
+
+* Name: `ci.pipeline.run.duration`
+* Type: Histogram with buckets: `1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096, 8192` (buckets subject to change)
+* Unit: `s`
+* Attributes:
+ * `ci.pipeline.id`: The full name of the Jenkins job if complying with the allow and deny lists specified through
+ configuration parameters documented below, otherwise `#other#` to limit the cardinality of the metric.
+ Example: `my-team/my-app/main`. See `hudson.model.AbstractItem#getFullName()`.
+ * `ci.pipeline.result`: `SUCCESS`, `UNSTABLE`, `FAILUIRE`, `NOT_BUILT`, `ABORTED`. See `hudson.model.Run#getResult()`.
+* Configuration parameters to control the cardinality of the `ci.pipeline.id` attribute:
+ * `otel.instrumentation.jenkins.run.metric.duration.allow_list`: Java regex, default value: `$^` (ie match nothing). Example `jenkins_folder_a/.*|jenkins_folder_b/.*`
+ * `otel.instrumentation.jenkins.run.metric.duration.deny_list`: Java regex, default value: `$^` (ie match nothing). Example `.*test.*`
+
+## Jenkins Build & Health Metrics
Inventory of health metrics collected by the Jenkins OpenTelemetry integration:
@@ -35,128 +54,142 @@ Inventory of health metrics collected by the Jenkins OpenTelemetry integration:
Attribute value |
Description |
+
+ /td>
+ | `s` |
+ |
+ |
+ Duration of runs |
+
ci.pipeline.run.active |
- 1 |
+ `{jobs}` |
+ |
+ |
+ Gauge of active jobs |
+
+
+ ci.pipeline.run.active |
+ `{jobs}` |
|
|
Gauge of active jobs |
ci.pipeline.run.launched |
- 1 |
+ `{jobs}` |
|
|
Job launched |
ci.pipeline.run.started |
- 1 |
+ `{jobs}` |
|
|
Job started |
ci.pipeline.run.completed |
- 1 |
+ `{jobs}` |
|
|
Job completed |
ci.pipeline.run.aborted |
- 1 |
+ `{jobs}` |
|
|
Job aborted |
ci.pipeline.run.success |
- 1 |
+ `{jobs}` |
|
|
Job successful |
ci.pipeline.run.failed |
- 1 |
+ `{jobs}` |
|
|
Job failed |
jenkins.executor.available |
- 1 |
+ `${executors}` |
label |
|
|
jenkins.executor.busy |
- 1 |
+ `${executors}` |
label |
|
|
jenkins.executor.idle |
- 1 |
+ `${executors}` |
label |
|
|
jenkins.executor.online |
- 1 |
+ `${executors}` |
label |
|
|
jenkins.executor.connecting |
- 1 |
+ `${executors}` |
label |
|
|
jenkins.executor.defined |
- 1 |
+ `${executors}` |
label |
|
|
jenkins.executor.queue |
- 1 |
+ `${items}` |
label |
|
|
jenkins.queue.waiting |
- 1 |
+ `${items}` |
|
|
Number of tasks in the queue with the status 'buildable' or 'pending' (see `Queue#getUnblockedItems()`) |
jenkins.queue.blocked |
- 1 |
+ `${items}` |
|
|
Number of blocked tasks in the queue. Note that waiting for an executor to be available is not a reason to be counted as blocked. (see `QueueListener#onEnterBlocked() - QueueListener#onLeaveBlocked()`) |
jenkins.queue.buildable |
- 1 |
+ `${items}` |
|
|
Number of tasks in the queue with the status 'buildable' or 'pending' (see `Queue#getBuildableItems()`]) |
jenkins.queue.left |
- 1 |
+ `${items}` |
|
|
Total count of tasks that have been processed (see [`QueueListener#onLeft`]()- |
@@ -189,42 +222,42 @@ Inventory of health metrics collected by the Jenkins OpenTelemetry integration:
jenkins.agents.total |
- 1 |
+ `{agents}` |
|
|
Number of agents |
jenkins.agents.online |
- 1 |
+ `{agents}` |
|
|
Number of online agents |
jenkins.agents.offline |
- 1 |
+ `{agents}` |
|
|
Number of offline agents |
jenkins.agents.launch.failure |
- 1 |
+ `{agents}` |
|
|
Number of failed launched agents |
jenkins.cloud.agents.completed |
- 1 |
+ `{agents}` |
|
|
Number of provisioned cloud agents |
jenkins.cloud.agents.launch.failure |
- 1 |
+ `{agents}` |
|
|
Number of failed cloud agents |
@@ -243,7 +276,7 @@ Inventory of health metrics collected by the Jenkins OpenTelemetry integration:
github.api.rate_limit.remaining_requests |
- 1 |
+ `{requests}` |
Always reported: github.api.url, github.authentication
For user based authentication:, enduser.id
@@ -261,28 +294,28 @@ Inventory of health metrics collected by the Jenkins OpenTelemetry integration:
|
jenkins.scm.event.pool_size |
- 1 |
+ `{events}` |
|
|
Thread pool size of the SCM Event queue processor |
jenkins.scm.event.active_threads |
- 1 |
+ `{threads}` |
|
|
Number of active threads of the SCM events thread pool |
jenkins.scm.event.queued_tasks |
- 1 |
+ `{tasks}` |
|
|
Number of events in the SCM event queue |
jenkins.scm.event.completed_tasks |
- 1 |
+ `{tasks}` |
|
|
Number of processed SCM events |
@@ -304,7 +337,7 @@ See OpenTelemetry [Semantic Conventions for Runtime Environment Metrics](https:/
process.runtime.jvm.buffer.count |
The number of buffers in the pool |
- gauge |
+ gauge |
pool |
direct, mapped, mapped - 'non-volatile memory' |
@@ -435,8 +468,8 @@ See OpenTelemetry [Semantic Conventions for Runtime Environment Metrics](https:/
## Jenkins Security Metrics
-| Metrics | Unit | Attribute Key | Attribute value | Description |
-|----------------------------------|-------|-----------------------|-------------------------|------------------------|
-| login | 1 | | | Login count |
-| login_success | 1 | | | Successful login count |
-| login_failure | 1 | | | Failed login count |
+| Metrics | Unit | Attribute Key | Attribute value | Description |
+|----------------------------------|-------------|-----------------------|-------------------------|------------------------|
+| login | ${logins} | | | Login count |
+| login_success | ${logins} | | | Successful login count |
+| login_failure | ${logins} | | | Failed login count |
diff --git a/src/main/java/io/jenkins/plugins/opentelemetry/JenkinsOpenTelemetryPluginConfiguration.java b/src/main/java/io/jenkins/plugins/opentelemetry/JenkinsOpenTelemetryPluginConfiguration.java
index 77f8bdcc3..e8d6ce5b2 100644
--- a/src/main/java/io/jenkins/plugins/opentelemetry/JenkinsOpenTelemetryPluginConfiguration.java
+++ b/src/main/java/io/jenkins/plugins/opentelemetry/JenkinsOpenTelemetryPluginConfiguration.java
@@ -179,7 +179,7 @@ public boolean configure(StaplerRequest req, JSONObject json) throws FormExcepti
try {
configureOpenTelemetrySdk();
save();
- } catch (ConfigurationException e) {
+ } catch (RuntimeException e) {
LOGGER.log(Level.WARNING, "Exception configuring OpenTelemetry SDK", e);
throw new FormException("Exception configuring OpenTelemetry SDK: " + e.getMessage(), e, "endpoint");
}
diff --git a/src/main/java/io/jenkins/plugins/opentelemetry/computer/MonitoringCloudListener.java b/src/main/java/io/jenkins/plugins/opentelemetry/computer/MonitoringCloudListener.java
index c6034ead2..c32002996 100644
--- a/src/main/java/io/jenkins/plugins/opentelemetry/computer/MonitoringCloudListener.java
+++ b/src/main/java/io/jenkins/plugins/opentelemetry/computer/MonitoringCloudListener.java
@@ -39,11 +39,11 @@ public void postConstruct() {
failureCloudCounter = meter.counterBuilder(JenkinsSemanticMetrics.JENKINS_CLOUD_AGENTS_FAILURE)
.setDescription("Number of failed cloud agents when provisioning")
- .setUnit("1")
+ .setUnit("{agents}")
.build();
totalCloudCount = meter.counterBuilder(JenkinsSemanticMetrics.JENKINS_CLOUD_AGENTS_COMPLETED)
.setDescription("Number of provisioned cloud agents")
- .setUnit("1")
+ .setUnit("{agents}")
.build();
}
diff --git a/src/main/java/io/jenkins/plugins/opentelemetry/init/JenkinsExecutorMonitoringInitializer.java b/src/main/java/io/jenkins/plugins/opentelemetry/init/JenkinsExecutorMonitoringInitializer.java
index 9cdf14dae..89734b063 100644
--- a/src/main/java/io/jenkins/plugins/opentelemetry/init/JenkinsExecutorMonitoringInitializer.java
+++ b/src/main/java/io/jenkins/plugins/opentelemetry/init/JenkinsExecutorMonitoringInitializer.java
@@ -48,7 +48,7 @@ public void postConstruct() {
final ObservableLongMeasurement onlineExecutors = meter.gaugeBuilder(JENKINS_EXECUTOR_ONLINE).setUnit("${executors}").setDescription("Online executors").ofLongs().buildObserver();
final ObservableLongMeasurement connectingExecutors = meter.gaugeBuilder(JENKINS_EXECUTOR_CONNECTING).setUnit("${executors}").setDescription("Connecting executors").ofLongs().buildObserver();
final ObservableLongMeasurement definedExecutors = meter.gaugeBuilder(JENKINS_EXECUTOR_DEFINED).setUnit("${executors}").setDescription("Defined executors").ofLongs().buildObserver();
- final ObservableLongMeasurement queueLength = meter.gaugeBuilder(JENKINS_EXECUTOR_QUEUE).setUnit("${executors}").setDescription("Defined executors").ofLongs().buildObserver();
+ final ObservableLongMeasurement queueLength = meter.gaugeBuilder(JENKINS_EXECUTOR_QUEUE).setUnit("${items}").setDescription("Executors queue items").ofLongs().buildObserver();
logger.log(Level.FINER, () -> "Metrics: " + availableExecutors + ", " + busyExecutors + ", " + idleExecutors + ", " + onlineExecutors + ", " + connectingExecutors + ", " + definedExecutors + ", " + queueLength);
meter.batchCallback(() -> {
diff --git a/src/main/java/io/jenkins/plugins/opentelemetry/job/MonitoringRunListener.java b/src/main/java/io/jenkins/plugins/opentelemetry/job/MonitoringRunListener.java
index a67201986..6647a9447 100644
--- a/src/main/java/io/jenkins/plugins/opentelemetry/job/MonitoringRunListener.java
+++ b/src/main/java/io/jenkins/plugins/opentelemetry/job/MonitoringRunListener.java
@@ -5,6 +5,10 @@
package io.jenkins.plugins.opentelemetry.job;
+import static java.util.Arrays.asList;
+import static java.util.Collections.unmodifiableList;
+
+import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import com.google.errorprone.annotations.MustBeClosed;
import edu.umd.cs.findbugs.annotations.NonNull;
@@ -28,6 +32,8 @@
import io.jenkins.plugins.opentelemetry.queue.RemoteSpanAction;
import io.jenkins.plugins.opentelemetry.semconv.JenkinsOtelSemanticAttributes;
import io.jenkins.plugins.opentelemetry.semconv.JenkinsSemanticMetrics;
+import io.opentelemetry.api.common.Attributes;
+import io.opentelemetry.api.metrics.DoubleHistogram;
import io.opentelemetry.api.metrics.LongCounter;
import io.opentelemetry.api.metrics.Meter;
import io.opentelemetry.api.trace.Span;
@@ -58,10 +64,13 @@
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
+import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Supplier;
import java.util.logging.Level;
import java.util.logging.Logger;
+import java.util.regex.Pattern;
+import java.util.regex.PatternSyntaxException;
import java.util.stream.Collectors;
import static com.google.common.base.Verify.verifyNotNull;
@@ -72,10 +81,20 @@
@Extension(dynamicLoadable = YesNoMaybe.YES, optional = true)
public class MonitoringRunListener extends OtelContextAwareAbstractRunListener implements OpenTelemetryLifecycleListener {
+ static final Pattern MATCH_ANYTHING = Pattern.compile(".*");
+ static final Pattern MATCH_NOTHING = Pattern.compile("$^");
+
+ // TODO support configurability of these histogram buckets. Note that the conversion from a string to a list of
+ // doubles will require boilerplate so we are interested in getting user feedback before implementing this.
+ static final List DURATION_SECONDS_BUCKETS =
+ unmodifiableList(
+ asList(1D, 2D, 4D, 8D, 16D, 32D, 64D, 128D, 256D, 512D, 1024D, 2048D, 4096D, 8192D));
+
protected static final Logger LOGGER = Logger.getLogger(MonitoringRunListener.class.getName());
private AtomicInteger activeRunGauge;
private List causeHandlers;
+ private DoubleHistogram runDurationHistogram;
private LongCounter runLaunchedCounter;
private LongCounter runStartedCounter;
private LongCounter runCompletedCounter;
@@ -83,6 +102,10 @@ public class MonitoringRunListener extends OtelContextAwareAbstractRunListener i
private LongCounter runSuccessCounter;
private LongCounter runFailedCounter;
private List runHandlers;
+ @VisibleForTesting
+ Pattern runDurationHistogramAllowList;
+ @VisibleForTesting
+ Pattern runDurationHistogramDenyList;
@PostConstruct
public void postConstruct() {
@@ -104,42 +127,75 @@ public void postConstruct() {
// METRICS
activeRunGauge = new AtomicInteger();
+
+ runDurationHistogram = meter.histogramBuilder(JenkinsSemanticMetrics.CI_PIPELINE_RUN_DURATION)
+ .setUnit("s")
+ .setExplicitBucketBoundariesAdvice(DURATION_SECONDS_BUCKETS)
+ .build();
+ runDurationHistogramAllowList = MATCH_ANYTHING; // allow all
+ runDurationHistogramDenyList = MATCH_NOTHING; // deny nothing
+
meter.gaugeBuilder(JenkinsSemanticMetrics.CI_PIPELINE_RUN_ACTIVE)
.ofLongs()
.setDescription("Gauge of active jobs")
.setUnit("{jobs}")
.buildWithCallback(valueObserver -> valueObserver.record(this.activeRunGauge.get()));
runLaunchedCounter =
- meter.counterBuilder(JenkinsSemanticMetrics.CI_PIPELINE_RUN_LAUNCHED)
- .setDescription("Job launched")
- .setUnit("{jobs}")
- .build();
+ meter.counterBuilder(JenkinsSemanticMetrics.CI_PIPELINE_RUN_LAUNCHED)
+ .setDescription("Job launched")
+ .setUnit("{jobs}")
+ .build();
runStartedCounter =
- meter.counterBuilder(JenkinsSemanticMetrics.CI_PIPELINE_RUN_STARTED)
- .setDescription("Job started")
- .setUnit("{jobs}")
- .build();
+ meter.counterBuilder(JenkinsSemanticMetrics.CI_PIPELINE_RUN_STARTED)
+ .setDescription("Job started")
+ .setUnit("{jobs}")
+ .build();
runSuccessCounter =
- meter.counterBuilder(JenkinsSemanticMetrics.CI_PIPELINE_RUN_SUCCESS)
- .setDescription("Job succeed")
- .setUnit("{jobs}")
- .build();
+ meter.counterBuilder(JenkinsSemanticMetrics.CI_PIPELINE_RUN_SUCCESS)
+ .setDescription("Job succeed")
+ .setUnit("{jobs}")
+ .build();
runFailedCounter =
meter.counterBuilder(JenkinsSemanticMetrics.CI_PIPELINE_RUN_FAILED)
.setDescription("Job failed")
.setUnit("{jobs}")
.build();
runAbortedCounter =
- meter.counterBuilder(JenkinsSemanticMetrics.CI_PIPELINE_RUN_ABORTED)
- .setDescription("Job aborted")
- .setUnit("{jobs}")
- .build();
+ meter.counterBuilder(JenkinsSemanticMetrics.CI_PIPELINE_RUN_ABORTED)
+ .setDescription("Job aborted")
+ .setUnit("{jobs}")
+ .build();
runCompletedCounter =
- meter.counterBuilder(JenkinsSemanticMetrics.CI_PIPELINE_RUN_COMPLETED)
- .setDescription("Job completed")
- .setUnit("{jobs}")
- .build();
+ meter.counterBuilder(JenkinsSemanticMetrics.CI_PIPELINE_RUN_COMPLETED)
+ .setDescription("Job completed")
+ .setUnit("{jobs}")
+ .build();
+ }
+ @Override
+ public void afterConfiguration(ConfigProperties configProperties) {
+ Pattern newRunDurationHistogramAllowList;
+ Pattern newRunDurationHistogramDenyList;
+ try {
+ newRunDurationHistogramAllowList = Optional
+ .ofNullable(configProperties.getString(JenkinsOtelSemanticAttributes.OTEL_INSTRUMENTATION_JENKINS_RUN_DURATION_ALLOW_LIST))
+ .map(Pattern::compile)
+ .orElse(MATCH_NOTHING);
+ } catch (PatternSyntaxException e) {
+ throw new IllegalArgumentException("Invalid regex for '" +
+ JenkinsOtelSemanticAttributes.OTEL_INSTRUMENTATION_JENKINS_RUN_DURATION_ALLOW_LIST + "'", e);
+ }
+ try {
+ newRunDurationHistogramDenyList = Optional
+ .ofNullable(configProperties.getString(JenkinsOtelSemanticAttributes.OTEL_INSTRUMENTATION_JENKINS_RUN_DURATION_DENY_LIST))
+ .map(Pattern::compile)
+ .orElse(MATCH_NOTHING);
+ } catch (PatternSyntaxException e) {
+ throw new IllegalArgumentException("Invalid regex for '" +
+ JenkinsOtelSemanticAttributes.OTEL_INSTRUMENTATION_JENKINS_RUN_DURATION_DENY_LIST + "'", e);
+ }
+ this.runDurationHistogramAllowList = newRunDurationHistogramAllowList;
+ this.runDurationHistogramDenyList = newRunDurationHistogramDenyList;
}
@NonNull
@@ -149,11 +205,11 @@ public List getCauseHandlers() {
@NonNull
public CauseHandler getCauseHandler(@NonNull Cause cause) throws NoSuchElementException {
- return getCauseHandlers().stream().filter(ch -> ch.isSupported(cause)).findFirst().get();
+ return getCauseHandlers().stream().filter(ch -> ch.isSupported(cause)).findFirst().orElseThrow();
}
@Override
- public void _onInitialize(@NonNull Run run) {
+ public void _onInitialize(@NonNull Run, ?> run) {
LOGGER.log(Level.FINE, () -> run.getFullDisplayName() + " - onInitialize");
activeRunGauge.incrementAndGet();
@@ -167,28 +223,28 @@ public void _onInitialize(@NonNull Run run) {
// TODO move this to a pluggable span enrichment API with implementations for different observability backends
rootSpanBuilder
- .setAttribute(JenkinsOtelSemanticAttributes.ELASTIC_TRANSACTION_TYPE, "job");
+ .setAttribute(JenkinsOtelSemanticAttributes.ELASTIC_TRANSACTION_TYPE, "job");
rootSpanBuilder
- .setAttribute(JenkinsOtelSemanticAttributes.CI_PIPELINE_ID, run.getParent().getFullName())
- .setAttribute(JenkinsOtelSemanticAttributes.CI_PIPELINE_NAME, run.getParent().getFullDisplayName())
- .setAttribute(JenkinsOtelSemanticAttributes.CI_PIPELINE_RUN_URL, runUrl)
- .setAttribute(JenkinsOtelSemanticAttributes.CI_PIPELINE_RUN_NUMBER, (long) run.getNumber())
- .setAttribute(JenkinsOtelSemanticAttributes.CI_PIPELINE_TYPE, OtelUtils.getProjectType(run));
+ .setAttribute(JenkinsOtelSemanticAttributes.CI_PIPELINE_ID, run.getParent().getFullName())
+ .setAttribute(JenkinsOtelSemanticAttributes.CI_PIPELINE_NAME, run.getParent().getFullDisplayName())
+ .setAttribute(JenkinsOtelSemanticAttributes.CI_PIPELINE_RUN_URL, runUrl)
+ .setAttribute(JenkinsOtelSemanticAttributes.CI_PIPELINE_RUN_NUMBER, (long) run.getNumber())
+ .setAttribute(JenkinsOtelSemanticAttributes.CI_PIPELINE_TYPE, OtelUtils.getProjectType(run));
// CULPRITS
Set culpritIds;
if (run instanceof WorkflowRun) {
culpritIds = ((WorkflowRun) run).getCulprits();
} else if (run instanceof AbstractBuild) {
- culpritIds = ((AbstractBuild) run).getCulprits();
+ culpritIds = ((AbstractBuild, ?>) run).getCulprits();
} else {
culpritIds = null;
}
if (culpritIds != null) {
rootSpanBuilder
.setAttribute(JenkinsOtelSemanticAttributes.CI_PIPELINE_RUN_COMMITTERS,
- culpritIds.stream().map(p -> p.getId()).collect(Collectors.toList()));
+ culpritIds.stream().map(User::getId).collect(Collectors.toList()));
}
// PARAMETERS
@@ -232,14 +288,14 @@ public void _onInitialize(@NonNull Run run) {
});
// CAUSES
- List causesDescriptions = ((List) run.getCauses()).stream().map(c -> getCauseHandler(c).getStructuredDescription(c)).collect(Collectors.toList());
+ List causesDescriptions = run.getCauses().stream().map(c -> getCauseHandler(c).getStructuredDescription(c)).collect(Collectors.toList());
rootSpanBuilder.setAttribute(JenkinsOtelSemanticAttributes.CI_PIPELINE_RUN_CAUSE, causesDescriptions);
- Optional optCause = run.getCauses().stream().findFirst();
+ Optional optCause = run.getCauses().stream().findFirst();
optCause.ifPresent(cause -> {
if (cause instanceof Cause.UpstreamCause) {
Cause.UpstreamCause upstreamCause = (Cause.UpstreamCause) cause;
- Run upstreamRun = upstreamCause.getUpstreamRun();
+ Run, ?> upstreamRun = upstreamCause.getUpstreamRun();
if (upstreamRun == null) {
// hudson.model.Cause.UpstreamCause.getUpstreamRun() can return null, probably if upstream job or build has been deleted.
} else {
@@ -287,8 +343,8 @@ public String get(@Nullable Map carrier, String key) {
// START initialize span
Span startSpan = getTracer().spanBuilder(JenkinsOtelSemanticAttributes.JENKINS_JOB_SPAN_PHASE_START_NAME)
- .setParent(Context.current().with(rootSpan))
- .startSpan();
+ .setParent(Context.current().with(rootSpan))
+ .startSpan();
LOGGER.log(Level.FINE, () -> run.getFullDisplayName() + " - begin " + OtelUtils.toDebugString(startSpan));
this.getTraceService().putRunPhaseSpan(run, startSpan);
@@ -299,7 +355,7 @@ public String get(@Nullable Map carrier, String key) {
}
@Override
- public void _onStarted(@NonNull Run run, @NonNull TaskListener listener) {
+ public void _onStarted(@NonNull Run, ?> run, @NonNull TaskListener listener) {
try (Scope parentScope = endPipelinePhaseSpan(run)) {
Span runSpan = getTracer().spanBuilder(JenkinsOtelSemanticAttributes.JENKINS_JOB_SPAN_PHASE_RUN_NAME).setParent(Context.current()).startSpan();
LOGGER.log(Level.FINE, () -> run.getFullDisplayName() + " - begin " + OtelUtils.toDebugString(runSpan));
@@ -311,7 +367,7 @@ public void _onStarted(@NonNull Run run, @NonNull TaskListener listener) {
}
@Override
- public void _onCompleted(@NonNull Run run, @NonNull TaskListener listener) {
+ public void _onCompleted(@NonNull Run, ?> run, @NonNull TaskListener listener) {
try (Scope parentScope = endPipelinePhaseSpan(run)) {
Span finalizeSpan = getTracer().spanBuilder(JenkinsOtelSemanticAttributes.JENKINS_JOB_SPAN_PHASE_FINALIZE_NAME).setParent(Context.current()).startSpan();
LOGGER.log(Level.FINE, () -> run.getFullDisplayName() + " - begin " + OtelUtils.toDebugString(finalizeSpan));
@@ -323,7 +379,7 @@ public void _onCompleted(@NonNull Run run, @NonNull TaskListener listener) {
@MustBeClosed
@NonNull
- protected Scope endPipelinePhaseSpan(@NonNull Run run) {
+ protected Scope endPipelinePhaseSpan(@NonNull Run, ?> run) {
Span pipelinePhaseSpan = verifyNotNull(Span.current(), "No pipelinePhaseSpan found in context");
pipelinePhaseSpan.end();
LOGGER.log(Level.FINE, () -> run.getFullDisplayName() + " - end " + OtelUtils.toDebugString(pipelinePhaseSpan));
@@ -334,7 +390,7 @@ protected Scope endPipelinePhaseSpan(@NonNull Run run) {
}
@Override
- public void _onFinalized(@NonNull Run run) {
+ public void _onFinalized(@NonNull Run, ?> run) {
try (Scope parentScope = endPipelinePhaseSpan(run)) {
Span parentSpan = Span.current();
@@ -357,7 +413,7 @@ public void _onFinalized(@NonNull Run run) {
if (Result.SUCCESS.equals(runResult)) {
parentSpan.setStatus(StatusCode.OK, runResult.toString());
- } else if (Result.FAILURE.equals(runResult) || Result.UNSTABLE.equals(runResult)){
+ } else if (Result.FAILURE.equals(runResult) || Result.UNSTABLE.equals(runResult)) {
parentSpan.setAttribute(ExceptionAttributes.EXCEPTION_TYPE, "PIPELINE_" + runResult);
parentSpan.setAttribute(ExceptionAttributes.EXCEPTION_MESSAGE, "PIPELINE_" + runResult);
parentSpan.setStatus(StatusCode.ERROR, runResult.toString());
@@ -367,7 +423,7 @@ public void _onFinalized(@NonNull Run run) {
}
// NODE
if (run instanceof AbstractBuild) {
- Node node = ((AbstractBuild) run).getBuiltOn();
+ Node node = ((AbstractBuild, ?>) run).getBuiltOn();
if (node != null) {
parentSpan.setAttribute(JenkinsOtelSemanticAttributes.JENKINS_STEP_AGENT_LABEL, node.getLabelString());
parentSpan.setAttribute(JenkinsOtelSemanticAttributes.CI_PIPELINE_AGENT_ID, node.getNodeName());
@@ -381,7 +437,6 @@ public void _onFinalized(@NonNull Run run) {
this.getTraceService().purgeRun(run);
-
Result result = verifyNotNull(run.getResult(), "%s", run);
if (result.isCompleteBuild()) {
@@ -395,6 +450,19 @@ public void _onFinalized(@NonNull Run run) {
} else {
this.runAbortedCounter.add(1);
}
+
+ String jobFullName = run.getParent().getFullName();
+ String pipelineId =
+ runDurationHistogramAllowList.matcher(jobFullName).matches()
+ &&
+ !runDurationHistogramDenyList.matcher(jobFullName).matches() ?
+ jobFullName : "#other#";
+ runDurationHistogram.record(
+ TimeUnit.SECONDS.convert(run.getDuration(), TimeUnit.MILLISECONDS),
+ Attributes.of(
+ JenkinsOtelSemanticAttributes.CI_PIPELINE_ID, pipelineId,
+ JenkinsOtelSemanticAttributes.CI_PIPELINE_RUN_RESULT, result.toString())
+ );
} finally {
activeRunGauge.decrementAndGet();
}
diff --git a/src/main/java/io/jenkins/plugins/opentelemetry/job/opentelemetry/OtelContextAwareAbstractRunListener.java b/src/main/java/io/jenkins/plugins/opentelemetry/job/opentelemetry/OtelContextAwareAbstractRunListener.java
index 44fb6c4b5..886654965 100644
--- a/src/main/java/io/jenkins/plugins/opentelemetry/job/opentelemetry/OtelContextAwareAbstractRunListener.java
+++ b/src/main/java/io/jenkins/plugins/opentelemetry/job/opentelemetry/OtelContextAwareAbstractRunListener.java
@@ -30,7 +30,7 @@
* {@link RunListener} that setups the OpenTelemetry {@link io.opentelemetry.context.Context}
* with the current {@link Span}.
*/
-public abstract class OtelContextAwareAbstractRunListener extends RunListener {
+public abstract class OtelContextAwareAbstractRunListener extends RunListener> {
private final static Logger LOGGER = Logger.getLogger(OtelContextAwareAbstractRunListener.class.getName());
@@ -56,18 +56,18 @@ public final void setOpenTelemetry(@NonNull ReconfigurableOpenTelemetry jenkinsC
}
@Override
- public final void onCompleted(@NonNull Run run, @NonNull TaskListener listener) {
+ public final void onCompleted(@NonNull Run, ?> run, @NonNull TaskListener listener) {
Span span = getTraceService().getSpan(run);
try (Scope scope = span.makeCurrent()) {
this._onCompleted(run, listener);
}
}
- public void _onCompleted(@NonNull Run run, @NonNull TaskListener listener) {
+ public void _onCompleted(@NonNull Run, ?> run, @NonNull TaskListener listener) {
}
@Override
- public final void onFinalized(@NonNull Run run) {
+ public final void onFinalized(@NonNull Run, ?> run) {
Span span = getTraceService().getSpan(run);
try (Scope scope = span.makeCurrent()) {
this._onFinalized(run);
@@ -75,26 +75,26 @@ public final void onFinalized(@NonNull Run run) {
}
- public void _onFinalized(Run run) {
+ public void _onFinalized(Run, ?> run) {
}
@Override
- public final void onInitialize(@NonNull Run run) {
+ public final void onInitialize(@NonNull Run, ?> run) {
this._onInitialize(run);
}
- public void _onInitialize(@NonNull Run run) {
+ public void _onInitialize(@NonNull Run, ?> run) {
}
@Override
- public final void onStarted(@NonNull Run run, @NonNull TaskListener listener) {
+ public final void onStarted(@NonNull Run, ?> run, @NonNull TaskListener listener) {
Span span = getTraceService().getSpan(run);
try (Scope scope = span.makeCurrent()) {
this._onStarted(run, listener);
}
}
- public void _onStarted(@NonNull Run run, @NonNull TaskListener listener) {
+ public void _onStarted(@NonNull Run, ?> run, @NonNull TaskListener listener) {
}
@Override
@@ -112,14 +112,14 @@ public Environment _setUpEnvironment(@NonNull AbstractBuild build, @NonNull Laun
}
@Override
- public final void onDeleted(@NonNull Run run) {
+ public final void onDeleted(@NonNull Run, ?> run) {
Span span = getTraceService().getSpan(run);
try (Scope ignored = span.makeCurrent()) {
this._onDeleted(run);
}
}
- public void _onDeleted(@NonNull Run run) {
+ public void _onDeleted(@NonNull Run, ?> run) {
}
@NonNull
@@ -137,7 +137,7 @@ public Meter getMeter() {
return meter;
}
- public ConfigProperties getConfigProperties() {
+ protected ConfigProperties getConfigProperties() {
return configProperties;
}
}
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 f03d31996..71f2ca239 100644
--- a/src/main/java/io/jenkins/plugins/opentelemetry/semconv/JenkinsOtelSemanticAttributes.java
+++ b/src/main/java/io/jenkins/plugins/opentelemetry/semconv/JenkinsOtelSemanticAttributes.java
@@ -7,6 +7,8 @@
import hudson.PluginWrapper;
import hudson.model.Computer;
+import hudson.model.Job;
+import hudson.model.Run;
import io.jenkins.plugins.opentelemetry.api.semconv.JenkinsAttributes;
import io.opentelemetry.api.common.AttributeKey;
import jenkins.model.Jenkins;
@@ -21,6 +23,9 @@
public final class JenkinsOtelSemanticAttributes extends JenkinsAttributes {
public static final AttributeKey CI_PIPELINE_TYPE = AttributeKey.stringKey("ci.pipeline.type");
public static final AttributeKey CI_PIPELINE_MULTIBRANCH_TYPE = AttributeKey.stringKey("ci.pipeline.multibranch.type");
+ /**
+ * @see Job#getFullName()
+ */
public static final AttributeKey CI_PIPELINE_ID = AttributeKey.stringKey("ci.pipeline.id");
public static final AttributeKey CI_PIPELINE_NAME = AttributeKey.stringKey("ci.pipeline.name");
public static final AttributeKey CI_PIPELINE_TEMPLATE_ID = AttributeKey.stringKey("ci.pipeline.template.id");
@@ -42,6 +47,9 @@ public final class JenkinsOtelSemanticAttributes extends JenkinsAttributes {
public static final AttributeKey> CI_PIPELINE_RUN_PARAMETER_IS_SENSITIVE = AttributeKey.booleanArrayKey("ci.pipeline.parameter.sensitive");
public static final AttributeKey> CI_PIPELINE_RUN_PARAMETER_NAME = AttributeKey.stringArrayKey("ci.pipeline.parameter.name");
public static final AttributeKey> CI_PIPELINE_RUN_PARAMETER_VALUE = AttributeKey.stringArrayKey("ci.pipeline.parameter.value");
+ /**
+ * @see Run#getResult()
+ */
public static final AttributeKey CI_PIPELINE_RUN_RESULT = AttributeKey.stringKey("ci.pipeline.run.result");
public static final AttributeKey CI_PIPELINE_RUN_URL = AttributeKey.stringKey("ci.pipeline.run.url");
public static final AttributeKey CI_PIPELINE_RUN_USER = AttributeKey.stringKey("ci.pipeline.run.user");
@@ -135,6 +143,8 @@ public final class JenkinsOtelSemanticAttributes extends JenkinsAttributes {
public static final String OTEL_INSTRUMENTATION_JENKINS_WEB_ENABLED = "otel.instrumentation.jenkins.web.enabled";
public static final String OTEL_INSTRUMENTATION_JENKINS_REMOTE_SPAN_ENABLED = "otel.instrumentation.jenkins.remote.span.enabled";
+ public static final String OTEL_INSTRUMENTATION_JENKINS_RUN_DURATION_ALLOW_LIST = "otel.instrumentation.jenkins.run.metric.duration.allow_list";
+ public static final String OTEL_INSTRUMENTATION_JENKINS_RUN_DURATION_DENY_LIST = "otel.instrumentation.jenkins.run.metric.duration.deny_list";
/**
* Instrument Jenkins Remoting from the Jenkins controller to Jenkins build agents
*/
diff --git a/src/main/java/io/jenkins/plugins/opentelemetry/semconv/JenkinsSemanticMetrics.java b/src/main/java/io/jenkins/plugins/opentelemetry/semconv/JenkinsSemanticMetrics.java
index 1234cf00d..fb0bfc437 100644
--- a/src/main/java/io/jenkins/plugins/opentelemetry/semconv/JenkinsSemanticMetrics.java
+++ b/src/main/java/io/jenkins/plugins/opentelemetry/semconv/JenkinsSemanticMetrics.java
@@ -7,6 +7,7 @@
public class JenkinsSemanticMetrics {
public static final String CI_PIPELINE_RUN_ACTIVE = "ci.pipeline.run.active";
+ public static final String CI_PIPELINE_RUN_DURATION = "ci.pipeline.run.duration";
public static final String CI_PIPELINE_RUN_LAUNCHED = "ci.pipeline.run.launched";
public static final String CI_PIPELINE_RUN_STARTED = "ci.pipeline.run.started";
public static final String CI_PIPELINE_RUN_COMPLETED = "ci.pipeline.run.completed";
diff --git a/src/test/java/io/jenkins/plugins/opentelemetry/job/MonitoringRunListenerTest.java b/src/test/java/io/jenkins/plugins/opentelemetry/job/MonitoringRunListenerTest.java
new file mode 100644
index 000000000..7e3b3ab08
--- /dev/null
+++ b/src/test/java/io/jenkins/plugins/opentelemetry/job/MonitoringRunListenerTest.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright The Original Author or Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.jenkins.plugins.opentelemetry.job;
+
+import io.opentelemetry.sdk.autoconfigure.spi.internal.DefaultConfigProperties;
+import org.junit.Test;
+
+import java.util.Map;
+
+import static org.junit.Assert.*;
+
+public class MonitoringRunListenerTest {
+
+ @Test
+ public void test_default_allow_deny_list() {
+ MonitoringRunListener monitoringRunListener = new MonitoringRunListener();
+ Map configProperties = Map.of();
+ monitoringRunListener.afterConfiguration(DefaultConfigProperties.createFromMap(configProperties));
+ String jobFullName = "my-team/my-war/main";
+ assertFalse(monitoringRunListener.runDurationHistogramAllowList.matcher(jobFullName).matches());
+ assertFalse(monitoringRunListener.runDurationHistogramDenyList.matcher(jobFullName).matches());
+
+ }
+
+ @Test
+ public void test_deny_list_matching() {
+ MonitoringRunListener monitoringRunListener = new MonitoringRunListener();
+ Map configProperties = Map.of("otel.instrumentation.jenkins.run.metric.duration.allow_list", "my-team/.*",
+ "otel.instrumentation.jenkins.run.metric.duration.deny_list", ".*test.*");
+ monitoringRunListener.afterConfiguration(DefaultConfigProperties.createFromMap(configProperties));
+ String jobFullName = "my-team/my-war/test-123";
+ assertTrue(monitoringRunListener.runDurationHistogramAllowList.matcher(jobFullName).matches());
+ assertTrue(monitoringRunListener.runDurationHistogramDenyList.matcher(jobFullName).matches());
+ }
+
+ @Test
+ public void test_deny_list_not_matching() {
+ MonitoringRunListener monitoringRunListener = new MonitoringRunListener();
+ Map configProperties = Map.of("otel.instrumentation.jenkins.run.metric.duration.allow_list", "my-team/.*",
+ "otel.instrumentation.jenkins.run.metric.duration.deny_list", ".*test.*");
+ monitoringRunListener.afterConfiguration(DefaultConfigProperties.createFromMap(configProperties));
+ String jobFullName = "my-team/my-war/main";
+ assertTrue(monitoringRunListener.runDurationHistogramAllowList.matcher(jobFullName).matches());
+ assertFalse(monitoringRunListener.runDurationHistogramDenyList.matcher(jobFullName).matches());
+ }
+
+
+ @Test
+ public void test_allow_list_matching() {
+ MonitoringRunListener monitoringRunListener = new MonitoringRunListener();
+ Map configProperties = Map.of("otel.instrumentation.jenkins.run.metric.duration.allow_list", "my-team/.*");
+ monitoringRunListener.afterConfiguration(DefaultConfigProperties.createFromMap(configProperties));
+ String jobFullName = "my-team/my-war/main";
+ assertTrue(monitoringRunListener.runDurationHistogramAllowList.matcher(jobFullName).matches());
+ assertFalse(monitoringRunListener.runDurationHistogramDenyList.matcher(jobFullName).matches());
+ }
+
+ @Test
+ public void test_allow_list_not_matching() {
+ MonitoringRunListener monitoringRunListener = new MonitoringRunListener();
+ Map configProperties = Map.of("otel.instrumentation.jenkins.run.metric.duration.allow_list", "my-team/.*");
+ monitoringRunListener.afterConfiguration(DefaultConfigProperties.createFromMap(configProperties));
+ String jobFullName = "another-team/my-war/main";
+ assertFalse(monitoringRunListener.runDurationHistogramAllowList.matcher(jobFullName).matches());
+ assertFalse(monitoringRunListener.runDurationHistogramDenyList.matcher(jobFullName).matches());
+ }
+
+}
\ No newline at end of file