Repository: jenkinsci/prometheus-plugin (java)

Issue:
NullPointerException in disk usage data metric
### Jenkins and plugins versions report

<details>
  <summary>Environment</summary>

  <!-- Paste your environment details below -->
  ```text
Jenkins: 2.452.1
OS: Linux - 5.14.0-362.24.1.el9_3.x86_64
Java: 17.0.11 - Eclipse Adoptium (OpenJDK 64-Bit Server VM)
---
Parameterized-Remote-Trigger:3.2.0
ansicolor:1.0.4
ant:497.v94e7d9fffa_b_9
antisamy-markup-formatter:162.v0e6ec0fcfcf6
apache-httpcomponents-client-4-api:4.5.14-208.v438351942757
apache-httpcomponents-client-5-api:5.3.1-1.0
artifactory:4.0.6
asm-api:9.7-33.v4d23ef79fcc8
authentication-tokens:1.113.v81215a_241826
authorize-project:1.7.1
blueocean:1.27.12
blueocean-bitbucket-pipeline:1.27.12
blueocean-commons:1.27.12
blueocean-config:1.27.12
blueocean-core-js:1.27.12
blueocean-dashboard:1.27.12
blueocean-display-url:2.4.2
blueocean-events:1.27.12
blueocean-git-pipeline:1.27.12
blueocean-github-pipeline:1.27.12
blueocean-i18n:1.27.12
blueocean-jira:1.27.12
blueocean-jwt:1.27.12
blueocean-personalization:1.27.12
blueocean-pipeline-api-impl:1.27.12
blueocean-pipeline-editor:1.27.12
blueocean-pipeline-scm-api:1.27.12
blueocean-rest:1.27.12
blueocean-rest-impl:1.27.12
blueocean-web:1.27.12
bootstrap5-api:5.3.3-1
bouncycastle-api:2.30.1.78.1-233.vfdcdeb_0a_08a_a_
branch-api:2.1169.va_f810c56e895
build-monitor-plugin:1.14-883.vf620a_44eb_ec1
build-timeout:1.32
caffeine-api:3.1.8-133.v17b_1ff2e0599
checks-api:2.2.0
claim:554.va_f9b_58b_0a_088
cloud-stats:336.v788e4055508b_
cloudbees-bitbucket-branch-source:886.v44cf5e4ecec5
cloudbees-disk-usage-simple:203.v3f46a_7462b_1a_
cloudbees-folder:6.928.v7c780211d66e
command-launcher:107.v773860566e2e
commons-lang3-api:3.13.0-62.v7d18e55f51e2
commons-text-api:1.11.0-109.vfe16c66636eb_
compact-columns:1.185.vf3851b_4d31fe
config-file-provider:973.vb_a_80ecb_9a_4d0
configuration-as-code:1810.v9b_c30a_249a_4c
configuration-as-code-groovy:1.1
console-column-plugin:252.v0b_8fa_0e33b_72
content-replace:1.8.2
credentials:1337.v60b_d7b_c7b_c9f
credentials-binding:677.vdc9d38cb_254d
cucumber-reports:5.8.1
dark-theme:439.vdef09f81f85e
data-tables-api:2.0.7-1
dependency-track:5.0.0
display-url-api:2.204.vf6fddd8a_8b_e9
docker-commons:439.va_3cb_0a_6a_fb_29
docker-java-api:3.3.6-90.ve7c5c7535ddd
docker-plugin:1.6.1
docker-workflow:580.vc0c340686b_54
downstream-buildview:69.v16da_b_2c36f6c
durable-task:555.v6802fe0f0b_82
echarts-api:5.5.0-1
eddsa-api:0.3.0-4.v84c6f0f4969e
email-ext:1814.v404722f34263
extended-read-permission:53.v6499940139e5
external-monitor-job:215.v2e88e894db_f8
extra-columns:1.26
favorite:2.208.v91d65b_7792a_c
file-leak-detector:1.13
flyway-api:9.22.3-75.vfdfb_f75a_a_9b_e
flyway-runner:122.v004b_965a_cd08
font-awesome-api:6.5.2-1
gatling:1.3.0
git:5.2.2
git-client:4.7.0
git-parameter:0.9.19
git-server:126.v0d945d8d2b_39
github:1.39.0
github-api:1.318-461.v7a_c09c9fa_d63
github-branch-source:1789.v5b_0c0cea_18c3
gitlab-plugin:1.8.1
global-build-stats:293.vd7b_d6e361475
gradle:2.12
groovy-events-listener-plugin:2.210.v8a_4107f66127
gson-api:2.11.0-41.v019fcf6125dc
handy-uri-templates-2-api:2.1.8-30.v7e777411b_148
hashicorp-vault-plugin:368.v48134f694db_f
htmlpublisher:1.34
instance-identity:185.v303dc7c645f9
ionicons-api:74.v93d5eb_813d5f
jackson2-api:2.17.0-379.v02de8ec9f64c
jacoco:3.3.6
jakarta-activation-api:2.1.3-1
jakarta-mail-api:2.1.3-1
javadoc:243.vb_b_503b_b_45537
javax-activation-api:1.2.0-7
javax-mail-api:1.6.2-10
jaxb:2.3.9-1
jdk-tool:73.vddf737284550
jenkins-design-language:1.27.12
jersey2-api:2.42-147.va_28a_44603b_d5
jira:3.13
jjwt-api:0.11.5-112.ve82dfb_224b_a_d
job-dsl:1.87
joda-time-api:2.12.7-29.v5a_b_e3a_82269a_
jquery3-api:3.7.1-2
jsch:0.2.16-86.v42e010d9484b_
json-api:20240303-41.v94e11e6de726
json-path-api:2.9.0-58.v62e3e85b_a_655
junit:1265.v65b_14fa_f12f0
kubernetes:4238.v41b_3ef14a_5d8
kubernetes-client-api:6.10.0-240.v57880ce8b_0b_2
kubernetes-credentials:174.va_36e093562d9
kubernetes-pipeline-devops-steps:1.6
ldap:725.v3cb_b_711b_1a_ef
lockable-resources:1255.vf48745da_35d0
mailer:472.vf7c289a_4b_420
mapdb-api:1.0.9-40.v58107308b_7a_7
matrix-auth:3.2.2
matrix-project:831.v084e85a_b_4ea_d
maven-plugin:3.23
metrics:4.2.21-451.vd51df8df52ec
mina-sshd-api-common:2.12.1-101.v85b_e08b_780dd
mina-sshd-api-core:2.12.1-101.v85b_e08b_780dd
monitoring:1.99.0
naginator:1.449.ve19751d70eb_0
nested-view:1.33
next-executions:327.v136ff959e97b_
notification:1.17
oic-auth:4.269.va_7526f34f306
okhttp-api:4.11.0-172.vda_da_1feeb_c6e
pam-auth:1.11
pipeline-build-step:540.vb_e8849e1a_b_d8
pipeline-github-lib:61.v629f2cc41d83
pipeline-graph-analysis:216.vfd8b_ece330ca_
pipeline-groovy-lib:710.v4b_94b_077a_808
pipeline-input-step:495.ve9c153f6067b_
pipeline-milestone-step:119.vdfdc43fc3b_9a_
pipeline-model-api:2.2198.v41dd8ef6dd56
pipeline-model-definition:2.2198.v41dd8ef6dd56
pipeline-model-extensions:2.2198.v41dd8ef6dd56
pipeline-rest-api:2.34
pipeline-stage-step:312.v8cd10304c27a_
pipeline-stage-tags-metadata:2.2198.v41dd8ef6dd56
pipeline-stage-view:2.34
pipeline-utility-steps:2.16.2
plain-credentials:182.v468b_97b_9dcb_8
plugin-usage-plugin:4.4
plugin-util-api:4.1.0
prism-api:1.29.0-15
progress-bar-column-plugin:11.vdef198c2d6c1
prometheus:770.ve5e758db_606e
pubsub-light:1.18
role-strategy:727.vd344b_eec783d
run-condition:1.7
scm-api:690.vfc8b_54395023
script-security:1341.va_2819b_414686
slack:715.v1cfed1b_9c63c
snakeyaml-api:2.2-111.vc6598e30cc65
sonar:2.17.2
sse-gateway:1.26
ssh-agent:367.vf9076cd4ee21
ssh-credentials:337.v395d2403ccd4
ssh-slaves:2.968.v6f8823c91de4
sshd:3.322.v159e91f6a_550
structs:337.v1b_04ea_4df7c8
support-core:1449.v261d6f7b_f9f3
theme-manager:215.vc1ff18d67920
thinBackup:2.0
timestamper:1.27
token-macro:400.v35420b_922dcb_
trilead-api:2.147.vb_73cc728a_32e
uno-choice:2.8.3
variant:60.v7290fc0eb_b_cd
view-job-filters:377.v66f4b_796e5fa_
workflow-aggregator:596.v8c21c963d92d
workflow-api:1316.v33eb_726c50b_a_
workflow-basic-steps:1058.vcb_fc1e3a_21a_9
workflow-cps:3894.3896.vca_2c931e7935
workflow-durable-task-step:1353.v1891a_b_01da_18
workflow-job:1400.v7fd111b_ec82f
workflow-multibranch:783.va_6eb_ef636fb_d
workflow-scm-step:427.v4ca_6512e7df1
workflow-step-api:657.v03b_e8115821b_
workflow-support:907.v6713a_ed8a_573

  ```

