Repository: jenkinsci/prometheus-plugin (java)

Issue:
Metrics to identify stuck jobs
### What feature do you want to see added?

We would like a metric or metrics that would help us identify a job that is stuck.

We use jenkins jobs to run ansible playbooks over a large number of hosts, and in some sites the job might take a long time to run due to slowness in the underlying hosts, and sometimes job would simply be stuck

So the way we identify this today is by looking at the job console output log to see if there is any recent progress in the log. If there are no new log lines updated in the last 10 minutes or so, then we know that we need to diagnose that job further. 

Since we have a huge scale of sites and jobs with ever increasing number of sites and jobs, it is always not possible to always eyeball job logs, and we would need some form of metrics to identify a stuck job log, so we can have alerting integrations to alert engineers.

we can use 
`default_jenkins_builds_duration_milliseconds_summary_count` and `default_jenkins_builds_duration_milliseconds_summary_sum` to find average runtime of job and compare with `default_jenkins_builds_running_build_duration_milliseconds` to know if a job has exceeded the average time, but that won't necessarily mean that the job is stuck.

### Upstream changes

_No response_

Pull request:
Creating Build Metrics for log is updated and build is likely stuck
Fixes #567

### Checklist

- [x] Includes tests covering the new functionality?
- [x] Ready for review
- [x] Follows CONTRIBUTING rules

### Notify

@Waschndolos


