Repository: jenkinsci/prometheus-plugin (java)

Issue:
Metric for waiting time duration of builds
### What feature do you want to see added?

Can we add a metric for waiting time duration for each build. We want to analyze waiting times. 

![Image](https://github.com/user-attachments/assets/da1cb8b8-72c5-4ff3-b7ee-08d26e092f55)

### Upstream changes

_No response_

### Are you interested in contributing this feature?

_No response_

Pull request description:
New Gauge which displays the waiting time of a run
Fixes #730


Patch:
diff --git a/src/main/java/org/jenkinsci/plugins/prometheus/JobCollector.java b/src/main/java/org/jenkinsci/plugins/prometheus/JobCollector.java
index 16c66ba..b593ddd 100644
--- a/src/main/java/org/jenkinsci/plugins/prometheus/JobCollector.java
+++ b/src/main/java/org/jenkinsci/plugins/prometheus/JobCollector.java
@@ -49,6 +49,7 @@ public class JobCollector extends Collector {
         public MetricCollector<Run<?, ?>, ? extends Collector> jobBuildTestsFailing;
         public MetricCollector<Run<?,?>, ? extends Collector> jobBuildLikelyStuck;
         private MetricCollector<Run<?, ?>, ? extends Collector> buildLogFileSizeGauge;
+        private MetricCollector<Run<?, ?>, ? extends Collector> jobBuildWaitingDurationGauge;
 
         private final String buildPrefix;
 
@@ -69,6 +70,7 @@ public class JobCollector extends Collector {
             this.stageBuildResultOrdinal = factory.createRunCollector(CollectorType.STAGE_BUILDRESULT_ORDINAL, ArrayUtils.add(labelNameArray, "stage"), buildPrefix);
             this.jobBuildLikelyStuck = factory.createRunCollector(CollectorType.BUILD_LIKELY_STUCK_GAUGE, labelNameArray, buildPrefix);
             this.buildLogFileSizeGauge = factory.createRunCollector(CollectorType.BUILD_LOGFILE_SIZE_GAUGE, labelNameArray, buildPrefix);
+            this.jobBuildWaitingDurationGauge = factory.createRunCollector(CollectorType.BUILD_WAITING_GAUGE, labelNameArray, buildPrefix);
         }
     }
 
@@ -214,6 +216,7 @@ public class JobCollector extends Collector {
         addSamples(allSamples, buildMetrics.jobBuildResultOrdinal.collect(), "Adding [{}] samples from gauge ({})");
         addSamples(allSamples, buildMetrics.jobBuildResult.collect(), "Adding [{}] samples from gauge ({})");
         addSamples(allSamples, buildMetrics.jobBuildDuration.collect(), "Adding [{}] samples from gauge ({})");
+        addSamples(allSamples, buildMetrics.jobBuildWaitingDurationGauge.collect(), "Adding [{}] samples from gauge ({})");
         addSamples(allSamples, buildMetrics.jobBuildStartMillis.collect(), "Adding [{}] samples from gauge ({})");
         addSamples(allSamples, buildMetrics.jobBuildTestsTotal.collect(), "Adding [{}] samples from gauge ({})");
         addSamples(allSamples, buildMetrics.jobBuildTestsSkipped.collect(), "Adding [{}] samples from gauge ({})");
@@ -289,6 +292,7 @@ public class JobCollector extends Collector {
         buildMetrics.jobBuildTestsFailing.calculateMetric(run, buildLabelValueArray);
         buildMetrics.jobBuildLikelyStuck.calculateMetric(run,buildLabelValueArray);
         buildMetrics.buildLogFileSizeGauge.calculateMetric(run, buildLabelValueArray);
+        buildMetrics.jobBuildWaitingDurationGauge.calculateMetric(run, buildLabelValueArray);
     }
 
 }
diff --git a/src/main/java/org/jenkinsci/plugins/prometheus/collectors/CollectorType.java b/src/main/java/org/jenkinsci/plugins/prometheus/collectors/CollectorType.java
index 000a012..c0222c5 100644
--- a/src/main/java/org/jenkinsci/plugins/prometheus/collectors/CollectorType.java
+++ b/src/main/java/org/jenkinsci/plugins/prometheus/collectors/CollectorType.java
@@ -8,6 +8,7 @@ public enum CollectorType {
     JENKINS_QUIETDOWN_GAUGE("quietdown"),
     NODES_ONLINE_GAUGE("nodes_online"),
     BUILD_DURATION_GAUGE("build_duration_milliseconds"),
+    BUILD_WAITING_GAUGE("build_waiting_milliseconds"),
     BUILD_LOGFILE_SIZE_GAUGE("build_logfile_size_bytes"),
     BUILD_DURATION_SUMMARY("duration_milliseconds_summary"),
     BUILD_RESULT_GAUGE("build_result"),
diff --git a/src/main/java/org/jenkinsci/plugins/prometheus/collectors/builds/BuildCollectorFactory.java b/src/main/java/org/jenkinsci/plugins/prometheus/collectors/builds/BuildCollectorFactory.java
index 3ed3509..08478dc 100644
--- a/src/main/java/org/jenkinsci/plugins/prometheus/collectors/builds/BuildCollectorFactory.java
+++ b/src/main/java/org/jenkinsci/plugins/prometheus/collectors/builds/BuildCollectorFactory.java
@@ -45,6 +45,8 @@ public class BuildCollectorFactory extends BaseCollectorFactory {
                 return saveBuildCollector(new BuildTotalCounter(labelNames, namespace, subsystem, prefix));
             case BUILD_LOGFILE_SIZE_GAUGE:
                 return saveBuildCollector(new BuildLogFileSizeGauge(labelNames, namespace, subsystem, prefix));
+            case BUILD_WAITING_GAUGE:
+                return saveBuildCollector(new BuildWaitingDurationGauge(labelNames, namespace, subsystem, prefix));
             default:
                 return new NoOpMetricCollector<>();
         }
diff --git a/src/main/java/org/jenkinsci/plugins/prometheus/collectors/builds/BuildWaitingDurationGauge.java b/src/main/java/org/jenkinsci/plugins/prometheus/collectors/builds/BuildWaitingDurationGauge.java
new file mode 100644
index 0000000..b0892b6
--- /dev/null
+++ b/src/main/java/org/jenkinsci/plugins/prometheus/collectors/builds/BuildWaitingDurationGauge.java
@@ -0,0 +1,40 @@
+package org.jenkinsci.plugins.prometheus.collectors.builds;
+
+import hudson.model.Run;
+import io.prometheus.client.Gauge;
+import io.prometheus.client.SimpleCollector;
+import jenkins.metrics.impl.TimeInQueueAction;
+import org.jenkinsci.plugins.prometheus.collectors.CollectorType;
+
+public class BuildWaitingDurationGauge extends BuildsMetricCollector<Run<?, ?>, Gauge> {
+
+    protected BuildWaitingDurationGauge(String[] labelNames, String namespace, String subsystem, String namePrefix) {
+        super(labelNames, namespace, subsystem, namePrefix);
+    }
+
+    @Override
+    protected CollectorType getCollectorType() {
+        return CollectorType.BUILD_WAITING_GAUGE;
+    }
+
+    @Override
+    protected String getHelpText() {
+        return "Duration this Run spent queuing, that is the wall time from when it entered the queue until it left the queue.";
+    }
+
+    @Override
+    protected SimpleCollector.Builder<?, Gauge> getCollectorBuilder() {
+        return Gauge.build();
+    }
+
+    @Override
+    public void calculateMetric(Run<?, ?> jenkinsObject, String[] labelValues) {
+        if (!jenkinsObject.isBuilding()) {
+            TimeInQueueAction action = jenkinsObject.getAction(TimeInQueueAction.class);
+            if (action != null) {
+                long queuingDurationMillis = action.getQueuingDurationMillis();
+                collector.labels(labelValues).set(queuingDurationMillis);
+            }
+        }
+    }
+}


Test patch:
diff --git a/src/test/java/org/jenkinsci/plugins/prometheus/collectors/builds/BuildWaitingDurationGaugeTest.java b/src/test/java/org/jenkinsci/plugins/prometheus/collectors/builds/BuildWaitingDurationGaugeTest.java
new file mode 100644
index 0000000..e066744
--- /dev/null
+++ b/src/test/java/org/jenkinsci/plugins/prometheus/collectors/builds/BuildWaitingDurationGaugeTest.java
@@ -0,0 +1,43 @@
+package org.jenkinsci.plugins.prometheus.collectors.builds;
+
+import io.prometheus.client.Collector;
+import jenkins.metrics.impl.TimeInQueueAction;
+import org.jenkinsci.plugins.prometheus.collectors.testutils.MockedRunCollectorTest;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+
+import java.util.List;
+
+
+public class BuildWaitingDurationGaugeTest extends MockedRunCollectorTest {
+
+    @Test
+    public void testCalculateDurationWhenRunIsNotBuilding() {
+        Mockito.when(mock.isBuilding()).thenReturn(false);
+        TimeInQueueAction timeInQueueAction = new TimeInQueueAction(200,1,1,1);
+        Mockito.doReturn(timeInQueueAction).when(mock).getAction(TimeInQueueAction.class);
+
+        BuildWaitingDurationGauge sut = new BuildWaitingDurationGauge(getLabelNames(), getNamespace(), getSubSystem(), "");
+
+        sut.calculateMetric(mock, getLabelValues());
+
+        List<Collector.MetricFamilySamples> collect = sut.collect();
+        Assertions.assertEquals(1, collect.size());
+        Assertions.assertEquals(200L, collect.get(0).samples.get(0).value);
+        Assertions.assertEquals("default_jenkins_builds_build_waiting_milliseconds", collect.get(0).samples.get(0).name);
+    }
+
+    @Test
+    public void testCalculateDurationIsNotCalculatedWhenRunIsBuilding() {
+        Mockito.when(mock.isBuilding()).thenReturn(true);
+
+        BuildWaitingDurationGauge sut = new BuildWaitingDurationGauge(getLabelNames(), getNamespace(), getSubSystem(), "");
+
+        sut.calculateMetric(mock, getLabelValues());
+
+        List<Collector.MetricFamilySamples> collect = sut.collect();
+        Assertions.assertEquals(1, collect.size());
+        Assertions.assertEquals(0, collect.get(0).samples.size());
+    }
+}
\ No newline at end of file