</details>


### What Operating System are you using (both controller, and any agents involved in the problem)?

Docker image: jenkins/jenkins:2.452.1

### Reproduction steps

Every time the metrics are fetched by kuberntes, teh exception is thrown.

### Expected Results

The Metric should not fail

### Actual Results

```
2024-06-05 06:04:57.163+0000 [id=5899]	WARNING	o.j.p.p.DiskUsageCollector#collect: Failed to get disk usage data due to an unexpected error.
java.lang.NullPointerException: Cannot invoke "java.lang.Long.longValue()" because the return value of "com.cloudbees.simplediskusage.DiskItem.getCount()" is null
	at org.jenkinsci.plugins.prometheus.collectors.disk.DiskUsageFileCountGauge.calculateMetric(DiskUsageFileCountGauge.java:35)
	at org.jenkinsci.plugins.prometheus.collectors.disk.DiskUsageFileCountGauge.calculateMetric(DiskUsageFileCountGauge.java:9)
	at org.jenkinsci.plugins.prometheus.DiskUsageCollector.lambda$collectDiskUsage$0(DiskUsageCollector.java:64)
	at java.base/java.util.ArrayList.forEach(Unknown Source)
	at org.jenkinsci.plugins.prometheus.DiskUsageCollector.lambda$collectDiskUsage$1(DiskUsageCollector.java:64)
	at java.base/java.util.concurrent.CopyOnWriteArrayList.forEach(Unknown Source)
	at org.jenkinsci.plugins.prometheus.DiskUsageCollector.collectDiskUsage(DiskUsageCollector.java:61)
	at org.jenkinsci.plugins.prometheus.DiskUsageCollector.collect(DiskUsageCollector.java:36)
	at io.prometheus.client.Collector.collect(Collector.java:45)
	at io.prometheus.client.CollectorRegistry$MetricFamilySamplesEnumeration.findNextElement(CollectorRegistry.java:204)
	at io.prometheus.client.CollectorRegistry$MetricFamilySamplesEnumeration.nextElement(CollectorRegistry.java:219)
	at io.prometheus.client.CollectorRegistry$MetricFamilySamplesEnumeration.nextElement(CollectorRegistry.java:152)
	at java.base/java.util.Enumeration$1.next(Unknown Source)
	at org.jenkinsci.plugins.prometheus.config.disabledmetrics.FilteredMetricEnumeration.filterList(FilteredMetricEnumeration.java:21)
	at org.jenkinsci.plugins.prometheus.config.disabledmetrics.FilteredMetricEnumeration.<init>(FilteredMetricEnumeration.java:15)
	at org.jenkinsci.plugins.prometheus.service.DefaultPrometheusMetrics.collectMetrics(DefaultPrometheusMetrics.java:53)
	at org.jenkinsci.plugins.prometheus.service.PrometheusAsyncWorker.execute(PrometheusAsyncWorker.java:41)
	at hudson.model.AsyncPeriodicWork.lambda$doRun$0(AsyncPeriodicWork.java:102)
	at java.base/java.lang.Thread.run(Unknown Source)
```