Patch:
diff --git a/src/main/java/org/jenkinsci/plugins/prometheus/ExecutorCollector.java b/src/main/java/org/jenkinsci/plugins/prometheus/ExecutorCollector.java
index 67559ef..1ddfbd7 100644
--- a/src/main/java/org/jenkinsci/plugins/prometheus/ExecutorCollector.java
+++ b/src/main/java/org/jenkinsci/plugins/prometheus/ExecutorCollector.java
@@ -19,6 +19,9 @@ public class ExecutorCollector extends Collector {
 
     private static final Logger LOGGER = LoggerFactory.getLogger(ExecutorCollector.class);
 
+    public ExecutorCollector() {
+    }
+
     @Override
     public List<MetricFamilySamples> collect() {
         LOGGER.debug("Collecting executor metrics for prometheus");
@@ -30,13 +33,13 @@ public class ExecutorCollector extends Collector {
 
         List<MetricCollector<LoadStatistics.LoadStatisticsSnapshot, ? extends Collector>> collectors = new ArrayList<>();
 
-        collectors.add(factory.createExecutorCollector(CollectorType.EXECUTORS_AVAILABLE_GAUGE, labelNameArray, prefix));
-        collectors.add(factory.createExecutorCollector(CollectorType.EXECUTORS_BUSY_GAUGE, labelNameArray, prefix));
-        collectors.add(factory.createExecutorCollector(CollectorType.EXECUTORS_CONNECTING_GAUGE, labelNameArray, prefix));
-        collectors.add(factory.createExecutorCollector(CollectorType.EXECUTORS_DEFINED_GAUGE, labelNameArray, prefix));
-        collectors.add(factory.createExecutorCollector(CollectorType.EXECUTORS_IDLE_GAUGE, labelNameArray, prefix));
-        collectors.add(factory.createExecutorCollector(CollectorType.EXECUTORS_ONLINE_GAUGE, labelNameArray, prefix));
-        collectors.add(factory.createExecutorCollector(CollectorType.EXECUTORS_QUEUE_LENGTH_GAUGE, labelNameArray, prefix));
+        collectors.add(factory.createLoadStatisticsCollector(CollectorType.EXECUTORS_AVAILABLE_GAUGE, labelNameArray, prefix));
+        collectors.add(factory.createLoadStatisticsCollector(CollectorType.EXECUTORS_BUSY_GAUGE, labelNameArray, prefix));
+        collectors.add(factory.createLoadStatisticsCollector(CollectorType.EXECUTORS_CONNECTING_GAUGE, labelNameArray, prefix));
+        collectors.add(factory.createLoadStatisticsCollector(CollectorType.EXECUTORS_DEFINED_GAUGE, labelNameArray, prefix));
+        collectors.add(factory.createLoadStatisticsCollector(CollectorType.EXECUTORS_IDLE_GAUGE, labelNameArray, prefix));
+        collectors.add(factory.createLoadStatisticsCollector(CollectorType.EXECUTORS_ONLINE_GAUGE, labelNameArray, prefix));
+        collectors.add(factory.createLoadStatisticsCollector(CollectorType.EXECUTORS_QUEUE_LENGTH_GAUGE, labelNameArray, prefix));
 
         LOGGER.debug("getting load statistics for Executors");
 
diff --git a/src/main/java/org/jenkinsci/plugins/prometheus/JobCollector.java b/src/main/java/org/jenkinsci/plugins/prometheus/JobCollector.java
index a3e0558..0fd4037 100644
--- a/src/main/java/org/jenkinsci/plugins/prometheus/JobCollector.java
+++ b/src/main/java/org/jenkinsci/plugins/prometheus/JobCollector.java
@@ -32,6 +32,7 @@ public class JobCollector extends Collector {
     private MetricCollector<Job<?, ?>, ? extends Collector> nbBuildsGauge;
     private MetricCollector<Job<?, ?>, ? extends Collector> buildDiscardGauge;
     private MetricCollector<Job<?, ?>, ? extends Collector> currentRunDurationGauge;
+    private MetricCollector<Job<?,?>, ? extends Collector> logUpdatedGauge;
 
     private static class BuildMetrics {
 
@@ -44,6 +45,8 @@ public class JobCollector extends Collector {
         public MetricCollector<Run<?, ?>, ? extends Collector> jobBuildTestsSkipped;
         public MetricCollector<Run<?, ?>, ? extends Collector> jobBuildTestsFailing;
 
+        public MetricCollector<Run<?,?>, ? extends Collector> jobBuildLikelyStuck;
+
         private final String buildPrefix;
 
         public BuildMetrics(String buildPrefix) {
@@ -60,6 +63,7 @@ public class JobCollector extends Collector {
             this.jobBuildTestsSkipped = factory.createRunCollector(CollectorType.SKIPPED_TESTS_GAUGE, labelNameArray, buildPrefix);
             this.jobBuildTestsFailing = factory.createRunCollector(CollectorType.FAILED_TESTS_GAUGE, labelNameArray, buildPrefix);
             this.stageSummary = factory.createRunCollector(CollectorType.STAGE_SUMMARY, labelStageNameArray, buildPrefix);
+            this.jobBuildLikelyStuck = factory.createRunCollector(CollectorType.BUILD_LIKELY_STUCK_GAUGE, labelNameArray, buildPrefix);
         }
     }
 
@@ -129,6 +133,8 @@ public class JobCollector extends Collector {
 
         currentRunDurationGauge = factory.createJobCollector(CollectorType.CURRENT_RUN_DURATION_GAUGE, labelBaseNameArray);
 
+        logUpdatedGauge = factory.createJobCollector(CollectorType.JOB_LOG_UPDATED_GAUGE, labelBaseNameArray);
+
         if (PrometheusConfiguration.get().isPerBuildMetrics()) {
             labelNameArray = Arrays.copyOf(labelNameArray, labelNameArray.length + 1);
             labelNameArray[labelNameArray.length - 1] = "number";
@@ -164,6 +170,7 @@ public class JobCollector extends Collector {
         addSamples(samples, nbBuildsGauge.collect(), "Adding [{}] samples from gauge ({})");
         addSamples(samples, buildDiscardGauge.collect(), "Adding [{}] samples from gauge ({})");
         addSamples(samples, currentRunDurationGauge.collect(), "Adding [{}] samples from gauge ({})");
+        addSamples(samples, logUpdatedGauge.collect(), "Adding [{}] samples from gauge ({})");
         addSamples(samples, lastBuildMetrics);
         if (PrometheusConfiguration.get().isPerBuildMetrics()) {
             addSamples(samples, perBuildMetrics);
@@ -190,6 +197,7 @@ public class JobCollector extends Collector {
         addSamples(allSamples, buildMetrics.jobBuildTestsTotal.collect(), "Adding [{}] samples from gauge ({})");
         addSamples(allSamples, buildMetrics.jobBuildTestsSkipped.collect(), "Adding [{}] samples from gauge ({})");
         addSamples(allSamples, buildMetrics.jobBuildTestsFailing.collect(), "Adding [{}] samples from gauge ({})");
+        addSamples(allSamples, buildMetrics.jobBuildLikelyStuck.collect(), "Adding [{}] samples from gauge ({})");
         addSamples(allSamples, buildMetrics.stageSummary.collect(), "Adding [{}] samples from summary ({})");
     }
 
@@ -217,6 +225,8 @@ public class JobCollector extends Collector {
         jobHealthScoreGauge.calculateMetric(job, baseLabelValueArray);
         buildDiscardGauge.calculateMetric(job, baseLabelValueArray);
         currentRunDurationGauge.calculateMetric(job, baseLabelValueArray);
+        logUpdatedGauge.calculateMetric(job, baseLabelValueArray);
+
         processRun(job, lastBuild, baseLabelValueArray, lastBuildMetrics);
 
         Run<?, ?> run = lastBuild;
@@ -278,6 +288,7 @@ public class JobCollector extends Collector {
         buildMetrics.jobBuildTestsTotal.calculateMetric(run, buildLabelValueArray);
         buildMetrics.jobBuildTestsSkipped.calculateMetric(run, buildLabelValueArray);
         buildMetrics.jobBuildTestsFailing.calculateMetric(run, buildLabelValueArray);
+        buildMetrics.jobBuildLikelyStuck.calculateMetric(run,buildLabelValueArray);
     }
 
 }
diff --git a/src/main/java/org/jenkinsci/plugins/prometheus/collectors/CollectorFactory.java b/src/main/java/org/jenkinsci/plugins/prometheus/collectors/CollectorFactory.java
index 756dd39..cc72019 100644
--- a/src/main/java/org/jenkinsci/plugins/prometheus/collectors/CollectorFactory.java
+++ b/src/main/java/org/jenkinsci/plugins/prometheus/collectors/CollectorFactory.java
@@ -13,7 +13,6 @@ import org.jenkinsci.plugins.prometheus.collectors.disk.DiskCollectorFactory;
 import org.jenkinsci.plugins.prometheus.collectors.executors.ExecutorCollectorFactory;
 import org.jenkinsci.plugins.prometheus.collectors.jenkins.JenkinsCollectorFactory;
 import org.jenkinsci.plugins.prometheus.collectors.jobs.JobCollectorFactory;
-import org.json.Cookie;
 
 import java.nio.file.FileStore;
 
@@ -52,7 +51,7 @@ public class CollectorFactory {
         return jenkinsCollectorFactory.createCollector(type, labelNames);
     }
 
-    public MetricCollector<LoadStatistics.LoadStatisticsSnapshot, ? extends Collector> createExecutorCollector(CollectorType type, String[] labelNames, String prefix) {
+    public MetricCollector<LoadStatistics.LoadStatisticsSnapshot, ? extends Collector> createLoadStatisticsCollector(CollectorType type, String[] labelNames, String prefix) {
         return executorCollectorFactory.createCollector(type, labelNames, prefix);
     }
 
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 dfafd8d..1ffa967 100644
--- a/src/main/java/org/jenkinsci/plugins/prometheus/collectors/CollectorType.java
+++ b/src/main/java/org/jenkinsci/plugins/prometheus/collectors/CollectorType.java
@@ -13,6 +13,8 @@ public enum CollectorType {
     BUILD_RESULT_ORDINAL_GAUGE("build_result_ordinal"),
     BUILD_START_GAUGE("build_start_time_milliseconds"),
     BUILD_SUCCESSFUL_COUNTER("success_build_count"),
+    BUILD_LIKELY_STUCK_GAUGE("likely_stuck"),
+
     FAILED_TESTS_GAUGE("build_tests_failing"),
     SKIPPED_TESTS_GAUGE("last_build_tests_skipped"),
     STAGE_SUMMARY("stage_duration_milliseconds_summary"),
@@ -53,9 +55,7 @@ public enum CollectorType {
     COVERAGE_FILE_MISSED("coverage_file_missed"),
     COVERAGE_FILE_TOTAL("coverage_file_total"),
 
-
-
-    ;
+    JOB_LOG_UPDATED_GAUGE("job_log_updated");
 
     private final String name;
 
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 f9148a6..17c2ebb 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
@@ -39,6 +39,8 @@ public class BuildCollectorFactory extends BaseCollectorFactory {
                 return saveBuildCollector(new StageSummary(labelNames, namespace, subsystem, prefix));
             case TOTAL_TESTS_GAUGE:
                 return saveBuildCollector(new TotalTestsGauge(labelNames, namespace, subsystem, prefix));
+            case BUILD_LIKELY_STUCK_GAUGE:
+                return saveBuildCollector(new BuildLikelyStuckGauge(labelNames, namespace, subsystem, prefix));
             default:
                 return new NoOpMetricCollector<>();
         }
diff --git a/src/main/java/org/jenkinsci/plugins/prometheus/collectors/builds/BuildLikelyStuckGauge.java b/src/main/java/org/jenkinsci/plugins/prometheus/collectors/builds/BuildLikelyStuckGauge.java
new file mode 100644
index 0000000..c722aec
--- /dev/null
+++ b/src/main/java/org/jenkinsci/plugins/prometheus/collectors/builds/BuildLikelyStuckGauge.java
@@ -0,0 +1,44 @@
+package org.jenkinsci.plugins.prometheus.collectors.builds;
+
+import hudson.model.Executor;
+import hudson.model.Run;
+import io.prometheus.client.Gauge;
+import io.prometheus.client.SimpleCollector;
+import org.jenkinsci.plugins.prometheus.collectors.CollectorType;
+
+public class BuildLikelyStuckGauge extends BuildsMetricCollector<Run<?, ?>, Gauge> {
+
+    protected BuildLikelyStuckGauge(String[] labelNames, String namespace, String subsystem, String prefix) {
+        super(labelNames, namespace, subsystem, prefix);
+    }
+
+    @Override
+    protected CollectorType getCollectorType() {
+        return CollectorType.BUILD_LIKELY_STUCK_GAUGE;
+    }
+
+    @Override
+    protected String getHelpText() {
+        return "Provides a hint if a build is likely stuck. Uses Jenkins function Executor#isLikelyStuck.";
+    }
+
+    @Override
+    protected SimpleCollector.Builder<?, Gauge> getCollectorBuilder() {
+        return Gauge.build();
+    }
+
+    @Override
+    public void calculateMetric(Run<?, ?> run, String[] labelValues) {
+        if (run == null || !run.isBuilding()) {
+            return;
+        }
+
+        Executor executor = run.getExecutor();
+        if (executor == null) {
+            return;
+        }
+
+        double likelyStuckDoubleValue = executor.isLikelyStuck() ? 1.0 : 0.0;
+        this.collector.labels(labelValues).set(likelyStuckDoubleValue);
+    }
+}
diff --git a/src/main/java/org/jenkinsci/plugins/prometheus/collectors/jobs/JobCollectorFactory.java b/src/main/java/org/jenkinsci/plugins/prometheus/collectors/jobs/JobCollectorFactory.java
index 0597427..276e354 100644
--- a/src/main/java/org/jenkinsci/plugins/prometheus/collectors/jobs/JobCollectorFactory.java
+++ b/src/main/java/org/jenkinsci/plugins/prometheus/collectors/jobs/JobCollectorFactory.java
@@ -23,6 +23,8 @@ public class JobCollectorFactory extends BaseCollectorFactory {
                 return saveBuildCollector(new BuildDiscardGauge(labelNames, namespace, subsystem));
             case CURRENT_RUN_DURATION_GAUGE:
                 return saveBuildCollector(new CurrentRunDurationGauge(labelNames, namespace, subsystem));
+            case JOB_LOG_UPDATED_GAUGE:
+                return saveBuildCollector(new LogUpdatedGauge(labelNames, namespace, subsystem));
             default:
                 return new NoOpMetricCollector<>();
         }
diff --git a/src/main/java/org/jenkinsci/plugins/prometheus/collectors/jobs/LogUpdatedGauge.java b/src/main/java/org/jenkinsci/plugins/prometheus/collectors/jobs/LogUpdatedGauge.java
new file mode 100644
index 0000000..b9e1acf
--- /dev/null
+++ b/src/main/java/org/jenkinsci/plugins/prometheus/collectors/jobs/LogUpdatedGauge.java
@@ -0,0 +1,38 @@
+package org.jenkinsci.plugins.prometheus.collectors.jobs;
+
+import hudson.model.Job;
+import io.prometheus.client.Gauge;
+import io.prometheus.client.SimpleCollector;
+import org.jenkinsci.plugins.prometheus.collectors.CollectorType;
+import org.jenkinsci.plugins.prometheus.collectors.builds.BuildsMetricCollector;
+
+public class LogUpdatedGauge extends BuildsMetricCollector<Job<?, ?>, Gauge> {
+
+    protected LogUpdatedGauge(String[] labelNames, String namespace, String subsystem) {
+        super(labelNames, namespace, subsystem);
+    }
+
+    @Override
+    protected CollectorType getCollectorType() {
+        return CollectorType.JOB_LOG_UPDATED_GAUGE;
+    }
+
+    @Override
+    protected String getHelpText() {
+        return "Provides a hint if a job is still logging. Uses Jenkins function Job#isLogUpdated";
+    }
+
+    @Override
+    protected SimpleCollector.Builder<?, Gauge> getCollectorBuilder() {
+        return Gauge.build();
+    }
+
+    @Override
+    public void calculateMetric(Job<?, ?> jenkinsObject, String[] labelValues) {
+
+        if (jenkinsObject != null && jenkinsObject.isBuilding()) {
+            boolean logUpdated = jenkinsObject.isLogUpdated();
+            this.collector.labels(labelValues).set(logUpdated ? 1.0 : 0.0);
+        }
+    }
+}


Test patch:
diff --git a/src/test/java/org/jenkinsci/plugins/prometheus/collectors/builds/BuildLikelyStuckGaugeTest.java b/src/test/java/org/jenkinsci/plugins/prometheus/collectors/builds/BuildLikelyStuckGaugeTest.java
new file mode 100644
index 0000000..7deedf9
--- /dev/null
+++ b/src/test/java/org/jenkinsci/plugins/prometheus/collectors/builds/BuildLikelyStuckGaugeTest.java
@@ -0,0 +1,96 @@
+package org.jenkinsci.plugins.prometheus.collectors.builds;
+
+import hudson.model.Executor;
+import io.prometheus.client.Collector;
+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;
+
+import static org.mockito.Mockito.*;
+
+
+public class BuildLikelyStuckGaugeTest extends MockedRunCollectorTest {
+
+
+    @Test
+    public void testNothingCalculatedWhenRunIsNull() {
+        BuildLikelyStuckGauge sut = new BuildLikelyStuckGauge(getLabelNames(), getNamespace(), getSubSystem(), "");
+
+        sut.calculateMetric(null, getLabelValues());
+
+        List<Collector.MetricFamilySamples> collect = sut.collect();
+
+        Assertions.assertEquals(1, collect.size());
+        Assertions.assertEquals(0, collect.get(0).samples.size());
+
+    }
+
+    @Test
+    public void testNothingCalculatedWhenJobIsNotBuilding() {
+        Mockito.when(mock.isBuilding()).thenReturn(false);
+
+        BuildLikelyStuckGauge sut = new BuildLikelyStuckGauge(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());
+
+    }
+
+    @Test
+    public void testNothingCalculatedWhenNoExecutorFound() {
+        Mockito.when(mock.isBuilding()).thenReturn(true);
+        Mockito.when(mock.getExecutor()).thenReturn(null);
+
+        BuildLikelyStuckGauge sut = new BuildLikelyStuckGauge(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());
+    }
+
+    @Test
+    public void testBuildIsLikelyStuck() {
+        Mockito.when(mock.isBuilding()).thenReturn(true);
+        Executor mockedExecutor = mock(Executor.class);
+        when(mockedExecutor.isLikelyStuck()).thenReturn(true);
+        Mockito.when(mock.getExecutor()).thenReturn(mockedExecutor);
+
+        BuildLikelyStuckGauge sut = new BuildLikelyStuckGauge(getLabelNames(), getNamespace(), getSubSystem(), "");
+
+        sut.calculateMetric(mock, getLabelValues());
+
+        List<Collector.MetricFamilySamples> collect = sut.collect();
+
+        Assertions.assertEquals(1, collect.size());
+        Assertions.assertEquals(1, collect.get(0).samples.size());
+        Assertions.assertEquals(1.0, collect.get(0).samples.get(0).value);
+    }
+
+    @Test
+    public void testBuildIsNotLikelyStuck() {
+        Mockito.when(mock.isBuilding()).thenReturn(true);
+        Executor mockedExecutor = mock(Executor.class);
+        when(mockedExecutor.isLikelyStuck()).thenReturn(false);
+        Mockito.when(mock.getExecutor()).thenReturn(mockedExecutor);
+
+        BuildLikelyStuckGauge sut = new BuildLikelyStuckGauge(getLabelNames(), getNamespace(), getSubSystem(), "");
+
+        sut.calculateMetric(mock, getLabelValues());
+
+        List<Collector.MetricFamilySamples> collect = sut.collect();
+
+        Assertions.assertEquals(1, collect.size());
+        Assertions.assertEquals(1, collect.get(0).samples.size());
+        Assertions.assertEquals(0.0, collect.get(0).samples.get(0).value);
+    }
+}
\ No newline at end of file
diff --git a/src/test/java/org/jenkinsci/plugins/prometheus/collectors/jobs/LogUpdatedGaugeTest.java b/src/test/java/org/jenkinsci/plugins/prometheus/collectors/jobs/LogUpdatedGaugeTest.java
new file mode 100644
index 0000000..c55b3b2
--- /dev/null
+++ b/src/test/java/org/jenkinsci/plugins/prometheus/collectors/jobs/LogUpdatedGaugeTest.java
@@ -0,0 +1,72 @@
+package org.jenkinsci.plugins.prometheus.collectors.jobs;
+
+import io.prometheus.client.Collector;
+import org.junit.jupiter.api.Test;
+
+import java.util.List;
+
+import static org.mockito.Mockito.when;
+
+public class LogUpdatedGaugeTest extends JobCollectorTest {
+
+    @Test
+    public void testOnlyCalculatedIfCurrentlyBuilding() {
+        when(job.isBuilding()).thenReturn(false);
+
+        LogUpdatedGauge sut = new LogUpdatedGauge(new String[]{"jenkins_job", "repo"}, "default", "jenkins");
+
+        sut.calculateMetric(job, new String[]{"job1", "NA"});
+        List<Collector.MetricFamilySamples> collect = sut.collect();
+
+        validateMetricFamilySampleListSize(collect, 1);
+
+        Collector.MetricFamilySamples samples = collect.get(0);
+        validateNames(samples, new String[]{"default_jenkins_builds_job_log_updated"});
+        validateMetricFamilySampleSize(samples, 0);
+    }
+
+    @Test
+    public void testBasicAttributes() {
+        when(job.isLogUpdated()).thenReturn(true);
+        when(job.isBuilding()).thenReturn(true);
+
+        LogUpdatedGauge sut = new LogUpdatedGauge(new String[]{"jenkins_job", "repo"}, "default", "jenkins");
+
+        sut.calculateMetric(job, new String[]{"job1", "NA"});
+        List<Collector.MetricFamilySamples> collect = sut.collect();
+
+        validateMetricFamilySampleListSize(collect, 1);
+
+        Collector.MetricFamilySamples samples = collect.get(0);
+        validateNames(samples, new String[]{"default_jenkins_builds_job_log_updated"});
+        validateMetricFamilySampleSize(samples, 1);
+    }
+
+    @Test
+    public void testLogIsUpdatedReturnsOne() {
+
+        when(job.isLogUpdated()).thenReturn(true);
+        when(job.isBuilding()).thenReturn(true);
+
+        LogUpdatedGauge sut = new LogUpdatedGauge(new String[]{"jenkins_job", "repo"}, "default", "jenkins");
+
+        sut.calculateMetric(job, new String[]{"job1", "NA"});
+        List<Collector.MetricFamilySamples> collect = sut.collect();
+        Collector.MetricFamilySamples samples = collect.get(0);
+        validateValue(samples.samples.get(0), 1.0);
+    }
+
+    @Test
+    public void testLogIsNotUpdatedReturnsZero() {
+
+        when(job.isLogUpdated()).thenReturn(false);
+        when(job.isBuilding()).thenReturn(true);
+
+        LogUpdatedGauge sut = new LogUpdatedGauge(new String[]{"jenkins_job", "repo"}, "default", "jenkins");
+
+        sut.calculateMetric(job, new String[]{"job1", "NA"});
+        List<Collector.MetricFamilySamples> collect = sut.collect();
+        Collector.MetricFamilySamples samples = collect.get(0);
+        validateValue(samples.samples.get(0), 0.0);
+    }
+}
\ No newline at end of file