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 da713ba8b..4dde1186f 100644 --- a/src/main/java/io/jenkins/plugins/opentelemetry/init/JenkinsExecutorMonitoringInitializer.java +++ b/src/main/java/io/jenkins/plugins/opentelemetry/init/JenkinsExecutorMonitoringInitializer.java @@ -14,16 +14,21 @@ import hudson.model.Node; import io.jenkins.plugins.opentelemetry.JenkinsControllerOpenTelemetry; import io.jenkins.plugins.opentelemetry.api.OpenTelemetryLifecycleListener; +import io.jenkins.plugins.opentelemetry.opentelemetry.SemconvStability; +import io.jenkins.plugins.opentelemetry.semconv.CicdMetrics; import io.jenkins.plugins.opentelemetry.semconv.ExtendedJenkinsAttributes; +import io.opentelemetry.api.OpenTelemetry; import io.opentelemetry.api.common.Attributes; import io.opentelemetry.api.metrics.Meter; import io.opentelemetry.api.metrics.ObservableLongMeasurement; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; +import io.opentelemetry.semconv.incubating.CicdIncubatingAttributes; import java.util.Objects; import java.util.Optional; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.logging.Level; import java.util.logging.Logger; -import javax.annotation.PostConstruct; import javax.inject.Inject; import jenkins.YesNoMaybe; import jenkins.model.Jenkins; @@ -36,25 +41,45 @@ public class JenkinsExecutorMonitoringInitializer implements OpenTelemetryLifecy @Inject JenkinsControllerOpenTelemetry jenkinsControllerOpenTelemetry; + @Inject + SemconvStability semconvStability; + + final AtomicBoolean initialized = new AtomicBoolean(false); + public JenkinsExecutorMonitoringInitializer() { logger.log(Level.FINE, () -> "JenkinsExecutorMonitoringInitializer constructor"); } - @PostConstruct - public void postConstruct() { + @Override + public synchronized void afterConfiguration(ConfigProperties configProperties) { + if (initialized.get()) { + logger.log( + Level.FINE, + () -> "JenkinsExecutorMonitoringInitializer already initialized. " + + "This component doesn't support reconfiguration beyond changing the OTel SDK. " + + "It doesn't support reconfiguration of 'otel.semconv-stability.opt-in' 'cicd' vs 'cicd/dup' "); + return; + } + initialized.set(true); - logger.log(Level.FINE, () -> "Start monitoring Jenkins controller executor pool..."); + logger.log(Level.INFO, () -> "Start monitoring Jenkins controller executor pool..."); Meter meter = Objects.requireNonNull(jenkinsControllerOpenTelemetry).getDefaultMeter(); + Meter oldSemConventionsMeter = semconvStability.emitOldCicdSemconv() + ? meter + : OpenTelemetry.noop().getMeter("jenkins.opentelemetry"); + Meter newSemConventionsMeter = semconvStability.emitStableCicdSemconv() + ? meter + : OpenTelemetry.noop().getMeter("jenkins.opentelemetry"); final ObservableLongMeasurement queueLength = meter.gaugeBuilder(JENKINS_EXECUTOR_QUEUE) .setUnit("${items}") .setDescription("Executors queue items") .ofLongs() .buildObserver(); - final ObservableLongMeasurement totalExecutors = meter.gaugeBuilder(JENKINS_EXECUTOR_TOTAL) + final ObservableLongMeasurement executors = meter.gaugeBuilder(JENKINS_EXECUTOR_COUNT) .setUnit("${executors}") - .setDescription("Total executors") + .setDescription("Count of executors per label") .ofLongs() .buildObserver(); final ObservableLongMeasurement nodes = meter.gaugeBuilder(JENKINS_NODE) @@ -62,9 +87,126 @@ public void postConstruct() { .setDescription("Nodes") .ofLongs() .buildObserver(); - final ObservableLongMeasurement executors = meter.gaugeBuilder(JENKINS_EXECUTOR_COUNT) + + meter.batchCallback( + () -> { + logger.log(Level.FINE, () -> "Recording Jenkins controller executor pool metrics..."); + Jenkins jenkins = Jenkins.getInstanceOrNull(); + if (jenkins == null) { + logger.log(Level.FINE, "Jenkins instance is null, skipping executor pool metrics recording"); + return; + } + + // TOTAL EXECUTORS + AtomicInteger nodeOnline = new AtomicInteger(); + AtomicInteger nodeOffline = new AtomicInteger(); + + jenkins.getNodes().stream() + .map(Node::toComputer) + .filter(Objects::nonNull) + .forEach(node -> { + if (node.isOnline()) { + nodeOnline.incrementAndGet(); + } else { + nodeOffline.incrementAndGet(); + } + }); + + nodes.record(nodeOnline.get(), Attributes.of(STATUS, "online")); + nodes.record(nodeOffline.get(), Attributes.of(STATUS, "offline")); + + // PER LABEL + jenkins.getLabels().forEach(label -> { + LoadStatistics.LoadStatisticsSnapshot loadStatisticsSnapshot = + label.loadStatistics.computeSnapshot(); + Attributes attributes = Attributes.of(ExtendedJenkinsAttributes.LABEL, label.getDisplayName()); + + executors.record( + loadStatisticsSnapshot.getBusyExecutors(), + attributes.toBuilder().put(STATUS, "busy").build()); + executors.record( + loadStatisticsSnapshot.getIdleExecutors(), + attributes.toBuilder().put(STATUS, "idle").build()); + executors.record( + loadStatisticsSnapshot.getConnectingExecutors(), + attributes.toBuilder().put(STATUS, "connecting").build()); + queueLength.record(loadStatisticsSnapshot.getQueueLength(), attributes); + }); + }, + executors, + nodes, + queueLength); + + ObservableLongMeasurement cicdWorkers = CicdMetrics.newCiCdWorkerCounter(newSemConventionsMeter); + + newSemConventionsMeter.batchCallback( + () -> { + logger.log(Level.FINE, () -> "Recording Jenkins controller executor pool metrics..."); + Jenkins jenkins = Jenkins.getInstanceOrNull(); + if (jenkins == null) { + logger.log(Level.FINE, "Jenkins instance is null, skipping executor pool metrics recording"); + return; + } + + // EXECUTORS - AGGREGATED INDICATORS + AtomicInteger totalExecutorsIdle = new AtomicInteger(); + AtomicInteger totalExecutorsBusy = new AtomicInteger(); + AtomicInteger totalExecutorsOffline = new AtomicInteger(); + + if (jenkins.getNumExecutors() > 0) { + Optional.ofNullable(jenkins.toComputer()) + .map(Computer::getExecutors) + .ifPresent(e -> e.forEach(executor -> { + if (executor.isIdle()) { + totalExecutorsIdle.incrementAndGet(); + } else { + totalExecutorsBusy.incrementAndGet(); + } + })); + } + jenkins.getNodes().stream() + .map(Node::toComputer) + .filter(Objects::nonNull) + .forEach(node -> { + if (node.isOnline()) { + node.getExecutors().forEach(executor -> { + if (executor.isIdle()) { + totalExecutorsIdle.incrementAndGet(); + } else { + totalExecutorsBusy.incrementAndGet(); + } + }); + } else { + totalExecutorsOffline.addAndGet(node.countExecutors()); + } + }); + + cicdWorkers.record( + totalExecutorsBusy.get(), + Attributes.of( + CicdIncubatingAttributes.CICD_WORKER_STATE, + CicdIncubatingAttributes.CicdWorkerStateIncubatingValues.BUSY)); + cicdWorkers.record( + totalExecutorsIdle.get(), + Attributes.of( + CicdIncubatingAttributes.CICD_WORKER_STATE, + CicdIncubatingAttributes.CicdWorkerStateIncubatingValues.AVAILABLE)); + cicdWorkers.record( + totalExecutorsOffline.get(), + Attributes.of( + CicdIncubatingAttributes.CICD_WORKER_STATE, + CicdIncubatingAttributes.CicdWorkerStateIncubatingValues.OFFLINE)); + }, + cicdWorkers); + + createOldSemanticConventionsMeasurements(oldSemConventionsMeter); + } + + private static void createOldSemanticConventionsMeasurements(Meter oldSemConventionsMeter) { + final ObservableLongMeasurement totalExecutors = oldSemConventionsMeter + .gaugeBuilder(JENKINS_EXECUTOR_TOTAL) .setUnit("${executors}") - .setDescription("Count of executors per label") + .setDescription("Total executors") .ofLongs() .buildObserver(); @@ -72,43 +214,44 @@ public void postConstruct() { // * `jenkins.executor` metric with the `status` and `label`attributes // * `jenkins.node` metric with the `status` attribute // * `jenkins.executor.total` metric with the `status` attribute - final ObservableLongMeasurement availableExecutors = meter.gaugeBuilder(JENKINS_EXECUTOR_AVAILABLE) + final ObservableLongMeasurement availableExecutors = oldSemConventionsMeter + .gaugeBuilder(JENKINS_EXECUTOR_AVAILABLE) .setUnit("${executors}") .setDescription("Available executors") .ofLongs() .buildObserver(); - final ObservableLongMeasurement busyExecutors = meter.gaugeBuilder(JENKINS_EXECUTOR_BUSY) + final ObservableLongMeasurement busyExecutors = oldSemConventionsMeter + .gaugeBuilder(JENKINS_EXECUTOR_BUSY) .setUnit("${executors}") .setDescription("Busy executors") .ofLongs() .buildObserver(); - final ObservableLongMeasurement idleExecutors = meter.gaugeBuilder(JENKINS_EXECUTOR_IDLE) + final ObservableLongMeasurement idleExecutors = oldSemConventionsMeter + .gaugeBuilder(JENKINS_EXECUTOR_IDLE) .setUnit("${executors}") .setDescription("Idle executors") .ofLongs() .buildObserver(); - final ObservableLongMeasurement onlineExecutors = meter.gaugeBuilder(JENKINS_EXECUTOR_ONLINE) + final ObservableLongMeasurement onlineExecutors = oldSemConventionsMeter + .gaugeBuilder(JENKINS_EXECUTOR_ONLINE) .setUnit("${executors}") .setDescription("Online executors") .ofLongs() .buildObserver(); - final ObservableLongMeasurement connectingExecutors = meter.gaugeBuilder(JENKINS_EXECUTOR_CONNECTING) + final ObservableLongMeasurement connectingExecutors = oldSemConventionsMeter + .gaugeBuilder(JENKINS_EXECUTOR_CONNECTING) .setUnit("${executors}") .setDescription("Connecting executors") .ofLongs() .buildObserver(); - final ObservableLongMeasurement definedExecutors = meter.gaugeBuilder(JENKINS_EXECUTOR_DEFINED) + final ObservableLongMeasurement definedExecutors = oldSemConventionsMeter + .gaugeBuilder(JENKINS_EXECUTOR_DEFINED) .setUnit("${executors}") .setDescription("Defined executors") .ofLongs() .buildObserver(); - logger.log( - Level.FINER, - () -> "Metrics: " + availableExecutors + ", " + busyExecutors + ", " + idleExecutors + ", " - + onlineExecutors + ", " + connectingExecutors + ", " + definedExecutors + ", " + queueLength); - - meter.batchCallback( + oldSemConventionsMeter.batchCallback( () -> { logger.log(Level.FINE, () -> "Recording Jenkins controller executor pool metrics..."); Jenkins jenkins = Jenkins.getInstanceOrNull(); @@ -117,14 +260,11 @@ public void postConstruct() { return; } - // TOTAL EXECUTORS + // EXECUTORS - AGGREGATED INDICATORS AtomicInteger totalExecutorsIdle = new AtomicInteger(); AtomicInteger totalExecutorsBusy = new AtomicInteger(); - AtomicInteger nodeOnline = new AtomicInteger(); - AtomicInteger nodeOffline = new AtomicInteger(); if (jenkins.getNumExecutors() > 0) { - nodeOnline.incrementAndGet(); Optional.ofNullable(jenkins.toComputer()) .map(Computer::getExecutors) .ifPresent(e -> e.forEach(executor -> { @@ -140,7 +280,6 @@ public void postConstruct() { .filter(Objects::nonNull) .forEach(node -> { if (node.isOnline()) { - nodeOnline.incrementAndGet(); node.getExecutors().forEach(executor -> { if (executor.isIdle()) { totalExecutorsIdle.incrementAndGet(); @@ -149,32 +288,19 @@ public void postConstruct() { } }); } else { - nodeOffline.incrementAndGet(); + // offline } }); totalExecutors.record(totalExecutorsBusy.get(), Attributes.of(STATUS, "busy")); totalExecutors.record(totalExecutorsIdle.get(), Attributes.of(STATUS, "idle")); - nodes.record(nodeOnline.get(), Attributes.of(STATUS, "online")); - nodes.record(nodeOffline.get(), Attributes.of(STATUS, "offline")); - // PER LABEL + // EXECUTORS - PER LABEL INDICATORS jenkins.getLabels().forEach(label -> { LoadStatistics.LoadStatisticsSnapshot loadStatisticsSnapshot = label.loadStatistics.computeSnapshot(); Attributes attributes = Attributes.of(ExtendedJenkinsAttributes.LABEL, label.getDisplayName()); - executors.record( - loadStatisticsSnapshot.getBusyExecutors(), - attributes.toBuilder().put(STATUS, "busy").build()); - executors.record( - loadStatisticsSnapshot.getIdleExecutors(), - attributes.toBuilder().put(STATUS, "idle").build()); - executors.record( - loadStatisticsSnapshot.getConnectingExecutors(), - attributes.toBuilder().put(STATUS, "connecting").build()); - queueLength.record(loadStatisticsSnapshot.getQueueLength(), attributes); - // TODO the metrics below should be deprecated in favor of `jenkins.executor` metric with the // `status` // and `label`attributes @@ -192,9 +318,6 @@ public void postConstruct() { onlineExecutors, connectingExecutors, definedExecutors, - totalExecutors, - executors, - nodes, - queueLength); + totalExecutors); } } 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 e6948dc38..fcf375133 100644 --- a/src/main/java/io/jenkins/plugins/opentelemetry/job/MonitoringRunListener.java +++ b/src/main/java/io/jenkins/plugins/opentelemetry/job/MonitoringRunListener.java @@ -6,8 +6,6 @@ package io.jenkins.plugins.opentelemetry.job; import static com.google.common.base.Verify.verifyNotNull; -import static java.util.Arrays.asList; -import static java.util.Collections.unmodifiableList; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; @@ -30,13 +28,19 @@ import io.jenkins.plugins.opentelemetry.job.cause.CauseHandler; import io.jenkins.plugins.opentelemetry.job.opentelemetry.OtelContextAwareAbstractRunListener; import io.jenkins.plugins.opentelemetry.job.runhandler.RunHandler; +import io.jenkins.plugins.opentelemetry.opentelemetry.SemconvStability; import io.jenkins.plugins.opentelemetry.queue.RemoteSpanAction; +import io.jenkins.plugins.opentelemetry.semconv.CicdMetrics; import io.jenkins.plugins.opentelemetry.semconv.ConfigurationKey; import io.jenkins.plugins.opentelemetry.semconv.ExtendedJenkinsAttributes; import io.jenkins.plugins.opentelemetry.semconv.JenkinsMetrics; +import io.opentelemetry.api.OpenTelemetry; import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.incubator.metrics.ExtendedDoubleHistogramBuilder; import io.opentelemetry.api.metrics.DoubleHistogram; +import io.opentelemetry.api.metrics.DoubleHistogramBuilder; import io.opentelemetry.api.metrics.LongCounter; +import io.opentelemetry.api.metrics.LongUpDownCounter; import io.opentelemetry.api.metrics.Meter; import io.opentelemetry.api.trace.Span; import io.opentelemetry.api.trace.SpanBuilder; @@ -51,7 +55,9 @@ import io.opentelemetry.context.Scope; import io.opentelemetry.context.propagation.TextMapGetter; import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; +import io.opentelemetry.semconv.ErrorAttributes; import io.opentelemetry.semconv.ExceptionAttributes; +import io.opentelemetry.semconv.incubating.CicdIncubatingAttributes; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -75,11 +81,12 @@ import org.jenkinsci.plugins.workflow.support.steps.build.BuildUpstreamCause; /** - * TODO support reconfiguration + * TODO support reconfiguration. allow and deny lists are NOT reconfigurable at runtime for the moment. */ @Extension(dynamicLoadable = YesNoMaybe.YES, optional = true) public class MonitoringRunListener extends OtelContextAwareAbstractRunListener implements OpenTelemetryLifecycleListener { + static final String PIPELINE_NAME_OTHER = "#other#"; static final Pattern MATCH_ANYTHING = Pattern.compile(".*"); static final Pattern MATCH_NOTHING = Pattern.compile("$^"); @@ -87,19 +94,52 @@ public class MonitoringRunListener extends OtelContextAwareAbstractRunListener // 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)); + List.of(1D, 2D, 4D, 8D, 16D, 32D, 64D, 128D, 256D, 512D, 1024D, 2048D, 4096D, 8192D); protected static final Logger LOGGER = Logger.getLogger(MonitoringRunListener.class.getName()); + /** + * @deprecated use {@link #cicdPipelineRunActiveCounter} + */ + @Deprecated private AtomicInteger activeRunGauge; - private List causeHandlers; + /** + * @deprecated use {@link #cicdPipelineRunDurationHistogram} + */ + @Deprecated private DoubleHistogram runDurationHistogram; + /** + * @deprecated use {@link #cicdPipelineRunDurationHistogram} + */ + @Deprecated private LongCounter runLaunchedCounter; + /** + * @deprecated use {@link #cicdPipelineRunDurationHistogram} + */ + @Deprecated private LongCounter runStartedCounter; + /** + * @deprecated use {@link #cicdPipelineRunDurationHistogram} + */ + @Deprecated private LongCounter runCompletedCounter; + /** + * @deprecated use {@link #cicdPipelineRunDurationHistogram} + */ + @Deprecated private LongCounter runAbortedCounter; + /** + * @deprecated use {@link #cicdPipelineRunDurationHistogram} + */ + @Deprecated private LongCounter runSuccessCounter; + /** + * @deprecated use {@link #cicdPipelineRunDurationHistogram} + */ + @Deprecated private LongCounter runFailedCounter; + + private List causeHandlers; private List runHandlers; @VisibleForTesting @@ -108,6 +148,11 @@ public class MonitoringRunListener extends OtelContextAwareAbstractRunListener @VisibleForTesting Pattern runDurationHistogramDenyList; + private DoubleHistogram cicdPipelineRunDurationHistogram; + private LongUpDownCounter cicdPipelineRunActiveCounter; + private LongCounter cicdPipelineRunErrorsCounter; + private LongCounter cicdSystemErrorsCounter; // TODO implement + @PostConstruct public void postConstruct() { LOGGER.log(Level.FINE, () -> "Start monitoring Jenkins build executions..."); @@ -127,41 +172,80 @@ public void postConstruct() { this.runHandlers = runHandlers; // METRICS - activeRunGauge = new AtomicInteger(); - - runDurationHistogram = meter.histogramBuilder(JenkinsMetrics.CI_PIPELINE_RUN_DURATION) - .setUnit("s") - .setExplicitBucketBoundariesAdvice(DURATION_SECONDS_BUCKETS) - .build(); runDurationHistogramAllowList = MATCH_ANYTHING; // allow all runDurationHistogramDenyList = MATCH_NOTHING; // deny nothing - meter.gaugeBuilder(JenkinsMetrics.CI_PIPELINE_RUN_ACTIVE) + // keep the init of the new semantic conventions metrics in the `postConstruct()` to be consistent with + // the initialization of other metrics + // TODO only create new CICD semconv metrics if semconvStability.emitStableCicdSemconv() is true + // else shall we use use no-op metrics + SemconvStability semconvStability = new SemconvStability(); + semconvStability.afterConfiguration(configProperties); + Meter newSemConventionsMeter = semconvStability.emitStableCicdSemconv() + ? meter + : OpenTelemetry.noop().getMeter("jenkins.opentelemetry"); + cicdPipelineRunDurationHistogram = CicdMetrics.newCiCdPipelineRunDurationHistogram(newSemConventionsMeter); + cicdPipelineRunActiveCounter = CicdMetrics.newCiCdPipelineRunActiveCounter(newSemConventionsMeter); + cicdPipelineRunErrorsCounter = CicdMetrics.newCiCdPipelineRunErrorsCounter(newSemConventionsMeter); + // TODO when to qualify a build failure as a cicd system error? + cicdSystemErrorsCounter = CicdMetrics.newCiCdSystemErrorsCounter(newSemConventionsMeter); + + createOldSemanticConventionsMeasurements(semconvStability, meter); + } + + /** + * Locate the initialization of old semantic conventions measurements in a dedicated method to isolate the code, + * prevent misuse of the no-op meter, and ase removal of the code + */ + private void createOldSemanticConventionsMeasurements(SemconvStability semconvStability, Meter meter) { + Meter oldSemConventionsMeter = semconvStability.emitOldCicdSemconv() + ? meter + : OpenTelemetry.noop().getMeter("jenkins.opentelemetry"); + + activeRunGauge = new AtomicInteger(); + + DoubleHistogramBuilder runDurationHistogramBuilder = oldSemConventionsMeter + .histogramBuilder(JenkinsMetrics.CI_PIPELINE_RUN_DURATION) + .setUnit("s") + .setExplicitBucketBoundariesAdvice(DURATION_SECONDS_BUCKETS); + if (runDurationHistogramBuilder instanceof ExtendedDoubleHistogramBuilder extendedBuilder) { + extendedBuilder.setAttributesAdvice(List.of( + ExtendedJenkinsAttributes.CI_PIPELINE_ID, ExtendedJenkinsAttributes.CI_PIPELINE_RUN_RESULT)); + } + runDurationHistogram = runDurationHistogramBuilder.build(); + oldSemConventionsMeter + .gaugeBuilder(JenkinsMetrics.CI_PIPELINE_RUN_ACTIVE) .ofLongs() .setDescription("Gauge of active jobs") .setUnit("{jobs}") .buildWithCallback(valueObserver -> valueObserver.record(this.activeRunGauge.get())); - runLaunchedCounter = meter.counterBuilder(JenkinsMetrics.CI_PIPELINE_RUN_LAUNCHED) + runLaunchedCounter = oldSemConventionsMeter + .counterBuilder(JenkinsMetrics.CI_PIPELINE_RUN_LAUNCHED) .setDescription("Job launched") .setUnit("{jobs}") .build(); - runStartedCounter = meter.counterBuilder(JenkinsMetrics.CI_PIPELINE_RUN_STARTED) + runStartedCounter = oldSemConventionsMeter + .counterBuilder(JenkinsMetrics.CI_PIPELINE_RUN_STARTED) .setDescription("Job started") .setUnit("{jobs}") .build(); - runSuccessCounter = meter.counterBuilder(JenkinsMetrics.CI_PIPELINE_RUN_SUCCESS) + runSuccessCounter = oldSemConventionsMeter + .counterBuilder(JenkinsMetrics.CI_PIPELINE_RUN_SUCCESS) .setDescription("Job succeed") .setUnit("{jobs}") .build(); - runFailedCounter = meter.counterBuilder(JenkinsMetrics.CI_PIPELINE_RUN_FAILED) + runFailedCounter = oldSemConventionsMeter + .counterBuilder(JenkinsMetrics.CI_PIPELINE_RUN_FAILED) .setDescription("Job failed") .setUnit("{jobs}") .build(); - runAbortedCounter = meter.counterBuilder(JenkinsMetrics.CI_PIPELINE_RUN_ABORTED) + runAbortedCounter = oldSemConventionsMeter + .counterBuilder(JenkinsMetrics.CI_PIPELINE_RUN_ABORTED) .setDescription("Job aborted") .setUnit("{jobs}") .build(); - runCompletedCounter = meter.counterBuilder(JenkinsMetrics.CI_PIPELINE_RUN_COMPLETED) + runCompletedCounter = oldSemConventionsMeter + .counterBuilder(JenkinsMetrics.CI_PIPELINE_RUN_COMPLETED) .setDescription("Job completed") .setUnit("{jobs}") .build(); @@ -215,30 +299,42 @@ public void _onInitialize(@NonNull Run run) { LOGGER.log(Level.FINE, () -> run.getFullDisplayName() + " - onInitialize"); activeRunGauge.incrementAndGet(); + cicdPipelineRunActiveCounter.add( + 1, + Attributes.of( + CicdIncubatingAttributes.CICD_PIPELINE_NAME, + PIPELINE_NAME_OTHER, // FIXME CARDINALITY PROTECTION + CicdIncubatingAttributes.CICD_PIPELINE_RUN_STATE, + CicdIncubatingAttributes.CicdPipelineRunStateIncubatingValues.PENDING)); RunHandler runHandler = getRunHandlers().stream() - .filter(rh -> rh.canCreateSpanBuilder(run)) + .filter(rh -> rh.matches(run)) .findFirst() .orElseThrow((Supplier) () -> new IllegalStateException("No RunHandler found for run " + run.getClass() + " - " + run)); - SpanBuilder rootSpanBuilder = runHandler.createSpanBuilder(run, getTracer()); - + String pipelineShortName = runHandler.getPipelineShortName(run); + SpanBuilder rootSpanBuilder = getTracer() + .spanBuilder(ExtendedJenkinsAttributes.CI_PIPELINE_RUN_ROOT_SPAN_NAME_PREFIX + pipelineShortName); + runHandler.enrichPipelineRunSpan(run, rootSpanBuilder); rootSpanBuilder.setSpanKind(SpanKind.SERVER); - String runUrl = Objects.toString(Jenkins.get().getRootUrl(), "") + run.getUrl(); + String runUrl = Optional.ofNullable(Jenkins.get().getRootUrl()).orElse("") + run.getUrl(); // TODO move this to a pluggable span enrichment API with implementations for different observability backends rootSpanBuilder.setAttribute(ExtendedJenkinsAttributes.ELASTIC_TRANSACTION_TYPE, "job"); rootSpanBuilder + .setAttribute(CicdIncubatingAttributes.CICD_PIPELINE_NAME, pipelineShortName) + .setAttribute(CicdIncubatingAttributes.CICD_PIPELINE_RUN_URL_FULL, runUrl) + .setAttribute(CicdIncubatingAttributes.CICD_PIPELINE_RUN_ID, String.valueOf(run.getNumber())) .setAttribute( ExtendedJenkinsAttributes.CI_PIPELINE_ID, run.getParent().getFullName()) .setAttribute( ExtendedJenkinsAttributes.CI_PIPELINE_NAME, run.getParent().getFullDisplayName()) + .setAttribute(ExtendedJenkinsAttributes.CI_PIPELINE_TYPE, OtelUtils.getProjectType(run)) .setAttribute(ExtendedJenkinsAttributes.CI_PIPELINE_RUN_URL, runUrl) - .setAttribute(ExtendedJenkinsAttributes.CI_PIPELINE_RUN_NUMBER, (long) run.getNumber()) - .setAttribute(ExtendedJenkinsAttributes.CI_PIPELINE_TYPE, OtelUtils.getProjectType(run)); + .setAttribute(ExtendedJenkinsAttributes.CI_PIPELINE_RUN_NUMBER, (long) run.getNumber()); // CULPRITS Set culpritIds; @@ -316,8 +412,7 @@ public void _onInitialize(@NonNull Run run) { // unclear why this could happen. Maybe during the installation of the plugin if the plugin is // installed while a parent job triggers a downstream job w3cTraceContext = Collections.emptyMap(); - } else if (upstreamCause instanceof BuildUpstreamCause) { - BuildUpstreamCause buildUpstreamCause = (BuildUpstreamCause) cause; + } else if (upstreamCause instanceof BuildUpstreamCause buildUpstreamCause) { String upstreamNodeId = buildUpstreamCause.getNodeId(); w3cTraceContext = monitoringAction.getW3cTraceContext(upstreamNodeId); } else { @@ -367,6 +462,20 @@ public String get(@Nullable Map carrier, String key) { @Override public void _onStarted(@NonNull Run run, @NonNull TaskListener listener) { + cicdPipelineRunActiveCounter.add( + -1, + Attributes.of( + CicdIncubatingAttributes.CICD_PIPELINE_NAME, + PIPELINE_NAME_OTHER, // FIXME CARDINALITY PROTECTION + CicdIncubatingAttributes.CICD_PIPELINE_RUN_STATE, + CicdIncubatingAttributes.CicdPipelineRunStateIncubatingValues.PENDING)); + cicdPipelineRunActiveCounter.add( + 1, + Attributes.of( + CicdIncubatingAttributes.CICD_PIPELINE_NAME, + PIPELINE_NAME_OTHER, // FIXME CARDINALITY PROTECTION + CicdIncubatingAttributes.CICD_PIPELINE_RUN_STATE, + CicdIncubatingAttributes.CicdPipelineRunStateIncubatingValues.EXECUTING)); try (Scope parentScope = endPipelinePhaseSpan(run)) { Span runSpan = getTracer() .spanBuilder(ExtendedJenkinsAttributes.JENKINS_JOB_SPAN_PHASE_RUN_NAME) @@ -382,6 +491,21 @@ public void _onStarted(@NonNull Run run, @NonNull TaskListener listener) { @Override public void _onCompleted(@NonNull Run run, @NonNull TaskListener listener) { + cicdPipelineRunActiveCounter.add( + -1, + Attributes.of( + CicdIncubatingAttributes.CICD_PIPELINE_NAME, + PIPELINE_NAME_OTHER, // FIXME CARDINALITY PROTECTION + CicdIncubatingAttributes.CICD_PIPELINE_RUN_STATE, + CicdIncubatingAttributes.CicdPipelineRunStateIncubatingValues.EXECUTING)); + cicdPipelineRunActiveCounter.add( + 1, + Attributes.of( + CicdIncubatingAttributes.CICD_PIPELINE_NAME, + PIPELINE_NAME_OTHER, // FIXME CARDINALITY PROTECTION + CicdIncubatingAttributes.CICD_PIPELINE_RUN_STATE, + CicdIncubatingAttributes.CicdPipelineRunStateIncubatingValues.FINALIZING)); + try (Scope ignoredParentScope = endPipelinePhaseSpan(run)) { Span finalizeSpan = getTracer() .spanBuilder(ExtendedJenkinsAttributes.JENKINS_JOB_SPAN_PHASE_FINALIZE_NAME) @@ -437,8 +561,22 @@ public void _onFinalized(@NonNull Run run) { parentSpan.setAttribute(ExceptionAttributes.EXCEPTION_TYPE, "PIPELINE_" + runResult); parentSpan.setAttribute(ExceptionAttributes.EXCEPTION_MESSAGE, "PIPELINE_" + runResult); parentSpan.setStatus(StatusCode.ERROR, runResult.toString()); + cicdPipelineRunErrorsCounter.add( + 1, + Attributes.of( + CicdIncubatingAttributes.CICD_PIPELINE_NAME, + PIPELINE_NAME_OTHER, // FIXME IMPLEMENT CARDINALITY PROTECTION + ErrorAttributes.ERROR_TYPE, + runResult.toString())); } else if (Result.ABORTED.equals(runResult) || Result.NOT_BUILT.equals(runResult)) { parentSpan.setStatus(StatusCode.UNSET, runResult.toString()); + cicdPipelineRunErrorsCounter.add( + 1, + Attributes.of( + CicdIncubatingAttributes.CICD_PIPELINE_NAME, + PIPELINE_NAME_OTHER, // FIXME IMPLEMENT CARDINALITY PROTECTION + ErrorAttributes.ERROR_TYPE, + runResult.toString())); } } // NODE @@ -471,21 +609,46 @@ public void _onFinalized(@NonNull Run run) { this.runAbortedCounter.add(1); } - String jobFullName = run.getParent().getFullName(); - String pipelineId = - runDurationHistogramAllowList.matcher(jobFullName).matches() + // TODO perf optimization, reuse resolution done in `#_onInitialize(run)` + String pipelineShortName = getRunHandlers().stream() + .filter(rh -> rh.matches(run)) + .findFirst() + .orElseThrow((Supplier) () -> + new IllegalStateException("No RunHandler found for run " + run.getClass() + " - " + run)) + .getPipelineShortName(run); + String pipelineName = + runDurationHistogramAllowList.matcher(pipelineShortName).matches() && !runDurationHistogramDenyList - .matcher(jobFullName) + .matcher(pipelineShortName) .matches() - ? jobFullName - : "#other#"; + ? pipelineShortName + : PIPELINE_NAME_OTHER; runDurationHistogram.record( TimeUnit.SECONDS.convert(run.getDuration(), TimeUnit.MILLISECONDS), Attributes.of( ExtendedJenkinsAttributes.CI_PIPELINE_ID, - pipelineId, + pipelineName, ExtendedJenkinsAttributes.CI_PIPELINE_RUN_RESULT, result.toString())); + + // FIXME CicdIncubatingAttributes.CICD_PIPELINE_RUN_STATE & ErrorAttributes.ERROR_TYPE + cicdPipelineRunDurationHistogram.record( + TimeUnit.SECONDS.convert(run.getDuration(), TimeUnit.MILLISECONDS), + Attributes.of( + CicdIncubatingAttributes.CICD_PIPELINE_NAME, + pipelineName, + CicdIncubatingAttributes.CICD_PIPELINE_RUN_STATE, + CicdIncubatingAttributes.CicdPipelineRunStateIncubatingValues.FINALIZING, + CicdIncubatingAttributes.CICD_PIPELINE_RESULT, + CicdMetrics.fromJenkinsResultToOtelCicdPipelineResult(result))); + + cicdPipelineRunActiveCounter.add( + -1, + Attributes.of( + CicdIncubatingAttributes.CICD_PIPELINE_NAME, + PIPELINE_NAME_OTHER, // FIXME CARDINALITY PROTECTION + CicdIncubatingAttributes.CICD_PIPELINE_RUN_STATE, + CicdIncubatingAttributes.CicdPipelineRunStateIncubatingValues.FINALIZING)); } finally { activeRunGauge.decrementAndGet(); } diff --git a/src/main/java/io/jenkins/plugins/opentelemetry/job/runhandler/DefaultRunHandler.java b/src/main/java/io/jenkins/plugins/opentelemetry/job/runhandler/DefaultRunHandler.java index 4d50768ea..2b919af3d 100644 --- a/src/main/java/io/jenkins/plugins/opentelemetry/job/runhandler/DefaultRunHandler.java +++ b/src/main/java/io/jenkins/plugins/opentelemetry/job/runhandler/DefaultRunHandler.java @@ -9,9 +9,6 @@ import edu.umd.cs.findbugs.annotations.NonNull; import hudson.Extension; import hudson.model.Run; -import io.jenkins.plugins.opentelemetry.semconv.ExtendedJenkinsAttributes; -import io.opentelemetry.api.trace.SpanBuilder; -import io.opentelemetry.api.trace.Tracer; import java.util.Arrays; import java.util.Collections; import java.util.List; @@ -21,8 +18,8 @@ import jenkins.scm.api.mixin.ChangeRequestSCMHead; /** - * Use same root span name for all pull change request pipelines (pull request, merge request) - * Use different span names for different branches. + * Use same short pipeline name for all change request pipelines (pull request, merge request) + * Use different short pipeline name for different branches. */ @Extension public class DefaultRunHandler implements RunHandler { @@ -32,21 +29,21 @@ public class DefaultRunHandler implements RunHandler { "-" + ChangeRequestCheckoutStrategy.MERGE.name().toLowerCase(Locale.ENGLISH))); @Override - public boolean canCreateSpanBuilder(@NonNull Run run) { + public boolean matches(@NonNull Run run) { return true; } @NonNull @Override - public SpanBuilder createSpanBuilder(@NonNull Run run, @NonNull Tracer tracer) { + public String getPipelineShortName(@NonNull Run run) { SCMHead head = SCMHead.HeadByItem.findHead(run.getParent()); - String spanName; + String pipelineShortName; if (head instanceof ChangeRequestSCMHead) { - spanName = getChangeRequestRootSpanName(run.getParent().getFullName()); + pipelineShortName = getChangeRequestRootSpanName(run.getParent().getFullName()); } else { - spanName = run.getParent().getFullName(); + pipelineShortName = run.getParent().getFullName(); } - return tracer.spanBuilder(ExtendedJenkinsAttributes.CI_PIPELINE_RUN_ROOT_SPAN_NAME_PREFIX + spanName); + return pipelineShortName; } @VisibleForTesting diff --git a/src/main/java/io/jenkins/plugins/opentelemetry/job/runhandler/JobDslRunHandler.java b/src/main/java/io/jenkins/plugins/opentelemetry/job/runhandler/JobDslRunHandler.java index bf7a5c328..62224d555 100644 --- a/src/main/java/io/jenkins/plugins/opentelemetry/job/runhandler/JobDslRunHandler.java +++ b/src/main/java/io/jenkins/plugins/opentelemetry/job/runhandler/JobDslRunHandler.java @@ -13,9 +13,9 @@ import hudson.model.Run; import io.jenkins.plugins.opentelemetry.semconv.ExtendedJenkinsAttributes; import io.opentelemetry.api.trace.SpanBuilder; -import io.opentelemetry.api.trace.Tracer; import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; import java.util.Collection; +import java.util.Optional; import javaposse.jobdsl.plugin.actions.SeedJobAction; import javaposse.jobdsl.plugin.actions.SeedJobTransientActionFactory; import javax.inject.Inject; @@ -34,7 +34,7 @@ public JobDslRunHandler() throws ClassNotFoundException { } @Override - public boolean canCreateSpanBuilder(@NonNull Run run) { + public boolean matches(@NonNull Run run) { Job job = run.getParent(); // perf optimization: directly lookup up in the SeedJobTransientActionFactory over // `job.getAction(SeedJobAction.class)` @@ -44,7 +44,7 @@ public boolean canCreateSpanBuilder(@NonNull Run run) { @NonNull @Override - public SpanBuilder createSpanBuilder(@NonNull Run run, @NonNull Tracer tracer) { + public String getPipelineShortName(@NonNull Run run) { Job job = run.getParent(); // perf optimization: directly lookup up in the SeedJobTransientActionFactory over // `job.getAction(SeedJobAction.class)` @@ -56,30 +56,36 @@ public SpanBuilder createSpanBuilder(@NonNull Run run, @NonNull Tracer tra .orElseThrow(IllegalStateException::new); // TODO understand the difference between seedJobAction.getTemplateJob() and seedJobAction.getSeedJob() - Item seedJob = seedJobAction.getSeedJob(); - - String templateFullName; - String templateUrl; - String spanName; - if (seedJob == null) { - templateFullName = null; - templateUrl = null; - spanName = job.getFullName(); - } else { - templateFullName = seedJob.getFullName(); - templateUrl = seedJob.getUrl(); - spanName = collapseJobName ? "Job from seed '" + templateFullName + "'" : job.getFullName(); - } - - SpanBuilder spanBuilder = - tracer.spanBuilder(ExtendedJenkinsAttributes.CI_PIPELINE_RUN_ROOT_SPAN_NAME_PREFIX + spanName); - if (templateFullName != null) { - spanBuilder.setAttribute(ExtendedJenkinsAttributes.CI_PIPELINE_TEMPLATE_ID, templateFullName); - } - if (templateUrl != null) { - spanBuilder.setAttribute(ExtendedJenkinsAttributes.CI_PIPELINE_TEMPLATE_URL, templateUrl); - } - return spanBuilder; + Optional seedJob = Optional.ofNullable(seedJobAction.getSeedJob()); + + return collapseJobName + ? job.getFullName() + : seedJob.map(Item::getFullName) + .map(fn -> "Job from seed '" + fn + "'") + .orElse(job.getFullName()); + } + + @Override + public void enrichPipelineRunSpan(@NonNull Run run, @NonNull SpanBuilder spanBuilder) { + Job job = run.getParent(); + // perf optimization: directly lookup up in the SeedJobTransientActionFactory over + // `job.getAction(SeedJobAction.class)` + Collection actions = seedJobTransientActionFactory.createFor(job); + + SeedJobAction seedJobAction = (SeedJobAction) actions.stream() + .filter(action -> action instanceof SeedJobAction) + .findFirst() + .orElseThrow(IllegalStateException::new); + + // TODO understand the difference between seedJobAction.getTemplateJob() and seedJobAction.getSeedJob() + Optional seedJob = Optional.ofNullable(seedJobAction.getSeedJob()); + + seedJob.map(Item::getFullName) + .ifPresent(templateFullName -> + spanBuilder.setAttribute(ExtendedJenkinsAttributes.CI_PIPELINE_TEMPLATE_ID, templateFullName)); + seedJob.map(Item::getUrl) + .ifPresent(templateUrl -> + spanBuilder.setAttribute(ExtendedJenkinsAttributes.CI_PIPELINE_TEMPLATE_URL, templateUrl)); } @Override diff --git a/src/main/java/io/jenkins/plugins/opentelemetry/job/runhandler/MatrixRunHandler.java b/src/main/java/io/jenkins/plugins/opentelemetry/job/runhandler/MatrixRunHandler.java index 6bd651ce3..f86fec580 100644 --- a/src/main/java/io/jenkins/plugins/opentelemetry/job/runhandler/MatrixRunHandler.java +++ b/src/main/java/io/jenkins/plugins/opentelemetry/job/runhandler/MatrixRunHandler.java @@ -15,7 +15,6 @@ import hudson.model.Run; import io.jenkins.plugins.opentelemetry.semconv.ExtendedJenkinsAttributes; import io.opentelemetry.api.trace.SpanBuilder; -import io.opentelemetry.api.trace.Tracer; import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; import java.util.ArrayList; import java.util.List; @@ -32,22 +31,29 @@ public MatrixRunHandler() throws ClassNotFoundException { } @Override - public boolean canCreateSpanBuilder(@NonNull Run run) { + public boolean matches(@NonNull Run run) { return run instanceof MatrixRun || run instanceof MatrixBuild; } @NonNull @Override - public SpanBuilder createSpanBuilder(@NonNull Run run, @NonNull Tracer tracer) { + public String getPipelineShortName(@NonNull Run run) { if (run instanceof MatrixRun matrixRun) { MatrixConfiguration matrixConfiguration = matrixRun.getParent(); - MatrixProject matrixProject = matrixConfiguration.getParent(); - String spanName = - expandJobName ? run.getParent().getFullName() : matrixProject.getFullName() + "/execution"; - SpanBuilder spanBuilder = - tracer.spanBuilder(ExtendedJenkinsAttributes.CI_PIPELINE_RUN_ROOT_SPAN_NAME_PREFIX + spanName); - Combination combination = matrixConfiguration.getCombination(); + return expandJobName ? run.getParent().getFullName() : matrixProject.getFullName() + "/execution"; + } else if (run instanceof MatrixBuild matrixBuild) { + return matrixBuild.getParent().getFullName(); + } else { + throw new IllegalStateException("Unsupported run type " + run); + } + } + + @Override + public void enrichPipelineRunSpan(@NonNull Run run, @NonNull SpanBuilder spanBuilder) { + if (run instanceof MatrixRun matrixRun) { + + Combination combination = matrixRun.getParent().getCombination(); List axisNames = new ArrayList<>(); List axisValues = new ArrayList<>(); @@ -57,13 +63,6 @@ public SpanBuilder createSpanBuilder(@NonNull Run run, @NonNull Tracer tra }); spanBuilder.setAttribute(ExtendedJenkinsAttributes.CI_PIPELINE_RUN_AXIS_NAMES, axisNames); spanBuilder.setAttribute(ExtendedJenkinsAttributes.CI_PIPELINE_RUN_AXIS_VALUES, axisValues); - - return spanBuilder; - } else if (run instanceof MatrixBuild matrixBuild) { - return tracer.spanBuilder(ExtendedJenkinsAttributes.CI_PIPELINE_RUN_ROOT_SPAN_NAME_PREFIX - + matrixBuild.getParent().getFullName()); - } else { - throw new IllegalStateException("Unsupported run type " + run); } } diff --git a/src/main/java/io/jenkins/plugins/opentelemetry/job/runhandler/RunHandler.java b/src/main/java/io/jenkins/plugins/opentelemetry/job/runhandler/RunHandler.java index 880a1c246..8bd3702e2 100644 --- a/src/main/java/io/jenkins/plugins/opentelemetry/job/runhandler/RunHandler.java +++ b/src/main/java/io/jenkins/plugins/opentelemetry/job/runhandler/RunHandler.java @@ -8,17 +8,24 @@ import edu.umd.cs.findbugs.annotations.NonNull; import hudson.model.Run; import io.opentelemetry.api.trace.SpanBuilder; -import io.opentelemetry.api.trace.Tracer; import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; public interface RunHandler extends Comparable { default void configure(ConfigProperties config) {} - boolean canCreateSpanBuilder(@NonNull Run run); + boolean matches(@NonNull Run run); + /** + * Low cardinality pipeline name that fits with + * {@link io.opentelemetry.semconv.incubating.CicdIncubatingAttributes#CICD_PIPELINE_NAME}. + * High cardinality elements like the SCM pull request names of a Jenkins multibranch pipeline + * should be excluded from the pipeline short name. + */ @NonNull - SpanBuilder createSpanBuilder(@NonNull Run run, @NonNull Tracer tracer); + String getPipelineShortName(@NonNull Run run); + + default void enrichPipelineRunSpan(@NonNull Run run, @NonNull SpanBuilder spanBuilder) {} /** * @return the ordinal of this handler to execute run handlers in predictable order. The smallest ordinal is executed first. diff --git a/src/main/java/io/jenkins/plugins/opentelemetry/opentelemetry/SemconvStability.java b/src/main/java/io/jenkins/plugins/opentelemetry/opentelemetry/SemconvStability.java new file mode 100644 index 000000000..d96b3b962 --- /dev/null +++ b/src/main/java/io/jenkins/plugins/opentelemetry/opentelemetry/SemconvStability.java @@ -0,0 +1,80 @@ +package io.jenkins.plugins.opentelemetry.opentelemetry; + +import hudson.Extension; +import io.jenkins.plugins.opentelemetry.api.OpenTelemetryLifecycleListener; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.logging.Level; +import java.util.logging.Logger; +import jenkins.YesNoMaybe; + +/** + * Manages the configuration for semantic convention stability opt-in. + * Users can configure which version of the CI/CD semantic conventions to emit. + * TODO support hot config changes or document the need to restart Jenkins on config change. + */ +@Extension(dynamicLoadable = YesNoMaybe.MAYBE, optional = true) +public class SemconvStability implements OpenTelemetryLifecycleListener { + + private static final Logger logger = Logger.getLogger(SemconvStability.class.getName()); + + private final AtomicInteger configurationCounter = new AtomicInteger(0); + + private boolean emitOldCicdSemconv = true; + private boolean emitStableCicdSemconv = true; + + public boolean emitOldCicdSemconv() { + return emitOldCicdSemconv; + } + + public boolean emitStableCicdSemconv() { + return emitStableCicdSemconv; + } + + @Override + public void afterConfiguration(ConfigProperties configProperties) { + boolean oldCicd = true; + boolean stableCicd = true; + + // default to just emitting the old style metrics + String value = configProperties.getString("otel.semconv-stability.opt-in", "cicd/old"); + if (value != null) { + Set values = new HashSet<>(Arrays.asList(value.split(","))); + + // technically it's possible to set "cicd,cicd/dup" or "cicd,cicd/old" + if (values.contains("cicd/dup")) { + oldCicd = true; + stableCicd = true; + } else if (values.contains("cicd/old")) { + oldCicd = true; + stableCicd = false; + } else if (values.contains("cicd")) { + oldCicd = false; + stableCicd = true; + } + } + if (configurationCounter.get() > 0 && (emitOldCicdSemconv != oldCicd || emitStableCicdSemconv != stableCicd)) { + logger.log( + Level.INFO, + "SemconvStability: configuration changes from " + "emitOldCicdSemconv=" + + emitOldCicdSemconv + " to " + oldCicd + ", emitStableCicdSemconv=" + + emitStableCicdSemconv + " to " + stableCicd + + " may not support hot reload and may require restart"); + } + emitOldCicdSemconv = oldCicd; + emitStableCicdSemconv = stableCicd; + configurationCounter.incrementAndGet(); + logger.log( + Level.FINE, + () -> "SemconvStability: emitOldCicdSemconv=" + emitOldCicdSemconv + ", emitStableCicdSemconv=" + + emitStableCicdSemconv); + } + + @Override + public int ordinal() { + return Integer.MIN_VALUE; + } +} diff --git a/src/main/java/io/jenkins/plugins/opentelemetry/semconv/CicdMetrics.java b/src/main/java/io/jenkins/plugins/opentelemetry/semconv/CicdMetrics.java new file mode 100644 index 000000000..bd5d30818 --- /dev/null +++ b/src/main/java/io/jenkins/plugins/opentelemetry/semconv/CicdMetrics.java @@ -0,0 +1,118 @@ +package io.jenkins.plugins.opentelemetry.semconv; + +import hudson.model.Result; +import io.opentelemetry.api.incubator.metrics.ExtendedDoubleHistogramBuilder; +import io.opentelemetry.api.incubator.metrics.ExtendedDoubleUpDownCounterBuilder; +import io.opentelemetry.api.incubator.metrics.ExtendedLongCounterBuilder; +import io.opentelemetry.api.metrics.DoubleHistogram; +import io.opentelemetry.api.metrics.DoubleHistogramBuilder; +import io.opentelemetry.api.metrics.LongCounter; +import io.opentelemetry.api.metrics.LongCounterBuilder; +import io.opentelemetry.api.metrics.LongUpDownCounter; +import io.opentelemetry.api.metrics.LongUpDownCounterBuilder; +import io.opentelemetry.api.metrics.Meter; +import io.opentelemetry.api.metrics.ObservableLongMeasurement; +import io.opentelemetry.semconv.ErrorAttributes; +import io.opentelemetry.semconv.incubating.CicdIncubatingAttributes; +import java.util.List; + +public class CicdMetrics { + // FIXME WHAT ARE THE SPECS FOR THESE BUCKETS? + // 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 = + List.of(1D, 2D, 4D, 8D, 16D, 32D, 64D, 128D, 256D, 512D, 1024D, 2048D, 4096D, 8192D); + + public static DoubleHistogram newCiCdPipelineRunDurationHistogram(Meter meter) { + DoubleHistogramBuilder cicdPipelineRunDurationHistogramBuilder = meter.histogramBuilder( + "cicd.pipeline.run.duration") + .setUnit("s") + .setDescription("Duration of a pipeline run grouped by pipeline, state and result.") + .setExplicitBucketBoundariesAdvice(DURATION_SECONDS_BUCKETS); + if (cicdPipelineRunDurationHistogramBuilder + instanceof ExtendedDoubleHistogramBuilder extendedDoubleHistogramBuilder) { + extendedDoubleHistogramBuilder.setAttributesAdvice(List.of( + CicdIncubatingAttributes.CICD_PIPELINE_NAME, + CicdIncubatingAttributes.CICD_PIPELINE_RUN_STATE, + CicdIncubatingAttributes.CICD_PIPELINE_RESULT, + ErrorAttributes.ERROR_TYPE)); + } + + return cicdPipelineRunDurationHistogramBuilder.build(); + } + + public static LongUpDownCounter newCiCdPipelineRunActiveCounter(Meter meter) { + LongUpDownCounterBuilder cicdPipelineRunActiveCounterBuilder = meter.upDownCounterBuilder( + "cicd.pipeline.run.active") + .setUnit("{run}") + .setDescription("Number of active pipeline runs grouped by pipeline and state."); + if (cicdPipelineRunActiveCounterBuilder + instanceof ExtendedDoubleUpDownCounterBuilder pipelineRunActiveCounterBuilder) { + pipelineRunActiveCounterBuilder.setAttributesAdvice(List.of( + CicdIncubatingAttributes.CICD_PIPELINE_NAME, CicdIncubatingAttributes.CICD_PIPELINE_RUN_STATE)); + } + + return cicdPipelineRunActiveCounterBuilder.build(); + } + + /** + * FIXME shouldn't it be a gauge rather than an upDownCounter? + */ + public static ObservableLongMeasurement newCiCdWorkerCounter(Meter meter) { + LongUpDownCounterBuilder cicdWorkerCountBuilder = meter.upDownCounterBuilder("cicd.worker.count") + .setUnit("{worker}") + .setDescription("The number of workers on the CICD system by state."); + if (cicdWorkerCountBuilder instanceof ExtendedDoubleUpDownCounterBuilder cicdWorkerCountExtendedBuilder) { + cicdWorkerCountExtendedBuilder.setAttributesAdvice(List.of(CicdIncubatingAttributes.CICD_WORKER_STATE)); + } + + return cicdWorkerCountBuilder.buildObserver(); + } + + public static LongCounter newCiCdPipelineRunErrorsCounter(Meter meter) { + LongCounterBuilder cicdPipelineRunErrorsBuilder = meter.counterBuilder("cicd.pipeline.run.errors") + .setUnit("{error}") + .setDescription("Number of errors in a pipeline run grouped by pipeline and error type."); + if (cicdPipelineRunErrorsBuilder instanceof ExtendedLongCounterBuilder cicdPipelineRunErrorsExtendedBuilder) { + cicdPipelineRunErrorsExtendedBuilder.setAttributesAdvice( + List.of(CicdIncubatingAttributes.CICD_PIPELINE_NAME, ErrorAttributes.ERROR_TYPE)); + } + + return cicdPipelineRunErrorsBuilder.build(); + } + + public static LongCounter newCiCdSystemErrorsCounter(Meter meter) { + LongCounterBuilder cicdSystemErrorsBuilder = meter.counterBuilder("cicd.system.errors") + .setUnit("{error}") + .setDescription("Number of errors in the CICD system grouped by component and error type."); + if (cicdSystemErrorsBuilder instanceof ExtendedLongCounterBuilder cicdSystemErrorsExtendedBuilder) { + cicdSystemErrorsExtendedBuilder.setAttributesAdvice( + List.of(CicdIncubatingAttributes.CICD_SYSTEM_COMPONENT, ErrorAttributes.ERROR_TYPE)); + } + return cicdSystemErrorsBuilder.build(); + } + + /** + * Convert a Jenkins {@link Result} to a an OpenTelemetry + * {@link CicdIncubatingAttributes#CICD_PIPELINE_RESULT} according to the + * OpenTelemetry + * CICD Semantic Conventions + */ + public static String fromJenkinsResultToOtelCicdPipelineResult(Result result) { + if (result == null) { + return "#null#"; + } + if (result.equals(Result.ABORTED)) { + return CicdIncubatingAttributes.CicdPipelineResultIncubatingValues.CANCELLATION; + } else if (result.equals(Result.FAILURE)) { + return CicdIncubatingAttributes.CicdPipelineResultIncubatingValues.FAILURE; + } else if (result.equals(Result.NOT_BUILT)) { + return "not_built"; + } else if (result.equals(Result.SUCCESS)) { + return CicdIncubatingAttributes.CicdPipelineResultIncubatingValues.SUCCESS; + } else if (result.equals(Result.UNSTABLE)) { + return "unstable"; + } + return result.toString().toLowerCase(); + } +} diff --git a/src/main/java/io/jenkins/plugins/opentelemetry/semconv/ExtendedJenkinsAttributes.java b/src/main/java/io/jenkins/plugins/opentelemetry/semconv/ExtendedJenkinsAttributes.java index 33019d808..afa17a032 100644 --- a/src/main/java/io/jenkins/plugins/opentelemetry/semconv/ExtendedJenkinsAttributes.java +++ b/src/main/java/io/jenkins/plugins/opentelemetry/semconv/ExtendedJenkinsAttributes.java @@ -24,6 +24,9 @@ public final class ExtendedJenkinsAttributes extends JenkinsAttributes { public static final AttributeKey CI_PIPELINE_MULTIBRANCH_TYPE = AttributeKey.stringKey("ci.pipeline.multibranch.type"); /** + * Full Jenkins job name, different from the low cardinality + * {@link io.opentelemetry.semconv.incubating.CicdIncubatingAttributes#CICD_PIPELINE_NAME} that relies on + * {@link io.jenkins.plugins.opentelemetry.job.runhandler.RunHandler#getPipelineShortName(Run)} * @see Job#getFullName() */ public static final AttributeKey CI_PIPELINE_ID = AttributeKey.stringKey("ci.pipeline.id"); @@ -52,7 +55,12 @@ public final class ExtendedJenkinsAttributes extends JenkinsAttributes { AttributeKey.longKey("ci.pipeline.run.durationMillis"); public static final AttributeKey CI_PIPELINE_RUN_DESCRIPTION = AttributeKey.stringKey("ci.pipeline.run.description"); + /** + * @deprecated use {@link io.opentelemetry.semconv.incubating.CicdIncubatingAttributes#CICD_PIPELINE_RUN_ID} + */ + @Deprecated public static final AttributeKey CI_PIPELINE_RUN_NUMBER = AttributeKey.longKey("ci.pipeline.run.number"); + 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 = @@ -61,10 +69,16 @@ public final class ExtendedJenkinsAttributes extends JenkinsAttributes { AttributeKey.stringArrayKey("ci.pipeline.parameter.value"); /** * @see Run#getResult() + * @deprecated use {@link io.opentelemetry.semconv.incubating.CicdIncubatingAttributes#CICD_PIPELINE_RESULT} */ + @Deprecated public static final AttributeKey CI_PIPELINE_RUN_RESULT = AttributeKey.stringKey("ci.pipeline.run.result"); - + /** + * @deprecated use {@link io.opentelemetry.semconv.incubating.CicdIncubatingAttributes#CICD_PIPELINE_RUN_URL_FULL} + */ + @Deprecated 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"); public static final AttributeKey> CI_PIPELINE_RUN_AXIS_NAMES = @@ -72,19 +86,33 @@ public final class ExtendedJenkinsAttributes extends JenkinsAttributes { public static final AttributeKey> CI_PIPELINE_RUN_AXIS_VALUES = AttributeKey.stringArrayKey("ci.pipeline.axis.values"); + /** + * FIXME introduce {@link io.opentelemetry.semconv.incubating.VcsIncubatingAttributes#VCS_REPOSITORY_URL_FULL} and {@link io.opentelemetry.semconv.incubating.VcsIncubatingAttributes#VCS_OWNER_NAME} + * @deprecated use {@link io.opentelemetry.semconv.incubating.VcsIncubatingAttributes#VCS_REPOSITORY_NAME} + */ + @Deprecated public static final AttributeKey GIT_REPOSITORY = AttributeKey.stringKey("git.repository"); + /** + * @deprecated use {@link io.opentelemetry.semconv.incubating.VcsIncubatingAttributes#VCS_REF_BASE_NAME} and {@link io.opentelemetry.semconv.incubating.VcsIncubatingAttributes#VCS_REF_BASE_TYPE} + */ + @Deprecated public static final AttributeKey GIT_BRANCH = AttributeKey.stringKey("git.branch"); + public static final AttributeKey GIT_USERNAME = AttributeKey.stringKey("git.username"); public static final AttributeKey GIT_CLONE_DEPTH = AttributeKey.longKey("git.clone.depth"); public static final AttributeKey GIT_CLONE_SHALLOW = AttributeKey.booleanKey("git.clone.shallow"); /** * @see StepDescriptor#getDisplayName() + * @deprecated use {@link io.opentelemetry.semconv.incubating.CicdIncubatingAttributes#CICD_PIPELINE_TASK_NAME} */ + @Deprecated public static final AttributeKey JENKINS_STEP_NAME = AttributeKey.stringKey("jenkins.pipeline.step.name"); /** * @see StepDescriptor#getFunctionName() + * @deprecated use {@link io.opentelemetry.semconv.incubating.CicdIncubatingAttributes#CICD_PIPELINE_TASK_TYPE} */ + @Deprecated public static final AttributeKey JENKINS_STEP_TYPE = AttributeKey.stringKey("jenkins.pipeline.step.type"); /** * @see org.jenkinsci.plugins.workflow.graph.FlowNode#getId()