### Anything else?

_No response_

### Are you interested in contributing a fix?

_No response_

Pull request:
Fixing NullPointerException when calculating disk usage
Fixes #672


### Checklist

- [X] Includes tests covering the new functionality?
- [X] Ready for review
- [X] Follows CONTRIBUTING rules




Patch:
diff --git a/src/main/java/org/jenkinsci/plugins/prometheus/collectors/disk/DiskUsageBytesGauge.java b/src/main/java/org/jenkinsci/plugins/prometheus/collectors/disk/DiskUsageBytesGauge.java
index 9416686..81a743b 100644
--- a/src/main/java/org/jenkinsci/plugins/prometheus/collectors/disk/DiskUsageBytesGauge.java
+++ b/src/main/java/org/jenkinsci/plugins/prometheus/collectors/disk/DiskUsageBytesGauge.java
@@ -32,6 +32,12 @@ public class DiskUsageBytesGauge extends BaseMetricCollector<DiskItem, Gauge> {
         if (jenkinsObject == null) {
             return;
         }
-        this.collector.labels(labelValues).set(jenkinsObject.getUsage() * 1024);
+
+        Long usage = jenkinsObject.getUsage();
+        if (usage == null) {
+            return;
+        }
+
+        this.collector.labels(labelValues).set(usage * 1024);
     }
 }
