diff --git a/docs/monitoring-metrics.md b/docs/monitoring-metrics.md index 9a7117f31..9b497350f 100644 --- a/docs/monitoring-metrics.md +++ b/docs/monitoring-metrics.md @@ -117,6 +117,44 @@ Inventory of health metrics collected by the Jenkins OpenTelemetry integration: Job failed + + jenkins.executor + ${executors} + + label,
+ status + + + Jenkins build agent labelcode> like linux
+ busy, idle, connecting + + + Jenkins executors broken down by label and status. Executors annotated with + multiple label are reported multiple times + + + + jenkins.executor.total + ${executors} + + status + + + busy, idle + + Jenkins executors broken down by status + + + jenkins.node + ${nodes} + + status + + + online, offline + + Jenkins build nodes + jenkins.executor.available ${executors} @@ -166,6 +204,15 @@ Inventory of health metrics collected by the Jenkins OpenTelemetry integration: + + jenkins.queue + ${tasks} + status + + blocked, buildable, stuck, waiting, unknown + + Number of tasks in the queue. See statuscode> description [here](https://javadoc.jenkins.io/hudson/model/Queue.html) + jenkins.queue.waiting ${items} @@ -208,6 +255,35 @@ Inventory of health metrics collected by the Jenkins OpenTelemetry integration: Disk Usage size + + http.server.request.duration + s + + http.request.method,
+ url.scheme,
+ error.type,
+ http.response.status_code,
+ http.route,
+ server.address,
+ server.port + + + HTTP server duration metric as defined by the OpenTelemetry specification ([here](https://opentelemetry.io/docs/specs/semconv/http/http-metrics/#metric-httpserverrequestduration)) + + + jenkins.plugins + ${plugins} + status + active, inactive, failed + Jenkins plugins broken down by activation status + + + jenkins.plugins.updates + ${plugins} + status + hasUpdate, isUpToDate + Jenkins plugins broken down by updatability status + ## Jenkins agents metrics 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 89734b063..a8aa3cc48 100644 --- a/src/main/java/io/jenkins/plugins/opentelemetry/init/JenkinsExecutorMonitoringInitializer.java +++ b/src/main/java/io/jenkins/plugins/opentelemetry/init/JenkinsExecutorMonitoringInitializer.java @@ -5,11 +5,15 @@ package io.jenkins.plugins.opentelemetry.init; +import static io.jenkins.plugins.opentelemetry.semconv.JenkinsOtelSemanticAttributes.STATUS; + import hudson.Extension; +import hudson.model.Computer; import hudson.model.LoadStatistics; +import hudson.model.Node; import io.jenkins.plugins.opentelemetry.JenkinsControllerOpenTelemetry; import io.jenkins.plugins.opentelemetry.api.OpenTelemetryLifecycleListener; -import io.opentelemetry.api.common.AttributeKey; +import io.jenkins.plugins.opentelemetry.semconv.JenkinsOtelSemanticAttributes; import io.opentelemetry.api.common.Attributes; import io.opentelemetry.api.metrics.Meter; import io.opentelemetry.api.metrics.ObservableLongMeasurement; @@ -19,6 +23,8 @@ import javax.annotation.PostConstruct; import javax.inject.Inject; import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicInteger; import java.util.logging.Level; import java.util.logging.Logger; @@ -42,30 +48,87 @@ public void postConstruct() { logger.log(Level.FINE, () -> "Start monitoring Jenkins controller executor pool..."); Meter meter = Objects.requireNonNull(jenkinsControllerOpenTelemetry).getDefaultMeter(); + + final ObservableLongMeasurement queueLength = meter.gaugeBuilder(JENKINS_EXECUTOR_QUEUE).setUnit("${items}").setDescription("Executors queue items").ofLongs().buildObserver(); + final ObservableLongMeasurement totalExecutors = meter.gaugeBuilder(JENKINS_EXECUTOR_TOTAL).setUnit("${executors}").setDescription("Total executors").ofLongs().buildObserver(); + final ObservableLongMeasurement nodes = meter.gaugeBuilder(JENKINS_NODE).setUnit("${nodes}").setDescription("Nodes").ofLongs().buildObserver(); + final ObservableLongMeasurement executors = meter.gaugeBuilder(JENKINS_EXECUTOR).setUnit("${executors}").setDescription("Per label executors").ofLongs().buildObserver(); + + // TODO the metrics below should be deprecated in favor of + // * `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).setUnit("${executors}").setDescription("Available executors").ofLongs().buildObserver(); final ObservableLongMeasurement busyExecutors = meter.gaugeBuilder(JENKINS_EXECUTOR_BUSY).setUnit("${executors}").setDescription("Busy executors").ofLongs().buildObserver(); final ObservableLongMeasurement idleExecutors = meter.gaugeBuilder(JENKINS_EXECUTOR_IDLE).setUnit("${executors}").setDescription("Idle executors").ofLongs().buildObserver(); 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("${items}").setDescription("Executors queue items").ofLongs().buildObserver(); + logger.log(Level.FINER, () -> "Metrics: " + availableExecutors + ", " + busyExecutors + ", " + idleExecutors + ", " + onlineExecutors + ", " + connectingExecutors + ", " + definedExecutors + ", " + queueLength); meter.batchCallback(() -> { logger.log(Level.FINE, () -> "Recording Jenkins controller executor pool metrics..."); - logger.log(Level.FINER, () -> "Metrics: " + availableExecutors + ", " + busyExecutors + ", " + idleExecutors + ", " + onlineExecutors + ", " + connectingExecutors + ", " + definedExecutors + ", " + queueLength); Jenkins jenkins = Jenkins.get(); + + // TOTAL EXECUTORS + 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 -> { + if (executor.isIdle()) { + totalExecutorsIdle.incrementAndGet(); + } else { + totalExecutorsBusy.incrementAndGet(); + } + })); + } + jenkins.getNodes().stream().map(Node::toComputer).filter(Objects::nonNull).forEach(node -> { + if (node.isOnline()) { + nodeOnline.incrementAndGet(); + node.getExecutors() + .forEach(executor -> { + if (executor.isIdle()) { + totalExecutorsIdle.incrementAndGet(); + } else { + totalExecutorsBusy.incrementAndGet(); + } + }); + } else { + nodeOffline.incrementAndGet(); + } + }); + + 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 jenkins.getLabels().forEach(label -> { LoadStatistics.LoadStatisticsSnapshot loadStatisticsSnapshot = label.loadStatistics.computeSnapshot(); - Attributes attributes = Attributes.of(AttributeKey.stringKey("label"), label.getDisplayName()); + Attributes attributes = Attributes.of(JenkinsOtelSemanticAttributes.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 availableExecutors.record(loadStatisticsSnapshot.getAvailableExecutors(), attributes); busyExecutors.record(loadStatisticsSnapshot.getBusyExecutors(), attributes); idleExecutors.record(loadStatisticsSnapshot.getIdleExecutors(), attributes); onlineExecutors.record(loadStatisticsSnapshot.getOnlineExecutors(), attributes); definedExecutors.record(loadStatisticsSnapshot.getDefinedExecutors(), attributes); connectingExecutors.record(loadStatisticsSnapshot.getConnectingExecutors(), attributes); - queueLength.record(loadStatisticsSnapshot.getQueueLength(), attributes); }); - }, availableExecutors, busyExecutors, idleExecutors, onlineExecutors, connectingExecutors, definedExecutors, queueLength); + }, availableExecutors, busyExecutors, idleExecutors, onlineExecutors, connectingExecutors, definedExecutors, totalExecutors, executors, nodes, queueLength); } } diff --git a/src/main/java/io/jenkins/plugins/opentelemetry/init/PluginMonitoringInitializer.java b/src/main/java/io/jenkins/plugins/opentelemetry/init/PluginMonitoringInitializer.java new file mode 100644 index 000000000..4394492c6 --- /dev/null +++ b/src/main/java/io/jenkins/plugins/opentelemetry/init/PluginMonitoringInitializer.java @@ -0,0 +1,93 @@ +/* + * Copyright The Original Author or Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.jenkins.plugins.opentelemetry.init; + +import hudson.Extension; +import hudson.PluginManager; +import io.jenkins.plugins.opentelemetry.JenkinsControllerOpenTelemetry; +import io.jenkins.plugins.opentelemetry.api.OpenTelemetryLifecycleListener; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.metrics.Meter; +import io.opentelemetry.api.metrics.ObservableLongMeasurement; +import jenkins.YesNoMaybe; +import jenkins.model.Jenkins; + +import javax.annotation.PostConstruct; +import javax.inject.Inject; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.logging.Level; +import java.util.logging.Logger; + +import static io.jenkins.plugins.opentelemetry.semconv.JenkinsOtelSemanticAttributes.STATUS; +import static io.jenkins.plugins.opentelemetry.semconv.JenkinsSemanticMetrics.JENKINS_PLUGINS; +import static io.jenkins.plugins.opentelemetry.semconv.JenkinsSemanticMetrics.JENKINS_PLUGINS_UPDATES; + +/** + *

+ * Monitor the Jenkins plugins + *

+ *

+ * TODO report on `hasUpdate` plugin count. + *

+ */ +@Extension(dynamicLoadable = YesNoMaybe.MAYBE, optional = true) +public class PluginMonitoringInitializer implements OpenTelemetryLifecycleListener { + + private static final Logger logger = Logger.getLogger(PluginMonitoringInitializer.class.getName()); + + @Inject + JenkinsControllerOpenTelemetry jenkinsControllerOpenTelemetry; + + @PostConstruct + public void postConstruct() { + + logger.log(Level.FINE, () -> "Start monitoring Jenkins plugins..."); + + Meter meter = Objects.requireNonNull(jenkinsControllerOpenTelemetry).getDefaultMeter(); + + final ObservableLongMeasurement plugins = meter + .gaugeBuilder(JENKINS_PLUGINS) + .setUnit("${plugins}") + .setDescription("Jenkins plugins") + .ofLongs() + .buildObserver(); + final ObservableLongMeasurement pluginUpdates = meter + .gaugeBuilder(JENKINS_PLUGINS_UPDATES) + .setUnit("${plugins}") + .setDescription("Jenkins plugin updates") + .ofLongs() + .buildObserver(); + meter.batchCallback(() -> { + logger.log(Level.FINE, () -> "Recording Jenkins controller executor pool metrics..."); + + AtomicInteger active = new AtomicInteger(); + AtomicInteger inactive = new AtomicInteger(); + AtomicInteger hasUpdate = new AtomicInteger(); + AtomicInteger isUpToDate = new AtomicInteger(); + + PluginManager pluginManager = Jenkins.get().getPluginManager(); + pluginManager.getPlugins().forEach(plugin -> { + if (plugin.isActive()) { + active.incrementAndGet(); + } else { + inactive.incrementAndGet(); + } + if (plugin.hasUpdate()) { + hasUpdate.incrementAndGet(); + } else { + isUpToDate.incrementAndGet(); + } + }); + int failed = pluginManager.getFailedPlugins().size(); + plugins.record(active.get(), Attributes.of(STATUS, "active")); + plugins.record(inactive.get(), Attributes.of(STATUS, "inactive")); + plugins.record(failed, Attributes.of(STATUS, "failed")); + pluginUpdates.record(hasUpdate.get(), Attributes.of(STATUS, "hasUpdate")); + pluginUpdates.record(isUpToDate.get(), Attributes.of(STATUS, "isUpToDate")); + }, plugins, pluginUpdates); + } +} diff --git a/src/main/java/io/jenkins/plugins/opentelemetry/queue/MonitoringQueueListener.java b/src/main/java/io/jenkins/plugins/opentelemetry/queue/MonitoringQueueListener.java index be9415dee..8f875498d 100644 --- a/src/main/java/io/jenkins/plugins/opentelemetry/queue/MonitoringQueueListener.java +++ b/src/main/java/io/jenkins/plugins/opentelemetry/queue/MonitoringQueueListener.java @@ -5,6 +5,9 @@ package io.jenkins.plugins.opentelemetry.queue; +import static io.jenkins.plugins.opentelemetry.semconv.JenkinsSemanticMetrics.*; +import static io.jenkins.plugins.opentelemetry.semconv.JenkinsOtelSemanticAttributes.STATUS; + import hudson.Extension; import hudson.model.Queue; import hudson.model.queue.QueueListener; @@ -12,8 +15,10 @@ import io.jenkins.plugins.opentelemetry.api.OpenTelemetryLifecycleListener; 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.LongCounter; import io.opentelemetry.api.metrics.Meter; +import io.opentelemetry.api.metrics.ObservableLongMeasurement; import io.opentelemetry.api.trace.Span; import io.opentelemetry.api.trace.SpanContext; import io.opentelemetry.context.Context; @@ -23,12 +28,15 @@ import javax.annotation.PostConstruct; import javax.inject.Inject; +import java.util.Arrays; 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 static io.jenkins.plugins.opentelemetry.semconv.JenkinsSemanticMetrics.JENKINS_QUEUE; + /** * Monitor the Jenkins Build queue */ @@ -37,7 +45,6 @@ public class MonitoringQueueListener extends QueueListener implements OpenTeleme private final static Logger LOGGER = Logger.getLogger(MonitoringQueueListener.class.getName()); - private final AtomicInteger blockedItemGauge = new AtomicInteger(); private LongCounter leftItemCounter; private LongCounter timeInQueueInMillisCounter; @Inject @@ -48,53 +55,89 @@ public class MonitoringQueueListener extends QueueListener implements OpenTeleme public void afterConfiguration(ConfigProperties configProperties) { traceContextPropagationEnabled.set(configProperties.getBoolean(JenkinsOtelSemanticAttributes.OTEL_INSTRUMENTATION_JENKINS_REMOTE_SPAN_ENABLED, false)); } + @PostConstruct public void postConstruct() { LOGGER.log(Level.FINE, () -> "Start monitoring Jenkins queue..."); Meter meter = jenkinsControllerOpenTelemetry.getDefaultMeter(); - meter.gaugeBuilder(JenkinsSemanticMetrics.JENKINS_QUEUE_WAITING) + final ObservableLongMeasurement queueItems = meter.gaugeBuilder(JENKINS_QUEUE) + .ofLongs() + .setDescription("Number of tasks in the queue") + .setUnit("${tasks}") + .buildObserver(); + // should be deprecated in favor of "jenkins.queue" metric with status attribute + final ObservableLongMeasurement queueWaitingItems = meter.gaugeBuilder(JENKINS_QUEUE_WAITING) .ofLongs() .setDescription("Number of tasks in the queue with the status 'waiting', 'buildable' or 'pending'") .setUnit("{tasks}") - .buildWithCallback(valueObserver -> valueObserver.record((long) - Optional.ofNullable(Jenkins.getInstanceOrNull()).map(j -> j.getQueue()). - map(q -> q.getUnblockedItems().size()).orElse(0))); - - meter.gaugeBuilder(JenkinsSemanticMetrics.JENKINS_QUEUE_BLOCKED) + .buildObserver(); + // should be deprecated in favor of "jenkins.queue" metric with status attribute + final ObservableLongMeasurement queueBlockedItems = meter.gaugeBuilder(JENKINS_QUEUE_BLOCKED) .ofLongs() .setDescription("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") .setUnit("{tasks}") - .buildWithCallback(valueObserver -> valueObserver.record(this.blockedItemGauge.longValue())); - - meter.gaugeBuilder(JenkinsSemanticMetrics.JENKINS_QUEUE_BUILDABLE) + .buildObserver(); + // should be deprecated in favor of "jenkins.queue" metric with status attribute + final ObservableLongMeasurement queueBuildableItems = meter.gaugeBuilder(JENKINS_QUEUE_BUILDABLE) .ofLongs() .setDescription("Number of tasks in the queue with the status 'buildable' or 'pending'") .setUnit("{tasks}") - .buildWithCallback(valueObserver -> valueObserver.record((long) - Optional.ofNullable(Jenkins.getInstanceOrNull()).map(j -> j.getQueue()). - map(q -> q.countBuildableItems()).orElse(0))); - - leftItemCounter = meter.counterBuilder(JenkinsSemanticMetrics.JENKINS_QUEUE_LEFT) + .buildObserver(); + + meter.batchCallback(() -> { + LOGGER.log(Level.FINE, () -> "Recording Jenkins queue metrics..."); + + Optional queue = Optional.ofNullable(Jenkins.getInstanceOrNull()).map(Jenkins::getQueue); + queue.map(Queue::getItems) + .ifPresent(items -> { + AtomicInteger blocked = new AtomicInteger(); + AtomicInteger buildable = new AtomicInteger(); + AtomicInteger left = new AtomicInteger(); + AtomicInteger stuck = new AtomicInteger(); + AtomicInteger unknown = new AtomicInteger(); + AtomicInteger waiting = new AtomicInteger(); + Arrays.stream(items).forEach(item -> { + if (item instanceof Queue.BlockedItem) { + blocked.incrementAndGet(); + } else if (item instanceof Queue.BuildableItem) { + if (item.isStuck()) { + // buildable but here for too long + stuck.incrementAndGet(); + } else { + buildable.incrementAndGet(); + } + } else if (item instanceof Queue.WaitingItem) { + waiting.incrementAndGet(); + } else if (item instanceof Queue.LeftItem) { + left.incrementAndGet(); + } else { + LOGGER.log(Level.INFO, () -> "Unknown item: " + item + " - class=" + item.getClass()); + unknown.incrementAndGet(); + } + }); + queueItems.record(blocked.get(), Attributes.of(STATUS, "blocked")); + queueBlockedItems.record(blocked.get()); + queueItems.record(buildable.get(), Attributes.of(STATUS, "buildable")); + queueBuildableItems.record(buildable.get()); + queueItems.record(stuck.get(), Attributes.of(STATUS, "stuck")); + if (unknown.get() > 0) { + queueItems.record(unknown.get(), Attributes.of(STATUS, "unknown")); + } + queueItems.record(waiting.get(), Attributes.of(STATUS, "waiting")); + queueWaitingItems.record(waiting.get()); + }); + }, queueItems, queueWaitingItems, queueBlockedItems, queueBuildableItems); + + leftItemCounter = meter.counterBuilder(JENKINS_QUEUE_LEFT) .setDescription("Total count of tasks that have been processed") .setUnit("{tasks}") .build(); - timeInQueueInMillisCounter = meter.counterBuilder(JenkinsSemanticMetrics.JENKINS_QUEUE_TIME_SPENT_MILLIS) + timeInQueueInMillisCounter = meter.counterBuilder(JENKINS_QUEUE_TIME_SPENT_MILLIS) .setDescription("Total time spent in queue by the tasks that have been processed") .setUnit("ms") .build(); - - } - - @Override - public void onEnterBlocked(Queue.BlockedItem bi) { - this.blockedItemGauge.incrementAndGet(); - } - - @Override - public void onLeaveBlocked(Queue.BlockedItem bi) { - this.blockedItemGauge.decrementAndGet(); } @Override @@ -108,13 +151,11 @@ public void onLeft(Queue.LeftItem li) { public void onEnterWaiting(Queue.WaitingItem wi) { if (traceContextPropagationEnabled.get()) { Span span = Span.fromContextOrNull(Context.current()); - if (span != null && wi.getActions(RemoteSpanAction.class) != null) { + if (span != null) { SpanContext spanContext = span.getSpanContext(); wi.addAction(new RemoteSpanAction(spanContext.getTraceId(), spanContext.getSpanId(), spanContext.getTraceFlags().asByte(), spanContext.getTraceState().asMap())); LOGGER.log(Level.FINE, () -> "attach RemoteSpanAction to " + wi); } } } - - } 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 71f2ca239..71ac95dc2 100644 --- a/src/main/java/io/jenkins/plugins/opentelemetry/semconv/JenkinsOtelSemanticAttributes.java +++ b/src/main/java/io/jenkins/plugins/opentelemetry/semconv/JenkinsOtelSemanticAttributes.java @@ -173,6 +173,10 @@ public static final class EventCategoryValues { public static final String AUTHENTICATION = "authentication"; } + public static final AttributeKey STATUS = AttributeKey.stringKey("status"); + public static final AttributeKey LABEL = AttributeKey.stringKey("label"); + + /** * Values in {@link EventOutcomeValues} */ 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 fb0bfc437..e1f24e78d 100644 --- a/src/main/java/io/jenkins/plugins/opentelemetry/semconv/JenkinsSemanticMetrics.java +++ b/src/main/java/io/jenkins/plugins/opentelemetry/semconv/JenkinsSemanticMetrics.java @@ -21,6 +21,10 @@ public class JenkinsSemanticMetrics { public static final String JENKINS_EXECUTOR_CONNECTING = "jenkins.executor.connecting"; public static final String JENKINS_EXECUTOR_DEFINED = "jenkins.executor.defined"; public static final String JENKINS_EXECUTOR_QUEUE = "jenkins.executor.queue"; + public static final String JENKINS_EXECUTOR_TOTAL = "jenkins.executor.total"; + public static final String JENKINS_EXECUTOR = "jenkins.executor"; + public static final String JENKINS_NODE = "jenkins.node"; + public static final String JENKINS_QUEUE = "jenkins.queue"; public static final String JENKINS_QUEUE_WAITING = "jenkins.queue.waiting"; public static final String JENKINS_QUEUE_BLOCKED = "jenkins.queue.blocked"; public static final String JENKINS_QUEUE_BUILDABLE = "jenkins.queue.buildable"; @@ -34,6 +38,9 @@ public class JenkinsSemanticMetrics { public static final String JENKINS_CLOUD_AGENTS_COMPLETED = "jenkins.cloud.agents.completed"; public static final String JENKINS_DISK_USAGE_BYTES = "jenkins.disk.usage.bytes"; + public static final String JENKINS_PLUGINS = "jenkins.plugins"; + public static final String JENKINS_PLUGINS_UPDATES = "jenkins.plugins.updates"; + public static final String JENKINS_SCM_EVENT_POOL_SIZE = "jenkins.scm.event.pool_size"; public static final String JENKINS_SCM_EVENT_ACTIVE_THREADS = "jenkins.scm.event.active_threads"; public static final String JENKINS_SCM_EVENT_QUEUED_TASKS = "jenkins.scm.event.queued_tasks"; diff --git a/src/main/java/io/jenkins/plugins/opentelemetry/servlet/StaplerInstrumentationServletFilter.java b/src/main/java/io/jenkins/plugins/opentelemetry/servlet/StaplerInstrumentationServletFilter.java index 027a3469b..cec01fd9e 100644 --- a/src/main/java/io/jenkins/plugins/opentelemetry/servlet/StaplerInstrumentationServletFilter.java +++ b/src/main/java/io/jenkins/plugins/opentelemetry/servlet/StaplerInstrumentationServletFilter.java @@ -5,37 +5,42 @@ package io.jenkins.plugins.opentelemetry.servlet; +import edu.umd.cs.findbugs.annotations.Nullable; import hudson.Extension; import hudson.model.User; import io.jenkins.plugins.opentelemetry.api.OpenTelemetryLifecycleListener; import io.jenkins.plugins.opentelemetry.api.ReconfigurableOpenTelemetry; import io.jenkins.plugins.opentelemetry.semconv.JenkinsOtelSemanticAttributes; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.common.AttributesBuilder; +import io.opentelemetry.api.metrics.Meter; import io.opentelemetry.api.trace.Span; -import io.opentelemetry.api.trace.SpanBuilder; import io.opentelemetry.api.trace.SpanKind; import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.context.Context; import io.opentelemetry.context.Scope; +import io.opentelemetry.instrumentation.api.instrumenter.OperationListener; +import io.opentelemetry.instrumentation.api.semconv.http.HttpServerMetrics; import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; import io.opentelemetry.semconv.ClientAttributes; +import io.opentelemetry.semconv.ErrorAttributes; import io.opentelemetry.semconv.HttpAttributes; -import io.opentelemetry.semconv.NetworkAttributes; import io.opentelemetry.semconv.ServerAttributes; import io.opentelemetry.semconv.UrlAttributes; -import io.opentelemetry.semconv.incubating.EnduserIncubatingAttributes; +import io.opentelemetry.semconv.UserAgentAttributes; import io.opentelemetry.semconv.incubating.ThreadIncubatingAttributes; -import org.apache.commons.lang.StringUtils; - -import edu.umd.cs.findbugs.annotations.Nullable; - -import javax.inject.Inject; +import io.opentelemetry.semconv.incubating.UserIncubatingAttributes; import jakarta.servlet.Filter; import jakarta.servlet.FilterChain; -import jakarta.servlet.FilterConfig; import jakarta.servlet.ServletException; import jakarta.servlet.ServletRequest; import jakarta.servlet.ServletResponse; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import org.apache.commons.lang.StringUtils; + +import javax.annotation.PostConstruct; +import javax.inject.Inject; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; @@ -49,8 +54,6 @@ import java.util.logging.Level; import java.util.logging.Logger; import java.util.stream.Collectors; -import java.util.Set; -import java.util.HashSet; /** * Instrumentation of the Stapler MVC framework. @@ -60,11 +63,17 @@ */ @Extension public class StaplerInstrumentationServletFilter implements Filter, OpenTelemetryLifecycleListener { - private static final Set SKIP_PATHS = new HashSet<>(Arrays.asList("static", "adjuncts", "scripts", "plugin", "images", "sse-gateway")); private final static Logger logger = Logger.getLogger(StaplerInstrumentationServletFilter.class.getName()); final AtomicBoolean enabled = new AtomicBoolean(false); List capturedRequestParameters; Tracer tracer; + Meter meter; + OperationListener httpServerMetrics; + + @PostConstruct + public void postConstruct() { + httpServerMetrics = HttpServerMetrics.get().create(meter); + } @Override public void afterConfiguration(ConfigProperties configProperties) { @@ -87,174 +96,175 @@ public void doFilter(ServletRequest servletRequest, ServletResponse servletRespo } public void _doFilter(HttpServletRequest servletRequest, HttpServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { - String pathInfo = servletRequest.getPathInfo(); - List pathInfoTokens = Collections.list(new StringTokenizer(pathInfo, "/")).stream() - .map(token -> (String) token) - .filter(t -> !t.isEmpty()) - .collect(Collectors.toList()); - - if (pathInfoTokens.isEmpty()) { - pathInfoTokens = Collections.singletonList(""); - } - String rootPath = pathInfoTokens.get(0); - // The matched route (path template). - String httpRoute; - if (SKIP_PATHS.contains(rootPath)) { - // skip - filterChain.doFilter(servletRequest, servletResponse); - return; - } - if (rootPath.equals("$stapler")) { - // TODO handle URL pattern /$stapler/bound/ec328aeb-26be-43da-94a3-59f2d683131c/news - filterChain.doFilter(servletRequest, servletResponse); - return; - } - final SpanBuilder spanBuilder; + // Attributes common to Http Server span and metric + AttributesBuilder httpServerMetricOnStartAttributesBuilder = Attributes.builder() + .put(HttpAttributes.HTTP_REQUEST_METHOD, servletRequest.getMethod()) + .put(UrlAttributes.URL_SCHEME, servletRequest.getScheme()) + .put(ServerAttributes.SERVER_ADDRESS, servletRequest.getServerName()) + .put(ServerAttributes.SERVER_PORT, (long) servletRequest.getServerPort()); + + Thread currentThread = Thread.currentThread(); + AttributesBuilder httpServerSpanAttributesBuilder = Attributes.builder() + .putAll(httpServerMetricOnStartAttributesBuilder.build()) + .put(ThreadIncubatingAttributes.THREAD_NAME, currentThread.getName()) + .put(ThreadIncubatingAttributes.THREAD_ID, currentThread.getId()) + .put(ClientAttributes.CLIENT_ADDRESS, servletRequest.getRemoteAddr()) + .put(ClientAttributes.CLIENT_PORT, (long) servletRequest.getRemotePort()) + // See https://opentelemetry.io/docs/specs/semconv/attributes-registry/url/#url-full + // Security notes: + // * `HttpServletRequest.getRequestURL()` is safe not including URL credentials + // * Omit the URL query string to ensure we don't surface secrets. + // The OTel `url.full` spec requires to redact sensitive info including query parameters like + // `AWSAccessKeyId`, `Signature`, `X-Goog-Credential`, `X-Goog-Signature`, or `sig`. It's safer to omit + // the query string. + // Interesting query parameters should be captured explicitly and users can explicitly capture more + // using the config param `otel.instrumentation.servlet.experimental.capture-request-parameters` + // Note: OTel specs may stop making `url.full` mandatory in the future: + // https://github.com/open-telemetry/semantic-conventions/issues/128 + .put(UrlAttributes.URL_FULL, servletRequest.getRequestURL().toString()) + .put(UserAgentAttributes.USER_AGENT_ORIGINAL, servletRequest.getHeader("User-Agent")); + Optional.ofNullable(User.current()).ifPresent(user -> httpServerSpanAttributesBuilder.put(UserIncubatingAttributes.USER_ID, user.getId())); + + Context httpServerDurationMetricContext = httpServerMetrics.onStart(Context.current(), httpServerMetricOnStartAttributesBuilder.build(), System.nanoTime()); + + AttributesBuilder httpServerMetricOnEndAttributesBuilder = Attributes.builder(); try { - if (rootPath.equals("job")) { - // e.g /job/my-war/job/master/lastBuild/console - // e.g /job/my-war/job/master/2/console - ParsedJobUrl parsedJobUrl = parseJobUrl(pathInfoTokens); - httpRoute = parsedJobUrl.urlPattern; - spanBuilder = tracer.spanBuilder(servletRequest.getMethod() + " " + parsedJobUrl.urlPattern); - if (parsedJobUrl.jobName != null) { - spanBuilder.setAttribute(JenkinsOtelSemanticAttributes.CI_PIPELINE_ID, parsedJobUrl.jobName); - } - if (parsedJobUrl.runNumber != null) { - spanBuilder.setAttribute(JenkinsOtelSemanticAttributes.CI_PIPELINE_RUN_NUMBER, parsedJobUrl.runNumber); - } - } else if (rootPath.equals("blue")) { - if (pathInfoTokens.size() == 1) { - httpRoute = "/blue/"; - spanBuilder = tracer.spanBuilder(servletRequest.getMethod() + " " + httpRoute); - } else if ("rest".equals(pathInfoTokens.get(1))) { - if (pathInfoTokens.size() == 2) { - httpRoute = "/blue/rest/"; - spanBuilder = tracer.spanBuilder(servletRequest.getMethod() + " " + httpRoute); - } else if ("organizations".equals(pathInfoTokens.get(2)) && pathInfoTokens.size() > 7) { - // eg /blue/rest/organizations/jenkins/pipelines/ecommerce-antifraud/branches/main/runs/110/blueTestSummary/ - - ParsedJobUrl parsedBlueOceanPipelineUrl = parseBlueOceanRestPipelineUrl(pathInfoTokens); - httpRoute = parsedBlueOceanPipelineUrl.urlPattern; - spanBuilder = tracer.spanBuilder(servletRequest.getMethod() + " " + parsedBlueOceanPipelineUrl.urlPattern); - if (parsedBlueOceanPipelineUrl.jobName != null) { - spanBuilder.setAttribute(JenkinsOtelSemanticAttributes.CI_PIPELINE_ID, parsedBlueOceanPipelineUrl.jobName); + + List pathInfoTokens = Collections.list(new StringTokenizer(servletRequest.getPathInfo(), "/")).stream() + .map(token -> (String) token) + .filter(t -> !t.isEmpty()) + .collect(Collectors.toList()); + + if (pathInfoTokens.isEmpty()) { + pathInfoTokens = Collections.singletonList(""); + } + + String rootPath = pathInfoTokens.get(0); + String httpRoute; + + boolean skipSpan = false; + try { + switch (rootPath) { + case "job" -> { + // e.g /job/my-war/job/master/lastBuild/console + // e.g /job/my-war/job/master/2/console + ParsedJobUrl parsedJobUrl = parseJobUrl(pathInfoTokens); + httpRoute = parsedJobUrl.urlPattern; + Optional.ofNullable(parsedJobUrl.jobName).ifPresent(jobName -> httpServerSpanAttributesBuilder.put(JenkinsOtelSemanticAttributes.CI_PIPELINE_ID, jobName)); + Optional.ofNullable(parsedJobUrl.runNumber).ifPresent(runNumber -> httpServerSpanAttributesBuilder.put(JenkinsOtelSemanticAttributes.CI_PIPELINE_RUN_NUMBER, runNumber)); + } + case "blue" -> { + if (pathInfoTokens.size() == 1) { + httpRoute = "/blue/"; + } else if ("rest".equals(pathInfoTokens.get(1))) { + if (pathInfoTokens.size() == 2) { + httpRoute = "/blue/rest/"; + } else if ("organizations".equals(pathInfoTokens.get(2)) && pathInfoTokens.size() > 7) { + // eg /blue/rest/organizations/jenkins/pipelines/ecommerce-antifraud/branches/main/runs/110/blueTestSummary/ + ParsedJobUrl parsedBlueOceanPipelineUrl = parseBlueOceanRestPipelineUrl(pathInfoTokens); + httpRoute = parsedBlueOceanPipelineUrl.urlPattern; + Optional.ofNullable(parsedBlueOceanPipelineUrl.jobName).ifPresent(jobName -> httpServerSpanAttributesBuilder.put(JenkinsOtelSemanticAttributes.CI_PIPELINE_ID, jobName)); + Optional.ofNullable(parsedBlueOceanPipelineUrl.runNumber).ifPresent(runNumber -> httpServerSpanAttributesBuilder.put(JenkinsOtelSemanticAttributes.CI_PIPELINE_RUN_NUMBER, runNumber)); + } else if ("classes".equals(pathInfoTokens.get(2)) && pathInfoTokens.size() > 3) { + // eg /blue/rest/classes/io.jenkins.blueocean.rest.impl.pipeline.PipelineRunImpl/ + String blueOceanClass = pathInfoTokens.get(3); + httpRoute = "/blue/rest/classes/:blueOceanClass"; + httpServerSpanAttributesBuilder.put("blueOceanClass", blueOceanClass); + } else { + // eg /blue/rest/i18n/blueocean-personalization/1.25.2/jenkins.plugins.blueocean.personalization.Messages/en-US + httpRoute = "/blue/rest/" + pathInfoTokens.get(2) + "/*"; + } + } else { + httpRoute = "/blue/" + pathInfoTokens.get(1) + "/*"; } - if (parsedBlueOceanPipelineUrl.runNumber != null) { - spanBuilder.setAttribute(JenkinsOtelSemanticAttributes.CI_PIPELINE_RUN_NUMBER, parsedBlueOceanPipelineUrl.runNumber); + } + case "administrativeMonitor" -> { + // eg GET /administrativeMonitor/hudson.diagnosis.ReverseProxySetupMonitor/testForReverseProxySetup/http://localhost:8080/jenkins/manage/ + httpRoute = "/administrativeMonitor/:administrativeMonitor/*"; + if (pathInfoTokens.size() > 1) { + httpServerSpanAttributesBuilder.put("administrativeMonitor", pathInfoTokens.get(1)); } - } else if ("classes".equals(pathInfoTokens.get(2)) && pathInfoTokens.size() > 3) { - // eg /blue/rest/classes/io.jenkins.blueocean.rest.impl.pipeline.PipelineRunImpl/ - String blueOceanClass = pathInfoTokens.get(3); - httpRoute = "/blue/rest/classes/:blueOceanClass"; - spanBuilder = tracer.spanBuilder(servletRequest.getMethod() + " " + httpRoute) - .setAttribute("blueOceanClass", blueOceanClass); - } else { - // eg /blue/rest/i18n/blueocean-personalization/1.25.2/jenkins.plugins.blueocean.personalization.Messages/en-US - httpRoute = "/blue/rest/" + pathInfoTokens.get(2) + "/*"; - spanBuilder = tracer.spanBuilder(servletRequest.getMethod() + " " + httpRoute); } - } else { - httpRoute = "/blue/" + pathInfoTokens.get(1) + "/*"; - spanBuilder = tracer.spanBuilder(servletRequest.getMethod() + " " + httpRoute); + case "asynchPeople" -> { + httpRoute = "/asynchPeople"; + } + case "computer" -> { + // /computer/(master)/ + httpRoute = "/computer/:computer/*"; + // TODO add details + } + case "credentials" -> { + // eg /credentials/store/system/domain/_/ + httpRoute = "/credentials/store/:store/domain/:domain/*"; + // TODO add details + } + case "descriptorByName" -> { + httpRoute = "/descriptorByName/:descriptor/*"; + if (pathInfoTokens.size() > 1) { + httpServerSpanAttributesBuilder.put("descriptor", pathInfoTokens.get(1)); + } + } + case "extensionList" -> { + // eg /extensionList/hudson.diagnosis.MemoryUsageMonitor/0/heap/graph + httpRoute = "/extensionList/:extension/*"; + if (pathInfoTokens.size() > 1) { + httpServerSpanAttributesBuilder.put("extension", pathInfoTokens.get(1)); + } + } + case "fingerprint" -> { + httpRoute = "/fingerprint/:fingerprint"; + httpServerSpanAttributesBuilder.put("fingerprint", servletRequest.getPathInfo().substring("/fingerprint/".length())); + if (pathInfoTokens.size() > 1) { + httpServerSpanAttributesBuilder.put("fingerprint", pathInfoTokens.get(1)); + } + } + case "user" -> { + //eg /user/cyrille.leclerc/ /user/cyrille.leclerc/configure /user/cyrille.leclerc/my-views/view/all/ + httpRoute = "/user/:user/*"; + if (pathInfoTokens.size() > 1) { + httpServerSpanAttributesBuilder.put("user", pathInfoTokens.get(1)); + } + } + default -> { + // "static", "adjuncts", "scripts", "plugin", "images", "sse-gateway" + // e.g /$stapler/bound/ec328aeb-26be-43da-94a3-59f2d683131c/news + httpRoute = "/*"; + skipSpan = true; + } } + } catch (RuntimeException e) { + logger.log(Level.INFO, () -> "Exception processing URL " + servletRequest.getPathInfo() + ", skip instrumentation with tracing: " + e); + httpRoute = "/##error-processing-url-to-extract-http-route##"; + } - } else if (rootPath.equals("administrativeMonitor")) { - // eg GET /administrativeMonitor/hudson.diagnosis.ReverseProxySetupMonitor/testForReverseProxySetup/http://localhost:8080/jenkins/manage/ - httpRoute = "/administrativeMonitor/:administrativeMonitor/*"; - spanBuilder = tracer.spanBuilder(servletRequest.getMethod() + " " + "/administrativeMonitor/"); - if (pathInfoTokens.size() > 1) { - spanBuilder.setAttribute("administrativeMonitor", pathInfoTokens.get(1)); - } - } else if (rootPath.equals("asynchPeople")) { - httpRoute = "/asynchPeople"; - spanBuilder = tracer.spanBuilder(servletRequest.getMethod() + " " + "/asynchPeople"); - } else if (rootPath.equals("computer")) { - // /computer/(master)/ - httpRoute = "/computer/:computer/*"; - spanBuilder = tracer.spanBuilder(servletRequest.getMethod() + " " + "/computer/*"); - // TODO more details - } else if (rootPath.equals("credentials")) { - // eg /credentials/store/system/domain/_/ - httpRoute = "/credentials/store/:store/domain/:domain/*"; - spanBuilder = tracer.spanBuilder(servletRequest.getMethod() + " " + "/credentials/*"); - // TODO more details - } else if (rootPath.equals("descriptorByName")) { - httpRoute = "/descriptorByName/:descriptor/*"; - spanBuilder = tracer.spanBuilder(servletRequest.getMethod() + " " + "/descriptorByName/"); - if (pathInfoTokens.size() > 1) { - spanBuilder.setAttribute("descriptor", pathInfoTokens.get(1)); - } - } else if (rootPath.equals("extensionList")) { - // eg /extensionList/hudson.diagnosis.MemoryUsageMonitor/0/heap/graph - httpRoute = "/extensionList/:extension/*"; - spanBuilder = tracer.spanBuilder(servletRequest.getMethod() + " " + "/extensionList/"); - if (pathInfoTokens.size() > 1) { - spanBuilder.setAttribute("extension", pathInfoTokens.get(1)); - } - } else if (rootPath.equals("fingerprint")) { - httpRoute = "/fingerprint/:fingerprint"; - spanBuilder = tracer.spanBuilder(servletRequest.getMethod() + " " + "/fingerprint/"); - spanBuilder.setAttribute("fingerprint", pathInfo.substring("/fingerprint/".length())); - if (pathInfoTokens.size() > 1) { - spanBuilder.setAttribute("fingerprint", pathInfoTokens.get(1)); + httpServerSpanAttributesBuilder.put(HttpAttributes.HTTP_ROUTE, httpRoute); + httpServerMetricOnEndAttributesBuilder.put(HttpAttributes.HTTP_ROUTE, httpRoute); + capturedRequestParameters.forEach( + parameterName -> + Optional.ofNullable(servletRequest.getParameter(parameterName)) + .ifPresent(value -> httpServerSpanAttributesBuilder.put("http.request.parameter." + parameterName, value))); + + Span span = skipSpan ? Span.getInvalid() : tracer.spanBuilder(servletRequest.getMethod() + " " + httpRoute) + .setAllAttributes(httpServerSpanAttributesBuilder.build()) + .setSpanKind(SpanKind.SERVER).startSpan(); + try (Scope scope = span.makeCurrent()) { + filterChain.doFilter(servletRequest, servletResponse); + } catch (IOException | ServletException | RuntimeException e) { + if (servletResponse.getStatus() < 500) { + servletResponse.setStatus(500); } - } else if (rootPath.equals("user")) { - //eg /user/cyrille.leclerc/ /user/cyrille.leclerc/configure /user/cyrille.leclerc/my-views/view/all/ - httpRoute = "/user/:user/*"; - spanBuilder = tracer.spanBuilder(servletRequest.getMethod() + " " + "/user/*"); - if (pathInfoTokens.size() > 1) { - spanBuilder.setAttribute("user", pathInfoTokens.get(1)); - } - } else { - httpRoute = "/" + rootPath + "/*"; - spanBuilder = tracer.spanBuilder(servletRequest.getMethod() + " " + pathInfo); + span.recordException(e); + httpServerMetricOnEndAttributesBuilder.put(ErrorAttributes.ERROR_TYPE, e.getClass().getName()); + throw e; + } finally { + span.setAttribute(HttpAttributes.HTTP_RESPONSE_STATUS_CODE, servletResponse.getStatus()); + httpServerMetricOnEndAttributesBuilder.put(HttpAttributes.HTTP_RESPONSE_STATUS_CODE, servletResponse.getStatus()); + span.end(); } - } catch (RuntimeException e) { - logger.log(Level.INFO, () -> "Exception processing URL " + pathInfo + ", skip instrumentation with tracing: " + e); - filterChain.doFilter(servletRequest, servletResponse); - return; - } - - - String httpTarget = servletRequest.getRequestURI(); - String queryString = servletRequest.getQueryString(); - if (queryString != null && !queryString.isEmpty()) { - httpTarget += "?" + queryString; - } - - Thread currentThread = Thread.currentThread(); - spanBuilder - .setAttribute(ClientAttributes.CLIENT_ADDRESS, servletRequest.getRemoteAddr()) - .setAttribute(UrlAttributes.URL_SCHEME, servletRequest.getScheme()) - .setAttribute(ServerAttributes.SERVER_ADDRESS, servletRequest.getServerName()) - .setAttribute(ServerAttributes.SERVER_PORT, (long) servletRequest.getServerPort()) - .setAttribute(HttpAttributes.HTTP_REQUEST_METHOD, servletRequest.getMethod()) - .setAttribute(UrlAttributes.URL_PATH, httpTarget) - .setAttribute(HttpAttributes.HTTP_ROUTE, httpRoute) - .setAttribute(NetworkAttributes.NETWORK_TRANSPORT, NetworkAttributes.NetworkTransportValues.TCP) - .setAttribute(ClientAttributes.CLIENT_ADDRESS, servletRequest.getRemoteAddr()) - .setAttribute(ClientAttributes.CLIENT_PORT, (long) servletRequest.getRemotePort()) - .setAttribute(ThreadIncubatingAttributes.THREAD_NAME, currentThread.getName()) - .setAttribute(ThreadIncubatingAttributes.THREAD_ID, currentThread.getId()) - .setSpanKind(SpanKind.SERVER); - - Optional.ofNullable(User.current()).ifPresent(user -> spanBuilder.setAttribute(EnduserIncubatingAttributes.ENDUSER_ID, user.getId())); - - capturedRequestParameters.forEach( - parameterName -> - Optional.ofNullable(servletRequest.getParameter(parameterName)) - .ifPresent(value -> spanBuilder.setAttribute("http.request.parameter." + parameterName, value))); - - Span span = spanBuilder.startSpan(); - try (Scope scope = span.makeCurrent()) { - filterChain.doFilter(servletRequest, servletResponse); - span.setAttribute(HttpAttributes.HTTP_RESPONSE_STATUS_CODE, servletResponse.getStatus()); } finally { - span.end(); + httpServerMetrics.onEnd(httpServerDurationMetricContext, httpServerMetricOnEndAttributesBuilder.build(), System.nanoTime()); } - } /** @@ -628,16 +638,6 @@ public String toString() { } } - @Override - public void destroy() { - - } - - @Override - public void init(FilterConfig filterConfig) throws ServletException { - - } - @Override public boolean equals(Object o) { if (this == o) return true; @@ -652,5 +652,6 @@ public int hashCode() { @Inject public void setTracer(ReconfigurableOpenTelemetry openTelemetry) { this.tracer = openTelemetry.getTracer("io.jenkins.stapler"); + this.meter = openTelemetry.getMeter("io.jenkins.stapler"); } }