diff --git a/src/main/java/org/jenkinsci/plugins/prometheus/collectors/disk/DiskUsageFileCountGauge.java b/src/main/java/org/jenkinsci/plugins/prometheus/collectors/disk/DiskUsageFileCountGauge.java
index 35d66c8..db2688a 100644
--- a/src/main/java/org/jenkinsci/plugins/prometheus/collectors/disk/DiskUsageFileCountGauge.java
+++ b/src/main/java/org/jenkinsci/plugins/prometheus/collectors/disk/DiskUsageFileCountGauge.java
@@ -32,6 +32,10 @@ public class DiskUsageFileCountGauge extends BaseMetricCollector<DiskItem, Gauge
         if (jenkinsObject == null) {
             return;
         }
-        this.collector.labels(labelValues).set(jenkinsObject.getCount());
+        Long count = jenkinsObject.getCount();
+        if (count == null) {
+            return;
+        }
+        this.collector.labels(labelValues).set(count);
     }
 }
diff --git a/src/main/java/org/jenkinsci/plugins/prometheus/collectors/disk/JobUsageBytesGauge.java b/src/main/java/org/jenkinsci/plugins/prometheus/collectors/disk/JobUsageBytesGauge.java
index d7619d5..a9188d9 100644
--- a/src/main/java/org/jenkinsci/plugins/prometheus/collectors/disk/JobUsageBytesGauge.java
+++ b/src/main/java/org/jenkinsci/plugins/prometheus/collectors/disk/JobUsageBytesGauge.java
@@ -32,6 +32,11 @@ public class JobUsageBytesGauge extends BaseMetricCollector<JobDiskItem, Gauge>
         if (jenkinsObject == null) {
             return;
         }
-        this.collector.labels(labelValues).set(jenkinsObject.getUsage() * 1024);
+        Long usage = jenkinsObject.getUsage();
+        if (usage == null) {
+            return;
+        }
+
+        this.collector.labels(labelValues).set(usage * 1024);
     }
 }


Test patch:
diff --git a/src/test/java/org/jenkinsci/plugins/prometheus/collectors/disk/DiskUsageBytesGaugeTest.java b/src/test/java/org/jenkinsci/plugins/prometheus/collectors/disk/DiskUsageBytesGaugeTest.java
new file mode 100644
index 0000000..b236243
--- /dev/null
+++ b/src/test/java/org/jenkinsci/plugins/prometheus/collectors/disk/DiskUsageBytesGaugeTest.java
@@ -0,0 +1,64 @@
+package org.jenkinsci.plugins.prometheus.collectors.disk;
+
+import com.cloudbees.simplediskusage.DiskItem;
+import io.prometheus.client.Collector;
+import org.jenkinsci.plugins.prometheus.collectors.testutils.CollectorTest;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+class DiskUsageBytesGaugeTest extends CollectorTest {
+
+    @Mock
+    DiskItem mock;
+
+    @Test
+    public void testCollectResult() {
+
+        when(mock.getUsage()).thenReturn(10L);
+
+        DiskUsageBytesGauge sut = new DiskUsageBytesGauge(getLabelNames(), getNamespace(), getSubSystem());
+        sut.calculateMetric(mock, getLabelValues());
+
+        List<Collector.MetricFamilySamples> collect = sut.collect();
+
+        validateMetricFamilySampleListSize(collect, 1);
+
+        Collector.MetricFamilySamples samples = collect.get(0);
+
+        validateNames(samples, new String[]{"default_jenkins_disk_usage_bytes"});
+        validateMetricFamilySampleSize(samples, 1);
+        validateHelp(samples, "Disk usage of first level folder in JENKINS_HOME in bytes");
+        validateValue(samples, 0, 10240.0);
+    }
+
+    @Test
+    public void testDiskItemIsNull() {
+        DiskUsageBytesGauge sut = new DiskUsageBytesGauge(getLabelNames(), getNamespace(), getSubSystem());
+        sut.calculateMetric(null, getLabelValues());
+
+        List<Collector.MetricFamilySamples> collect = sut.collect();
+
+        validateMetricFamilySampleListSize(collect, 1);
+        validateMetricFamilySampleSize(collect.get(0), 0);
+    }
+
+    @Test
+    public void testDiskItemUsageIsNull() {
+        when(mock.getUsage()).thenReturn(null);
+        DiskUsageBytesGauge sut = new DiskUsageBytesGauge(getLabelNames(), getNamespace(), getSubSystem());
+        sut.calculateMetric(mock, getLabelValues());
+
+        List<Collector.MetricFamilySamples> collect = sut.collect();
+
+        validateMetricFamilySampleListSize(collect, 1);
+        validateMetricFamilySampleSize(collect.get(0), 0);
+    }
+}
\ No newline at end of file
diff --git a/src/test/java/org/jenkinsci/plugins/prometheus/collectors/disk/DiskUsageFileCountGaugeTest.java b/src/test/java/org/jenkinsci/plugins/prometheus/collectors/disk/DiskUsageFileCountGaugeTest.java
index d395c5f..4c055d6 100644
--- a/src/test/java/org/jenkinsci/plugins/prometheus/collectors/disk/DiskUsageFileCountGaugeTest.java
+++ b/src/test/java/org/jenkinsci/plugins/prometheus/collectors/disk/DiskUsageFileCountGaugeTest.java
@@ -48,4 +48,16 @@ class DiskUsageFileCountGaugeTest extends CollectorTest {
         validateMetricFamilySampleListSize(collect, 1);
         validateMetricFamilySampleSize(collect.get(0), 0);
     }
+
+    @Test
+    public void testDiskItemCountIsNull() {
+        when(mock.getCount()).thenReturn(null);
+        DiskUsageFileCountGauge sut = new DiskUsageFileCountGauge(getLabelNames(), getNamespace(), getSubSystem());
+        sut.calculateMetric(mock, getLabelValues());
+
+        List<Collector.MetricFamilySamples> collect = sut.collect();
+
+        validateMetricFamilySampleListSize(collect, 1);
+        validateMetricFamilySampleSize(collect.get(0), 0);
+    }
 }
\ No newline at end of file
diff --git a/src/test/java/org/jenkinsci/plugins/prometheus/collectors/disk/JobUsageBytesGaugeTest.java b/src/test/java/org/jenkinsci/plugins/prometheus/collectors/disk/JobUsageBytesGaugeTest.java
new file mode 100644
index 0000000..08b994e
--- /dev/null
+++ b/src/test/java/org/jenkinsci/plugins/prometheus/collectors/disk/JobUsageBytesGaugeTest.java
@@ -0,0 +1,66 @@
+package org.jenkinsci.plugins.prometheus.collectors.disk;
+
+import com.cloudbees.simplediskusage.DiskItem;
+import com.cloudbees.simplediskusage.JobDiskItem;
+import io.prometheus.client.Collector;
+import org.jenkinsci.plugins.prometheus.collectors.testutils.CollectorTest;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import java.util.List;
+
+import static org.mockito.Mockito.when;
+
+
+
+@ExtendWith(MockitoExtension.class)
+class JobUsageBytesGaugeTest extends CollectorTest {
+
+    @Mock
+    JobDiskItem mock;
+
+    @Test
+    public void testCollectResult() {
+
+        when(mock.getUsage()).thenReturn(10L);
+
+        JobUsageBytesGauge sut = new JobUsageBytesGauge(getLabelNames(), getNamespace(), getSubSystem());
+        sut.calculateMetric(mock, getLabelValues());
+
+        List<Collector.MetricFamilySamples> collect = sut.collect();
+
+        validateMetricFamilySampleListSize(collect, 1);
+
+        Collector.MetricFamilySamples samples = collect.get(0);
+
+        validateNames(samples, new String[]{"default_jenkins_job_usage_bytes"});
+        validateMetricFamilySampleSize(samples, 1);
+        validateHelp(samples, "Amount of disk usage (bytes) for each job in Jenkins");
+        validateValue(samples, 0, 10240.0);
+    }
+
+    @Test
+    public void testJobDiskItemIsNull() {
+        JobUsageBytesGauge sut = new JobUsageBytesGauge(getLabelNames(), getNamespace(), getSubSystem());
+        sut.calculateMetric(null, getLabelValues());
+
+        List<Collector.MetricFamilySamples> collect = sut.collect();
+
+        validateMetricFamilySampleListSize(collect, 1);
+        validateMetricFamilySampleSize(collect.get(0), 0);
+    }
+
+    @Test
+    public void testJobDiskItemUsageIsNull() {
+        when(mock.getUsage()).thenReturn(null);
+        JobUsageBytesGauge sut = new JobUsageBytesGauge(getLabelNames(), getNamespace(), getSubSystem());
+        sut.calculateMetric(mock, getLabelValues());
+
+        List<Collector.MetricFamilySamples> collect = sut.collect();
+
+        validateMetricFamilySampleListSize(collect, 1);
+        validateMetricFamilySampleSize(collect.get(0), 0);
+    }
+}
\ No newline at end of file