From 0a99a36826a344fccb874487060044707cfd6ae3 Mon Sep 17 00:00:00 2001 From: Jason Pickering Date: Wed, 22 Jan 2025 08:02:52 +0100 Subject: [PATCH 01/26] Add Prometheus export of data summary stats --- .../controller/DataSummaryControllerTest.java | 84 ++++++++++++++ dhis-2/dhis-web-api/pom.xml | 5 + .../datasummary/DataSummaryController.java | 108 +++++++++++++++++- 3 files changed, 196 insertions(+), 1 deletion(-) create mode 100644 dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/DataSummaryControllerTest.java diff --git a/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/DataSummaryControllerTest.java b/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/DataSummaryControllerTest.java new file mode 100644 index 000000000000..068a8161c018 --- /dev/null +++ b/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/DataSummaryControllerTest.java @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2004-2025, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.webapi.controller; + +import static org.junit.jupiter.api.Assertions.*; + +import org.hisp.dhis.http.HttpClientAdapter; +import org.hisp.dhis.http.HttpStatus; +import org.hisp.dhis.test.webapi.PostgresControllerIntegrationTestBase; +import org.junit.jupiter.api.Test; + +class DataSummaryControllerTest extends PostgresControllerIntegrationTestBase { + + @Test + void canGetPrometheusMetrics() { + // Send the GET request and check the status + HttpResponse response = GET("/api/dataSummary/metrics", HttpClientAdapter.Accept("text/plain")); + assertEquals(HttpStatus.OK, response.status()); + + // Extract the response content + String content = response.content("text/plain"); + assertFalse(content.isEmpty(), "Response content should not be empty"); + + // Verify the presence of system information metrics + assertTrue( + content.contains("# HELP data_summary_system_info DHIS2 System information"), + "System information help text is missing"); + assertTrue( + content.contains("data_summary_system_info{key=\"build_time\""), + "Build time metric is missing"); + assertTrue( + content.contains("data_summary_system_info{key=\"version\""), "Version metric is missing"); + + // Verify active users metrics + assertTrue( + content.contains("# HELP data_summary_active_users Active users over days"), + "Active users help text is missing"); + assertTrue(content.contains("data_summary_active_user"), "Active users metric is missing"); + + // Verify object counts metrics + assertTrue( + content.contains("# HELP data_summary_object_counts Count of objects by type"), + "Object counts help text is missing"); + assertTrue(content.contains("data_summary_object_counts"), "Object counts metric is missing"); + + // Verify data value count metrics + assertTrue( + content.contains("# HELP data_summary_data_value_count Data value counts over time"), + "Data value count help text is missing"); + assertTrue( + content.contains("data_summary_data_value_count"), "Data value count metric is missing"); + + // Verify event count metrics + assertTrue( + content.contains("# HELP data_summary_event_count Event counts over time"), + "Event count help text is missing"); + assertTrue(content.contains("data_summary_event_count"), "Event count metric is missing"); + } +} diff --git a/dhis-2/dhis-web-api/pom.xml b/dhis-2/dhis-web-api/pom.xml index 508037ba2f08..0aaf880d3dd2 100644 --- a/dhis-2/dhis-web-api/pom.xml +++ b/dhis-2/dhis-web-api/pom.xml @@ -185,6 +185,11 @@ io.micrometer micrometer-core + + io.prometheus + prometheus-metrics-core + 1.3.5 + org.apache.commons commons-lang3 diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/datasummary/DataSummaryController.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/datasummary/DataSummaryController.java index c5d115d79fe2..b56eb0f7aa25 100644 --- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/datasummary/DataSummaryController.java +++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/datasummary/DataSummaryController.java @@ -29,7 +29,14 @@ import static org.hisp.dhis.security.Authorities.F_PERFORM_MAINTENANCE; import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; +import static org.springframework.http.MediaType.TEXT_PLAIN_VALUE; +import io.prometheus.client.CollectorRegistry; +import io.prometheus.client.Gauge; +import io.prometheus.client.exporter.common.TextFormat; +import java.io.IOException; +import java.io.StringWriter; +import java.io.Writer; import org.hisp.dhis.common.DhisApiVersion; import org.hisp.dhis.common.OpenApi; import org.hisp.dhis.datastatistics.DataStatisticsService; @@ -55,11 +62,110 @@ @ApiVersion({DhisApiVersion.DEFAULT, DhisApiVersion.ALL}) public class DataSummaryController { - @Autowired private DataStatisticsService dataStatisticsService; + private static final Gauge objectCountsGauge = + Gauge.build() + .name("data_summary_object_counts") + .help("Count of objects by type") + .labelNames("type") + .register(); + + private static final Gauge activeUsersGauge = + Gauge.build() + .name("data_summary_active_users") + .help("Active users over days") + .labelNames("days") + .register(); + + private static final Gauge userInvitationsGauge = + Gauge.build() + .name("data_summary_user_invitations") + .help("Count of user invitations") + .labelNames("type") + .register(); + + private static final Gauge dataValueCountGauge = + Gauge.build() + .name("data_summary_data_value_count") + .help("Data value counts over time") + .labelNames("time") + .register(); + + private static final Gauge eventCountGauge = + Gauge.build() + .name("data_summary_event_count") + .help("Event counts over time") + .labelNames("time") + .register(); + + private static final Gauge systemInfoGauge = + Gauge.build() + .name("data_summary_system_info") + .help("DHIS2 System information") + .labelNames("key", "value") + .register(); + + @Autowired public DataStatisticsService dataStatisticsService; @GetMapping(produces = APPLICATION_JSON_VALUE) @RequiresAuthority(anyOf = F_PERFORM_MAINTENANCE) public @ResponseBody DataSummary getStatistics() { return dataStatisticsService.getSystemStatisticsSummary(); } + + @GetMapping(value = "/metrics", produces = TEXT_PLAIN_VALUE) + @RequiresAuthority(anyOf = F_PERFORM_MAINTENANCE) + public @ResponseBody String getPrometheusMetrics() throws IOException { + DataSummary summary = dataStatisticsService.getSystemStatisticsSummary(); + + // Update object counts + summary.getObjectCounts().forEach((type, count) -> objectCountsGauge.labels(type).set(count)); + + // Update active users + summary + .getActiveUsers() + .forEach((days, count) -> activeUsersGauge.labels(days.toString()).set(count)); + + // Update user invitations + summary + .getUserInvitations() + .forEach((type, count) -> userInvitationsGauge.labels(type).set(count)); + + // Update data value count + summary + .getDataValueCount() + .forEach((time, count) -> dataValueCountGauge.labels(time.toString()).set(count)); + + // Update event count + summary + .getEventCount() + .forEach((time, count) -> eventCountGauge.labels(time.toString()).set(count)); + + // Update system info as static gauges + if (summary.getSystem() != null) { + if (summary.getSystem().getVersion() != null) { + systemInfoGauge.labels("version", summary.getSystem().getVersion()).set(1); + } + if (summary.getSystem().getRevision() != null) { + systemInfoGauge.labels("revision", summary.getSystem().getRevision()).set(1); + } + if (summary.getSystem().getBuildTime() != null) { + systemInfoGauge + .labels("build_time", String.valueOf(summary.getSystem().getBuildTime())) + .set(1); + } + if (summary.getSystem().getSystemId() != null) { + systemInfoGauge.labels("system_id", summary.getSystem().getSystemId()).set(1); + } + if (summary.getSystem().getServerDate() != null) { + systemInfoGauge + .labels("server_date", String.valueOf(summary.getSystem().getServerDate())) + .set(1); + } + } + + // Generate Prometheus metrics as plain text + Writer writer = new StringWriter(); + TextFormat.write004(writer, CollectorRegistry.defaultRegistry.metricFamilySamples()); + return writer.toString(); + } } From 07d99bc0384b91093d51deb12f72e0e3a48d2369 Mon Sep 17 00:00:00 2001 From: Jason Pickering Date: Wed, 22 Jan 2025 08:26:56 +0100 Subject: [PATCH 02/26] Remove dependency --- dhis-2/dhis-web-api/pom.xml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/dhis-2/dhis-web-api/pom.xml b/dhis-2/dhis-web-api/pom.xml index 0aaf880d3dd2..508037ba2f08 100644 --- a/dhis-2/dhis-web-api/pom.xml +++ b/dhis-2/dhis-web-api/pom.xml @@ -185,11 +185,6 @@ io.micrometer micrometer-core - - io.prometheus - prometheus-metrics-core - 1.3.5 - org.apache.commons commons-lang3 From 6df66a9f737652fd91e7952b7522f70556f0d777 Mon Sep 17 00:00:00 2001 From: Jason Pickering Date: Wed, 22 Jan 2025 09:55:23 +0100 Subject: [PATCH 03/26] Minor refactor --- .../datasummary/DataSummaryController.java | 92 +------------- .../DataSummaryPrometheusMetrics.java | 118 ++++++++++++++++++ 2 files changed, 119 insertions(+), 91 deletions(-) create mode 100644 dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/datasummary/DataSummaryPrometheusMetrics.java diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/datasummary/DataSummaryController.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/datasummary/DataSummaryController.java index b56eb0f7aa25..f533a23e838f 100644 --- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/datasummary/DataSummaryController.java +++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/datasummary/DataSummaryController.java @@ -32,7 +32,6 @@ import static org.springframework.http.MediaType.TEXT_PLAIN_VALUE; import io.prometheus.client.CollectorRegistry; -import io.prometheus.client.Gauge; import io.prometheus.client.exporter.common.TextFormat; import java.io.IOException; import java.io.StringWriter; @@ -62,48 +61,6 @@ @ApiVersion({DhisApiVersion.DEFAULT, DhisApiVersion.ALL}) public class DataSummaryController { - private static final Gauge objectCountsGauge = - Gauge.build() - .name("data_summary_object_counts") - .help("Count of objects by type") - .labelNames("type") - .register(); - - private static final Gauge activeUsersGauge = - Gauge.build() - .name("data_summary_active_users") - .help("Active users over days") - .labelNames("days") - .register(); - - private static final Gauge userInvitationsGauge = - Gauge.build() - .name("data_summary_user_invitations") - .help("Count of user invitations") - .labelNames("type") - .register(); - - private static final Gauge dataValueCountGauge = - Gauge.build() - .name("data_summary_data_value_count") - .help("Data value counts over time") - .labelNames("time") - .register(); - - private static final Gauge eventCountGauge = - Gauge.build() - .name("data_summary_event_count") - .help("Event counts over time") - .labelNames("time") - .register(); - - private static final Gauge systemInfoGauge = - Gauge.build() - .name("data_summary_system_info") - .help("DHIS2 System information") - .labelNames("key", "value") - .register(); - @Autowired public DataStatisticsService dataStatisticsService; @GetMapping(produces = APPLICATION_JSON_VALUE) @@ -116,54 +73,7 @@ public class DataSummaryController { @RequiresAuthority(anyOf = F_PERFORM_MAINTENANCE) public @ResponseBody String getPrometheusMetrics() throws IOException { DataSummary summary = dataStatisticsService.getSystemStatisticsSummary(); - - // Update object counts - summary.getObjectCounts().forEach((type, count) -> objectCountsGauge.labels(type).set(count)); - - // Update active users - summary - .getActiveUsers() - .forEach((days, count) -> activeUsersGauge.labels(days.toString()).set(count)); - - // Update user invitations - summary - .getUserInvitations() - .forEach((type, count) -> userInvitationsGauge.labels(type).set(count)); - - // Update data value count - summary - .getDataValueCount() - .forEach((time, count) -> dataValueCountGauge.labels(time.toString()).set(count)); - - // Update event count - summary - .getEventCount() - .forEach((time, count) -> eventCountGauge.labels(time.toString()).set(count)); - - // Update system info as static gauges - if (summary.getSystem() != null) { - if (summary.getSystem().getVersion() != null) { - systemInfoGauge.labels("version", summary.getSystem().getVersion()).set(1); - } - if (summary.getSystem().getRevision() != null) { - systemInfoGauge.labels("revision", summary.getSystem().getRevision()).set(1); - } - if (summary.getSystem().getBuildTime() != null) { - systemInfoGauge - .labels("build_time", String.valueOf(summary.getSystem().getBuildTime())) - .set(1); - } - if (summary.getSystem().getSystemId() != null) { - systemInfoGauge.labels("system_id", summary.getSystem().getSystemId()).set(1); - } - if (summary.getSystem().getServerDate() != null) { - systemInfoGauge - .labels("server_date", String.valueOf(summary.getSystem().getServerDate())) - .set(1); - } - } - - // Generate Prometheus metrics as plain text + DataSummaryPrometheusMetrics.updateMetrics(summary); Writer writer = new StringWriter(); TextFormat.write004(writer, CollectorRegistry.defaultRegistry.metricFamilySamples()); return writer.toString(); diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/datasummary/DataSummaryPrometheusMetrics.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/datasummary/DataSummaryPrometheusMetrics.java new file mode 100644 index 000000000000..fba4574d8b7b --- /dev/null +++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/datasummary/DataSummaryPrometheusMetrics.java @@ -0,0 +1,118 @@ +/* + * Copyright (c) 2004-2022, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.webapi.controller.datasummary; + +import io.prometheus.client.Gauge; +import org.hisp.dhis.datasummary.DataSummary; + +public class DataSummaryPrometheusMetrics { + public static final Gauge objectCountsGauge = + Gauge.build() + .name("data_summary_object_counts") + .help("Count of objects by type") + .labelNames("type") + .register(); + public static final Gauge activeUsersGauge = + Gauge.build() + .name("data_summary_active_users") + .help("Active users over days") + .labelNames("days") + .register(); + public static final Gauge userInvitationsGauge = + Gauge.build() + .name("data_summary_user_invitations") + .help("Count of user invitations") + .labelNames("type") + .register(); + public static final Gauge dataValueCountGauge = + Gauge.build() + .name("data_summary_data_value_count") + .help("Data value counts over time") + .labelNames("time") + .register(); + public static final Gauge eventCountGauge = + Gauge.build() + .name("data_summary_event_count") + .help("Event counts over time") + .labelNames("time") + .register(); + public static final Gauge systemInfoGauge = + Gauge.build() + .name("data_summary_system_info") + .help("DHIS2 System information") + .labelNames("key", "value") + .register(); + + public static void updateMetrics(DataSummary summary) { + // Update object counts + summary.getObjectCounts().forEach((type, count) -> DataSummaryPrometheusMetrics.objectCountsGauge.labels(type).set(count)); + + // Update active users + summary + .getActiveUsers() + .forEach((days, count) -> DataSummaryPrometheusMetrics.activeUsersGauge.labels(days.toString()).set(count)); + + // Update user invitations + summary + .getUserInvitations() + .forEach((type, count) -> DataSummaryPrometheusMetrics.userInvitationsGauge.labels(type).set(count)); + + // Update data value count + summary + .getDataValueCount() + .forEach((time, count) -> DataSummaryPrometheusMetrics.dataValueCountGauge.labels(time.toString()).set(count)); + + // Update event count + summary + .getEventCount() + .forEach((time, count) -> DataSummaryPrometheusMetrics.eventCountGauge.labels(time.toString()).set(count)); + + // Update system info as static gauges + if (summary.getSystem() != null) { + if (summary.getSystem().getVersion() != null) { + DataSummaryPrometheusMetrics.systemInfoGauge.labels("version", summary.getSystem().getVersion()).set(1); + } + if (summary.getSystem().getRevision() != null) { + DataSummaryPrometheusMetrics.systemInfoGauge.labels("revision", summary.getSystem().getRevision()).set(1); + } + if (summary.getSystem().getBuildTime() != null) { + DataSummaryPrometheusMetrics.systemInfoGauge + .labels("build_time", String.valueOf(summary.getSystem().getBuildTime())) + .set(1); + } + if (summary.getSystem().getSystemId() != null) { + DataSummaryPrometheusMetrics.systemInfoGauge.labels("system_id", summary.getSystem().getSystemId()).set(1); + } + if (summary.getSystem().getServerDate() != null) { + DataSummaryPrometheusMetrics.systemInfoGauge + .labels("server_date", String.valueOf(summary.getSystem().getServerDate())) + .set(1); + } + } + } +} From e0d6608f3531185edc2615c1211c66a9acfcb8a3 Mon Sep 17 00:00:00 2001 From: Jason Pickering Date: Wed, 22 Jan 2025 09:55:46 +0100 Subject: [PATCH 04/26] Fix(?) flaky test --- ...DataIntegrityUsersCaptureOrgUnitNotInTeiSearchHierarchy.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/dataintegrity/DataIntegrityUsersCaptureOrgUnitNotInTeiSearchHierarchy.java b/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/dataintegrity/DataIntegrityUsersCaptureOrgUnitNotInTeiSearchHierarchy.java index a4ca75ff856e..f336ada3d794 100644 --- a/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/dataintegrity/DataIntegrityUsersCaptureOrgUnitNotInTeiSearchHierarchy.java +++ b/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/dataintegrity/DataIntegrityUsersCaptureOrgUnitNotInTeiSearchHierarchy.java @@ -119,7 +119,7 @@ void testDataCaptureUnitInDataViewHierarchy() { // Note that there are already two users which exist due to the overall test setup, thus, five // users in total. Only userB should be flagged. - assertHasDataIntegrityIssues(DETAILS_ID_TYPE, CHECK_NAME, 20, userBUid, "janedoe", null, true); + assertHasDataIntegrityIssues(DETAILS_ID_TYPE, CHECK_NAME, 25, userBUid, "janedoe", null, true); JsonDataIntegrityDetails details = getDetails(CHECK_NAME); JsonList issues = details.getIssues(); From 3600adb2c51582e32d4776de3642ef399a4a56b6 Mon Sep 17 00:00:00 2001 From: Jason Pickering Date: Wed, 22 Jan 2025 09:56:26 +0100 Subject: [PATCH 05/26] Linting --- .../DataSummaryPrometheusMetrics.java | 174 ++++++++++-------- 1 file changed, 97 insertions(+), 77 deletions(-) diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/datasummary/DataSummaryPrometheusMetrics.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/datasummary/DataSummaryPrometheusMetrics.java index fba4574d8b7b..49b1299e6c23 100644 --- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/datasummary/DataSummaryPrometheusMetrics.java +++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/datasummary/DataSummaryPrometheusMetrics.java @@ -31,88 +31,108 @@ import org.hisp.dhis.datasummary.DataSummary; public class DataSummaryPrometheusMetrics { - public static final Gauge objectCountsGauge = - Gauge.build() - .name("data_summary_object_counts") - .help("Count of objects by type") - .labelNames("type") - .register(); - public static final Gauge activeUsersGauge = - Gauge.build() - .name("data_summary_active_users") - .help("Active users over days") - .labelNames("days") - .register(); - public static final Gauge userInvitationsGauge = - Gauge.build() - .name("data_summary_user_invitations") - .help("Count of user invitations") - .labelNames("type") - .register(); - public static final Gauge dataValueCountGauge = - Gauge.build() - .name("data_summary_data_value_count") - .help("Data value counts over time") - .labelNames("time") - .register(); - public static final Gauge eventCountGauge = - Gauge.build() - .name("data_summary_event_count") - .help("Event counts over time") - .labelNames("time") - .register(); - public static final Gauge systemInfoGauge = - Gauge.build() - .name("data_summary_system_info") - .help("DHIS2 System information") - .labelNames("key", "value") - .register(); + public static final Gauge objectCountsGauge = + Gauge.build() + .name("data_summary_object_counts") + .help("Count of objects by type") + .labelNames("type") + .register(); + public static final Gauge activeUsersGauge = + Gauge.build() + .name("data_summary_active_users") + .help("Active users over days") + .labelNames("days") + .register(); + public static final Gauge userInvitationsGauge = + Gauge.build() + .name("data_summary_user_invitations") + .help("Count of user invitations") + .labelNames("type") + .register(); + public static final Gauge dataValueCountGauge = + Gauge.build() + .name("data_summary_data_value_count") + .help("Data value counts over time") + .labelNames("time") + .register(); + public static final Gauge eventCountGauge = + Gauge.build() + .name("data_summary_event_count") + .help("Event counts over time") + .labelNames("time") + .register(); + public static final Gauge systemInfoGauge = + Gauge.build() + .name("data_summary_system_info") + .help("DHIS2 System information") + .labelNames("key", "value") + .register(); - public static void updateMetrics(DataSummary summary) { - // Update object counts - summary.getObjectCounts().forEach((type, count) -> DataSummaryPrometheusMetrics.objectCountsGauge.labels(type).set(count)); + public static void updateMetrics(DataSummary summary) { + // Update object counts + summary + .getObjectCounts() + .forEach( + (type, count) -> + DataSummaryPrometheusMetrics.objectCountsGauge.labels(type).set(count)); - // Update active users - summary - .getActiveUsers() - .forEach((days, count) -> DataSummaryPrometheusMetrics.activeUsersGauge.labels(days.toString()).set(count)); + // Update active users + summary + .getActiveUsers() + .forEach( + (days, count) -> + DataSummaryPrometheusMetrics.activeUsersGauge.labels(days.toString()).set(count)); - // Update user invitations - summary - .getUserInvitations() - .forEach((type, count) -> DataSummaryPrometheusMetrics.userInvitationsGauge.labels(type).set(count)); + // Update user invitations + summary + .getUserInvitations() + .forEach( + (type, count) -> + DataSummaryPrometheusMetrics.userInvitationsGauge.labels(type).set(count)); - // Update data value count - summary - .getDataValueCount() - .forEach((time, count) -> DataSummaryPrometheusMetrics.dataValueCountGauge.labels(time.toString()).set(count)); + // Update data value count + summary + .getDataValueCount() + .forEach( + (time, count) -> + DataSummaryPrometheusMetrics.dataValueCountGauge + .labels(time.toString()) + .set(count)); - // Update event count - summary - .getEventCount() - .forEach((time, count) -> DataSummaryPrometheusMetrics.eventCountGauge.labels(time.toString()).set(count)); + // Update event count + summary + .getEventCount() + .forEach( + (time, count) -> + DataSummaryPrometheusMetrics.eventCountGauge.labels(time.toString()).set(count)); - // Update system info as static gauges - if (summary.getSystem() != null) { - if (summary.getSystem().getVersion() != null) { - DataSummaryPrometheusMetrics.systemInfoGauge.labels("version", summary.getSystem().getVersion()).set(1); - } - if (summary.getSystem().getRevision() != null) { - DataSummaryPrometheusMetrics.systemInfoGauge.labels("revision", summary.getSystem().getRevision()).set(1); - } - if (summary.getSystem().getBuildTime() != null) { - DataSummaryPrometheusMetrics.systemInfoGauge - .labels("build_time", String.valueOf(summary.getSystem().getBuildTime())) - .set(1); - } - if (summary.getSystem().getSystemId() != null) { - DataSummaryPrometheusMetrics.systemInfoGauge.labels("system_id", summary.getSystem().getSystemId()).set(1); - } - if (summary.getSystem().getServerDate() != null) { - DataSummaryPrometheusMetrics.systemInfoGauge - .labels("server_date", String.valueOf(summary.getSystem().getServerDate())) - .set(1); - } - } + // Update system info as static gauges + if (summary.getSystem() != null) { + if (summary.getSystem().getVersion() != null) { + DataSummaryPrometheusMetrics.systemInfoGauge + .labels("version", summary.getSystem().getVersion()) + .set(1); + } + if (summary.getSystem().getRevision() != null) { + DataSummaryPrometheusMetrics.systemInfoGauge + .labels("revision", summary.getSystem().getRevision()) + .set(1); + } + if (summary.getSystem().getBuildTime() != null) { + DataSummaryPrometheusMetrics.systemInfoGauge + .labels("build_time", String.valueOf(summary.getSystem().getBuildTime())) + .set(1); + } + if (summary.getSystem().getSystemId() != null) { + DataSummaryPrometheusMetrics.systemInfoGauge + .labels("system_id", summary.getSystem().getSystemId()) + .set(1); + } + if (summary.getSystem().getServerDate() != null) { + DataSummaryPrometheusMetrics.systemInfoGauge + .labels("server_date", String.valueOf(summary.getSystem().getServerDate())) + .set(1); + } } + } } From b58adf26777d08ea7acb92cd612ee64d164f24f9 Mon Sep 17 00:00:00 2001 From: Jason Pickering Date: Wed, 22 Jan 2025 10:06:52 +0100 Subject: [PATCH 06/26] Fix code smells --- .../datasummary/DataSummaryController.java | 19 +++++++++++++------ .../DataSummaryPrometheusMetrics.java | 7 ++++++- 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/datasummary/DataSummaryController.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/datasummary/DataSummaryController.java index f533a23e838f..4d21366d033d 100644 --- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/datasummary/DataSummaryController.java +++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/datasummary/DataSummaryController.java @@ -43,10 +43,12 @@ import org.hisp.dhis.security.RequiresAuthority; import org.hisp.dhis.webapi.mvc.annotation.ApiVersion; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.server.ResponseStatusException; /** * dataSummary endpoint to access System Statistics @@ -71,11 +73,16 @@ public class DataSummaryController { @GetMapping(value = "/metrics", produces = TEXT_PLAIN_VALUE) @RequiresAuthority(anyOf = F_PERFORM_MAINTENANCE) - public @ResponseBody String getPrometheusMetrics() throws IOException { - DataSummary summary = dataStatisticsService.getSystemStatisticsSummary(); - DataSummaryPrometheusMetrics.updateMetrics(summary); - Writer writer = new StringWriter(); - TextFormat.write004(writer, CollectorRegistry.defaultRegistry.metricFamilySamples()); - return writer.toString(); + public @ResponseBody String getPrometheusMetrics() { + try { + DataSummary summary = dataStatisticsService.getSystemStatisticsSummary(); + DataSummaryPrometheusMetrics.updateMetrics(summary); + Writer writer = new StringWriter(); + TextFormat.write004(writer, CollectorRegistry.defaultRegistry.metricFamilySamples()); + return writer.toString(); + } catch (IOException e) { + throw new ResponseStatusException( + HttpStatus.INTERNAL_SERVER_ERROR, "Unable to produce metrics", e); + } } } diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/datasummary/DataSummaryPrometheusMetrics.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/datasummary/DataSummaryPrometheusMetrics.java index 49b1299e6c23..990ffca3b4e1 100644 --- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/datasummary/DataSummaryPrometheusMetrics.java +++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/datasummary/DataSummaryPrometheusMetrics.java @@ -30,7 +30,12 @@ import io.prometheus.client.Gauge; import org.hisp.dhis.datasummary.DataSummary; -public class DataSummaryPrometheusMetrics { +class DataSummaryPrometheusMetrics { + + private DataSummaryPrometheusMetrics() { + throw new IllegalStateException("Utility class"); + } + public static final Gauge objectCountsGauge = Gauge.build() .name("data_summary_object_counts") From fe0b9b11cea7e65b7a7754897f30632c5718970a Mon Sep 17 00:00:00 2001 From: Jason Pickering Date: Wed, 22 Jan 2025 10:08:38 +0100 Subject: [PATCH 07/26] Minor --- .../controller/datasummary/DataSummaryPrometheusMetrics.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/datasummary/DataSummaryPrometheusMetrics.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/datasummary/DataSummaryPrometheusMetrics.java index 990ffca3b4e1..cf46c372c0d8 100644 --- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/datasummary/DataSummaryPrometheusMetrics.java +++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/datasummary/DataSummaryPrometheusMetrics.java @@ -58,13 +58,13 @@ private DataSummaryPrometheusMetrics() { Gauge.build() .name("data_summary_data_value_count") .help("Data value counts over time") - .labelNames("time") + .labelNames("days") .register(); public static final Gauge eventCountGauge = Gauge.build() .name("data_summary_event_count") .help("Event counts over time") - .labelNames("time") + .labelNames("days") .register(); public static final Gauge systemInfoGauge = Gauge.build() From 47d6ee5d83fcd1f5bfd012dbaaa3979245c58cca Mon Sep 17 00:00:00 2001 From: Jason Pickering Date: Wed, 22 Jan 2025 13:13:51 +0100 Subject: [PATCH 08/26] Revert change in unrelated test --- ...DataIntegrityUsersCaptureOrgUnitNotInTeiSearchHierarchy.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/dataintegrity/DataIntegrityUsersCaptureOrgUnitNotInTeiSearchHierarchy.java b/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/dataintegrity/DataIntegrityUsersCaptureOrgUnitNotInTeiSearchHierarchy.java index f336ada3d794..a4ca75ff856e 100644 --- a/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/dataintegrity/DataIntegrityUsersCaptureOrgUnitNotInTeiSearchHierarchy.java +++ b/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/dataintegrity/DataIntegrityUsersCaptureOrgUnitNotInTeiSearchHierarchy.java @@ -119,7 +119,7 @@ void testDataCaptureUnitInDataViewHierarchy() { // Note that there are already two users which exist due to the overall test setup, thus, five // users in total. Only userB should be flagged. - assertHasDataIntegrityIssues(DETAILS_ID_TYPE, CHECK_NAME, 25, userBUid, "janedoe", null, true); + assertHasDataIntegrityIssues(DETAILS_ID_TYPE, CHECK_NAME, 20, userBUid, "janedoe", null, true); JsonDataIntegrityDetails details = getDetails(CHECK_NAME); JsonList issues = details.getIssues(); From 378bfe166f77d1a66abbd2259bd1da70ce31172b Mon Sep 17 00:00:00 2001 From: Jason Pickering Date: Wed, 22 Jan 2025 13:47:20 +0100 Subject: [PATCH 09/26] Fix test --- .../dhis/webapi/controller/DataSummaryControllerTest.java | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/DataSummaryControllerTest.java b/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/DataSummaryControllerTest.java index 068a8161c018..13d6feb7eb5e 100644 --- a/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/DataSummaryControllerTest.java +++ b/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/DataSummaryControllerTest.java @@ -51,10 +51,8 @@ void canGetPrometheusMetrics() { content.contains("# HELP data_summary_system_info DHIS2 System information"), "System information help text is missing"); assertTrue( - content.contains("data_summary_system_info{key=\"build_time\""), - "Build time metric is missing"); - assertTrue( - content.contains("data_summary_system_info{key=\"version\""), "Version metric is missing"); + content.contains("data_summary_system_info"), + "Build time metrics are missing"); // Verify active users metrics assertTrue( From 7938943f2d6fa054ef329d853fb848581abaf5b0 Mon Sep 17 00:00:00 2001 From: Jason Pickering Date: Wed, 22 Jan 2025 14:12:58 +0100 Subject: [PATCH 10/26] Linting --- .../dhis/webapi/controller/DataSummaryControllerTest.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/DataSummaryControllerTest.java b/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/DataSummaryControllerTest.java index 13d6feb7eb5e..0b1f3cdb8bbc 100644 --- a/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/DataSummaryControllerTest.java +++ b/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/DataSummaryControllerTest.java @@ -50,9 +50,7 @@ void canGetPrometheusMetrics() { assertTrue( content.contains("# HELP data_summary_system_info DHIS2 System information"), "System information help text is missing"); - assertTrue( - content.contains("data_summary_system_info"), - "Build time metrics are missing"); + assertTrue(content.contains("data_summary_system_info"), "Build time metrics are missing"); // Verify active users metrics assertTrue( From aba1fe23ef8a8e4aec45f959736ee91e48e5a7f1 Mon Sep 17 00:00:00 2001 From: Jason Pickering Date: Wed, 22 Jan 2025 15:14:26 +0100 Subject: [PATCH 11/26] Add test for data summary JSON response --- .../controller/DataSummaryControllerTest.java | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/DataSummaryControllerTest.java b/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/DataSummaryControllerTest.java index 0b1f3cdb8bbc..2929698f7df1 100644 --- a/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/DataSummaryControllerTest.java +++ b/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/DataSummaryControllerTest.java @@ -31,6 +31,8 @@ import org.hisp.dhis.http.HttpClientAdapter; import org.hisp.dhis.http.HttpStatus; +import org.hisp.dhis.jsontree.JsonMixed; +import org.hisp.dhis.jsontree.JsonValue; import org.hisp.dhis.test.webapi.PostgresControllerIntegrationTestBase; import org.junit.jupiter.api.Test; @@ -77,4 +79,64 @@ void canGetPrometheusMetrics() { "Event count help text is missing"); assertTrue(content.contains("data_summary_event_count"), "Event count metric is missing"); } + + @Test + void canGetSummaryStatistics() { + + HttpResponse response = GET("/api/dataSummary"); + assertEquals(HttpStatus.OK, response.status()); + JsonMixed content = response.content(); + assertFalse(content.isEmpty(), "Response content should not be empty"); + assertTrue(content.has("system"), "System information is missing"); + assertTrue(content.has("objectCounts"), "Object counts are missing"); + // All values in each of the object count keys should be integers + content + .get("objectCounts") + .asMap(JsonValue.class) + .values() + .forEach(value -> assertTrue(value.isInteger(), "Object count values should be integers")); + assertTrue(content.has("activeUsers"), "Active users are missing"); + // All values in each of the active user keys should be integers + content + .get("activeUsers") + .asMap(JsonValue.class) + .values() + .forEach(value -> assertTrue(value.isInteger(), "Active user values should be integers")); + content + .get("activeUsers") + .asMap(JsonValue.class) + .keys() + .forEach(key -> assertTrue(key.matches("\\d{1,2}"), "Active user keys should be integers")); + assertTrue(content.has("userInvitations"), "User invitations are missing"); + content + .get("activeUsers") + .asMap(JsonValue.class) + .values() + .forEach( + value -> assertTrue(value.isInteger(), "User invitation values should be integers")); + assertTrue(content.has("dataValueCount"), "Data value counts are missing"); + content + .get("dataValueCount") + .asMap(JsonValue.class) + .values() + .forEach( + value -> assertTrue(value.isInteger(), "Data value count values should be integers")); + content + .get("dataValueCount") + .asMap(JsonValue.class) + .keys() + .forEach( + key -> assertTrue(key.matches("\\d{1,2}"), "Data value count keys should be integers")); + assertTrue(content.has("eventCount"), "Event counts are missing"); + content + .get("eventCount") + .asMap(JsonValue.class) + .values() + .forEach(value -> assertTrue(value.isInteger(), "Event count values should be integers")); + content + .get("eventCount") + .asMap(JsonValue.class) + .keys() + .forEach(key -> assertTrue(key.matches("\\d{1,2}"), "Event count keys should be integers")); + } } From c2b7bbca915a48f8a2dfc5caec7e8618cbaa12d5 Mon Sep 17 00:00:00 2001 From: Jason Pickering Date: Wed, 22 Jan 2025 18:04:33 +0100 Subject: [PATCH 12/26] Refactor to use StringBuilder --- .../datasummary/DataSummaryController.java | 68 +++++++-- .../DataSummaryPrometheusMetrics.java | 143 ------------------ .../webapi/utils/PrometheusTextBuilder.java | 67 ++++++++ 3 files changed, 119 insertions(+), 159 deletions(-) delete mode 100644 dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/datasummary/DataSummaryPrometheusMetrics.java create mode 100644 dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/utils/PrometheusTextBuilder.java diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/datasummary/DataSummaryController.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/datasummary/DataSummaryController.java index 4d21366d033d..444ab30766d0 100644 --- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/datasummary/DataSummaryController.java +++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/datasummary/DataSummaryController.java @@ -31,24 +31,18 @@ import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; import static org.springframework.http.MediaType.TEXT_PLAIN_VALUE; -import io.prometheus.client.CollectorRegistry; -import io.prometheus.client.exporter.common.TextFormat; -import java.io.IOException; -import java.io.StringWriter; -import java.io.Writer; import org.hisp.dhis.common.DhisApiVersion; import org.hisp.dhis.common.OpenApi; import org.hisp.dhis.datastatistics.DataStatisticsService; import org.hisp.dhis.datasummary.DataSummary; import org.hisp.dhis.security.RequiresAuthority; import org.hisp.dhis.webapi.mvc.annotation.ApiVersion; +import org.hisp.dhis.webapi.utils.PrometheusTextBuilder; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.HttpStatus; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseBody; -import org.springframework.web.server.ResponseStatusException; /** * dataSummary endpoint to access System Statistics @@ -74,15 +68,57 @@ public class DataSummaryController { @GetMapping(value = "/metrics", produces = TEXT_PLAIN_VALUE) @RequiresAuthority(anyOf = F_PERFORM_MAINTENANCE) public @ResponseBody String getPrometheusMetrics() { - try { - DataSummary summary = dataStatisticsService.getSystemStatisticsSummary(); - DataSummaryPrometheusMetrics.updateMetrics(summary); - Writer writer = new StringWriter(); - TextFormat.write004(writer, CollectorRegistry.defaultRegistry.metricFamilySamples()); - return writer.toString(); - } catch (IOException e) { - throw new ResponseStatusException( - HttpStatus.INTERNAL_SERVER_ERROR, "Unable to produce metrics", e); + DataSummary summary = dataStatisticsService.getSystemStatisticsSummary(); + final String PROMETHEUS_GAUGE_NAME = "gauge"; + PrometheusTextBuilder metrics = new PrometheusTextBuilder(); + + metrics.updateMetricsFromMap( + summary.getObjectCounts(), + "data_summary_object_counts", + "type", + "Count of metadata objects", + PROMETHEUS_GAUGE_NAME); + + metrics.updateMetricsFromMap( + summary.getActiveUsers(), + "data_summary_active_users", + "days", + "Count of active users", + PROMETHEUS_GAUGE_NAME); + + metrics.updateMetricsFromMap( + summary.getUserInvitations(), + "data_summary_user_invitations", + "type", + "Count of user invitations", + PROMETHEUS_GAUGE_NAME); + + metrics.updateMetricsFromMap( + summary.getDataValueCount(), + "data_summary_data_value_count", + "days", + "Count of updated data values by day", + PROMETHEUS_GAUGE_NAME); + + metrics.updateMetricsFromMap( + summary.getEventCount(), + "data_summary_event_count", + "days", + "Count of updated events by day", + PROMETHEUS_GAUGE_NAME); + + // The system information is presented just as a static gauge with value 1.0 + // The key is the field name and the value is the field value + metrics.createPrometheusHelpLine("data_summary_system_info", "System information"); + metrics.createPrometheusTypeLine("data_summary_system_info", "gauge"); + if (summary.getSystem() != null) { + metrics.appendSystemInfo("version", summary.getSystem().getVersion()); + metrics.appendSystemInfo("revision", summary.getSystem().getRevision()); + metrics.appendSystemInfo("build_time", summary.getSystem().getBuildTime().toString()); + metrics.appendSystemInfo("system_id", summary.getSystem().getSystemId()); + metrics.appendSystemInfo("server_date", summary.getSystem().getServerDate().toString()); } + + return metrics.getMetrics(); } } diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/datasummary/DataSummaryPrometheusMetrics.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/datasummary/DataSummaryPrometheusMetrics.java deleted file mode 100644 index cf46c372c0d8..000000000000 --- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/datasummary/DataSummaryPrometheusMetrics.java +++ /dev/null @@ -1,143 +0,0 @@ -/* - * Copyright (c) 2004-2022, University of Oslo - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * Redistributions of source code must retain the above copyright notice, this - * list of conditions and the following disclaimer. - * - * Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * Neither the name of the HISP project nor the names of its contributors may - * be used to endorse or promote products derived from this software without - * specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND - * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR - * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ -package org.hisp.dhis.webapi.controller.datasummary; - -import io.prometheus.client.Gauge; -import org.hisp.dhis.datasummary.DataSummary; - -class DataSummaryPrometheusMetrics { - - private DataSummaryPrometheusMetrics() { - throw new IllegalStateException("Utility class"); - } - - public static final Gauge objectCountsGauge = - Gauge.build() - .name("data_summary_object_counts") - .help("Count of objects by type") - .labelNames("type") - .register(); - public static final Gauge activeUsersGauge = - Gauge.build() - .name("data_summary_active_users") - .help("Active users over days") - .labelNames("days") - .register(); - public static final Gauge userInvitationsGauge = - Gauge.build() - .name("data_summary_user_invitations") - .help("Count of user invitations") - .labelNames("type") - .register(); - public static final Gauge dataValueCountGauge = - Gauge.build() - .name("data_summary_data_value_count") - .help("Data value counts over time") - .labelNames("days") - .register(); - public static final Gauge eventCountGauge = - Gauge.build() - .name("data_summary_event_count") - .help("Event counts over time") - .labelNames("days") - .register(); - public static final Gauge systemInfoGauge = - Gauge.build() - .name("data_summary_system_info") - .help("DHIS2 System information") - .labelNames("key", "value") - .register(); - - public static void updateMetrics(DataSummary summary) { - // Update object counts - summary - .getObjectCounts() - .forEach( - (type, count) -> - DataSummaryPrometheusMetrics.objectCountsGauge.labels(type).set(count)); - - // Update active users - summary - .getActiveUsers() - .forEach( - (days, count) -> - DataSummaryPrometheusMetrics.activeUsersGauge.labels(days.toString()).set(count)); - - // Update user invitations - summary - .getUserInvitations() - .forEach( - (type, count) -> - DataSummaryPrometheusMetrics.userInvitationsGauge.labels(type).set(count)); - - // Update data value count - summary - .getDataValueCount() - .forEach( - (time, count) -> - DataSummaryPrometheusMetrics.dataValueCountGauge - .labels(time.toString()) - .set(count)); - - // Update event count - summary - .getEventCount() - .forEach( - (time, count) -> - DataSummaryPrometheusMetrics.eventCountGauge.labels(time.toString()).set(count)); - - // Update system info as static gauges - if (summary.getSystem() != null) { - if (summary.getSystem().getVersion() != null) { - DataSummaryPrometheusMetrics.systemInfoGauge - .labels("version", summary.getSystem().getVersion()) - .set(1); - } - if (summary.getSystem().getRevision() != null) { - DataSummaryPrometheusMetrics.systemInfoGauge - .labels("revision", summary.getSystem().getRevision()) - .set(1); - } - if (summary.getSystem().getBuildTime() != null) { - DataSummaryPrometheusMetrics.systemInfoGauge - .labels("build_time", String.valueOf(summary.getSystem().getBuildTime())) - .set(1); - } - if (summary.getSystem().getSystemId() != null) { - DataSummaryPrometheusMetrics.systemInfoGauge - .labels("system_id", summary.getSystem().getSystemId()) - .set(1); - } - if (summary.getSystem().getServerDate() != null) { - DataSummaryPrometheusMetrics.systemInfoGauge - .labels("server_date", String.valueOf(summary.getSystem().getServerDate())) - .set(1); - } - } - } -} diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/utils/PrometheusTextBuilder.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/utils/PrometheusTextBuilder.java new file mode 100644 index 000000000000..538e9e60997f --- /dev/null +++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/utils/PrometheusTextBuilder.java @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2004-2025, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.webapi.utils; + +import java.util.Map; + +public class PrometheusTextBuilder { + + private StringBuilder metrics = new StringBuilder(); + + public void createPrometheusHelpLine(String metricName, String help) { + metrics.append("# HELP ").append(metricName).append(" ").append(help).append("\n"); + } + + public void createPrometheusTypeLine(String metricName, String type) { + metrics.append("# TYPE ").append(metricName).append(" ").append(type).append("\n"); + } + + public void updateMetricsFromMap( + Map map, String metricName, String keyName, String help, String type) { + createPrometheusHelpLine(metricName, help); + createPrometheusTypeLine(metricName, type); + map.forEach( + (key, value) -> + metrics.append("%s{%s=\"%s\", } %s.0 \n".formatted(metricName, keyName, key, value))); + } + + public void appendSystemInfo(String key, String value) { + if (value != null) { + metrics + .append("data_summary_system_info{key=\"") + .append(key) + .append("\", value=\"") + .append(value) + .append("\"} 1.0\n"); + } + } + + public String getMetrics() { + return metrics.toString(); + } +} From 739f8dcb9cecb24a1773e553710ed5866251dbd7 Mon Sep 17 00:00:00 2001 From: Jason Pickering Date: Wed, 22 Jan 2025 18:43:04 +0100 Subject: [PATCH 13/26] Rename some methods --- .../datasummary/DataSummaryController.java | 21 +++++++++----- .../webapi/utils/PrometheusTextBuilder.java | 28 ++++++++++++------- 2 files changed, 32 insertions(+), 17 deletions(-) diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/datasummary/DataSummaryController.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/datasummary/DataSummaryController.java index 444ab30766d0..3d775f55d7e2 100644 --- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/datasummary/DataSummaryController.java +++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/datasummary/DataSummaryController.java @@ -109,14 +109,21 @@ public class DataSummaryController { // The system information is presented just as a static gauge with value 1.0 // The key is the field name and the value is the field value - metrics.createPrometheusHelpLine("data_summary_system_info", "System information"); - metrics.createPrometheusTypeLine("data_summary_system_info", "gauge"); + metrics.helpLine("data_summary_system_info", "System information"); + metrics.typeLine("data_summary_system_info", "gauge"); if (summary.getSystem() != null) { - metrics.appendSystemInfo("version", summary.getSystem().getVersion()); - metrics.appendSystemInfo("revision", summary.getSystem().getRevision()); - metrics.appendSystemInfo("build_time", summary.getSystem().getBuildTime().toString()); - metrics.appendSystemInfo("system_id", summary.getSystem().getSystemId()); - metrics.appendSystemInfo("server_date", summary.getSystem().getServerDate().toString()); + metrics.appendStaticKeyValue( + "data_summary_system_info", "version", summary.getSystem().getVersion()); + metrics.appendStaticKeyValue( + "data_summary_system_info", "revision", summary.getSystem().getRevision()); + metrics.appendStaticKeyValue( + "data_summary_system_info", "build_time", summary.getSystem().getBuildTime().toString()); + metrics.appendStaticKeyValue( + "data_summary_system_info", "system_id", summary.getSystem().getSystemId()); + metrics.appendStaticKeyValue( + "data_summary_system_info", + "server_date", + summary.getSystem().getServerDate().toString()); } return metrics.getMetrics(); diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/utils/PrometheusTextBuilder.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/utils/PrometheusTextBuilder.java index 538e9e60997f..baf71015d8ad 100644 --- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/utils/PrometheusTextBuilder.java +++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/utils/PrometheusTextBuilder.java @@ -29,35 +29,43 @@ import java.util.Map; +/** + * A simple utility class to build Prometheus text format metrics. Note that there is no validation + * of the input, so the user should make sure that the input is correct. The Prometheus text format + * is documented here: https://prometheus.io/docs/instrumenting/exposition_formats/ + * + * @author Jason P. Pickering + */ public class PrometheusTextBuilder { private StringBuilder metrics = new StringBuilder(); - public void createPrometheusHelpLine(String metricName, String help) { - metrics.append("# HELP ").append(metricName).append(" ").append(help).append("\n"); + public void helpLine(String metricName, String help) { + metrics.append("# HELP ").append(metricName).append(" ").append(help).append("%n"); } - public void createPrometheusTypeLine(String metricName, String type) { - metrics.append("# TYPE ").append(metricName).append(" ").append(type).append("\n"); + public void typeLine(String metricName, String type) { + metrics.append("# TYPE ").append(metricName).append(" ").append(type).append("%n"); } public void updateMetricsFromMap( Map map, String metricName, String keyName, String help, String type) { - createPrometheusHelpLine(metricName, help); - createPrometheusTypeLine(metricName, type); + helpLine(metricName, help); + typeLine(metricName, type); map.forEach( (key, value) -> - metrics.append("%s{%s=\"%s\", } %s.0 \n".formatted(metricName, keyName, key, value))); + metrics.append("%s{%s=\"%s\" } %s%n".formatted(metricName, keyName, key, value))); } - public void appendSystemInfo(String key, String value) { + public void appendStaticKeyValue(String metricName, String key, String value) { if (value != null) { metrics - .append("data_summary_system_info{key=\"") + .append(metricName) + .append("{key=\"") .append(key) .append("\", value=\"") .append(value) - .append("\"} 1.0\n"); + .append("\"} 1%n"); } } From ba2c355c6e98f0a210046d2b17c7336fb4ea25b9 Mon Sep 17 00:00:00 2001 From: Jason Pickering Date: Wed, 22 Jan 2025 20:26:22 +0100 Subject: [PATCH 14/26] Null fix --- .../datasummary/DataSummaryController.java | 36 ++++++++++++------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/datasummary/DataSummaryController.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/datasummary/DataSummaryController.java index 3d775f55d7e2..17aea6e6d8ee 100644 --- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/datasummary/DataSummaryController.java +++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/datasummary/DataSummaryController.java @@ -57,7 +57,12 @@ @ApiVersion({DhisApiVersion.DEFAULT, DhisApiVersion.ALL}) public class DataSummaryController { - @Autowired public DataStatisticsService dataStatisticsService; + private final DataStatisticsService dataStatisticsService; + + @Autowired + public DataSummaryController(DataStatisticsService dataStatisticsService) { + this.dataStatisticsService = dataStatisticsService; + } @GetMapping(produces = APPLICATION_JSON_VALUE) @RequiresAuthority(anyOf = F_PERFORM_MAINTENANCE) @@ -112,18 +117,23 @@ public class DataSummaryController { metrics.helpLine("data_summary_system_info", "System information"); metrics.typeLine("data_summary_system_info", "gauge"); if (summary.getSystem() != null) { - metrics.appendStaticKeyValue( - "data_summary_system_info", "version", summary.getSystem().getVersion()); - metrics.appendStaticKeyValue( - "data_summary_system_info", "revision", summary.getSystem().getRevision()); - metrics.appendStaticKeyValue( - "data_summary_system_info", "build_time", summary.getSystem().getBuildTime().toString()); - metrics.appendStaticKeyValue( - "data_summary_system_info", "system_id", summary.getSystem().getSystemId()); - metrics.appendStaticKeyValue( - "data_summary_system_info", - "server_date", - summary.getSystem().getServerDate().toString()); + if (summary.getSystem() != null) { + if (summary.getSystem().getVersion() != null) { + metrics.appendStaticKeyValue("data_summary_system_info", "version", summary.getSystem().getVersion()); + } + if (summary.getSystem().getRevision() != null) { + metrics.appendStaticKeyValue("data_summary_system_info", "revision", summary.getSystem().getRevision()); + } + if (summary.getSystem().getBuildTime() != null) { + metrics.appendStaticKeyValue("data_summary_system_info", "build_time", summary.getSystem().getBuildTime().toString()); + } + if (summary.getSystem().getSystemId() != null) { + metrics.appendStaticKeyValue("data_summary_system_info", "system_id", summary.getSystem().getSystemId()); + } + if (summary.getSystem().getServerDate() != null) { + metrics.appendStaticKeyValue("data_summary_system_info", "server_date", summary.getSystem().getServerDate().toString()); + } + } } return metrics.getMetrics(); From 0a0cede4922fa989ee0abbe09d0b098641151f78 Mon Sep 17 00:00:00 2001 From: Jason Pickering Date: Wed, 22 Jan 2025 20:26:59 +0100 Subject: [PATCH 15/26] Linting --- .../datasummary/DataSummaryController.java | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/datasummary/DataSummaryController.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/datasummary/DataSummaryController.java index 17aea6e6d8ee..0451988109ff 100644 --- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/datasummary/DataSummaryController.java +++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/datasummary/DataSummaryController.java @@ -119,19 +119,28 @@ public DataSummaryController(DataStatisticsService dataStatisticsService) { if (summary.getSystem() != null) { if (summary.getSystem() != null) { if (summary.getSystem().getVersion() != null) { - metrics.appendStaticKeyValue("data_summary_system_info", "version", summary.getSystem().getVersion()); + metrics.appendStaticKeyValue( + "data_summary_system_info", "version", summary.getSystem().getVersion()); } if (summary.getSystem().getRevision() != null) { - metrics.appendStaticKeyValue("data_summary_system_info", "revision", summary.getSystem().getRevision()); + metrics.appendStaticKeyValue( + "data_summary_system_info", "revision", summary.getSystem().getRevision()); } if (summary.getSystem().getBuildTime() != null) { - metrics.appendStaticKeyValue("data_summary_system_info", "build_time", summary.getSystem().getBuildTime().toString()); + metrics.appendStaticKeyValue( + "data_summary_system_info", + "build_time", + summary.getSystem().getBuildTime().toString()); } if (summary.getSystem().getSystemId() != null) { - metrics.appendStaticKeyValue("data_summary_system_info", "system_id", summary.getSystem().getSystemId()); + metrics.appendStaticKeyValue( + "data_summary_system_info", "system_id", summary.getSystem().getSystemId()); } if (summary.getSystem().getServerDate() != null) { - metrics.appendStaticKeyValue("data_summary_system_info", "server_date", summary.getSystem().getServerDate().toString()); + metrics.appendStaticKeyValue( + "data_summary_system_info", + "server_date", + summary.getSystem().getServerDate().toString()); } } } From 169e809af76e4553666c6313a260ccaab61d0a28 Mon Sep 17 00:00:00 2001 From: Jason Pickering Date: Thu, 23 Jan 2025 06:09:53 +0100 Subject: [PATCH 16/26] Simplification and tests --- .../datasummary/DataSummaryController.java | 38 +++----- .../webapi/utils/PrometheusTextBuilder.java | 50 ++++++++--- .../utils/PrometheusTextBuilderTest.java | 86 +++++++++++++++++++ 3 files changed, 135 insertions(+), 39 deletions(-) create mode 100644 dhis-2/dhis-web-api/src/test/java/org/hisp/dhis/webapi/utils/PrometheusTextBuilderTest.java diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/datasummary/DataSummaryController.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/datasummary/DataSummaryController.java index 0451988109ff..83833116e2bd 100644 --- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/datasummary/DataSummaryController.java +++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/datasummary/DataSummaryController.java @@ -117,32 +117,18 @@ public DataSummaryController(DataStatisticsService dataStatisticsService) { metrics.helpLine("data_summary_system_info", "System information"); metrics.typeLine("data_summary_system_info", "gauge"); if (summary.getSystem() != null) { - if (summary.getSystem() != null) { - if (summary.getSystem().getVersion() != null) { - metrics.appendStaticKeyValue( - "data_summary_system_info", "version", summary.getSystem().getVersion()); - } - if (summary.getSystem().getRevision() != null) { - metrics.appendStaticKeyValue( - "data_summary_system_info", "revision", summary.getSystem().getRevision()); - } - if (summary.getSystem().getBuildTime() != null) { - metrics.appendStaticKeyValue( - "data_summary_system_info", - "build_time", - summary.getSystem().getBuildTime().toString()); - } - if (summary.getSystem().getSystemId() != null) { - metrics.appendStaticKeyValue( - "data_summary_system_info", "system_id", summary.getSystem().getSystemId()); - } - if (summary.getSystem().getServerDate() != null) { - metrics.appendStaticKeyValue( - "data_summary_system_info", - "server_date", - summary.getSystem().getServerDate().toString()); - } - } + metrics.appendStaticKeyValue( + "data_summary_system_info", "version", summary.getSystem().getVersion()); + metrics.appendStaticKeyValue( + "data_summary_system_info", "revision", summary.getSystem().getRevision()); + metrics.appendStaticKeyValue( + "data_summary_system_info", "build_time", summary.getSystem().getBuildTime().toString()); + metrics.appendStaticKeyValue( + "data_summary_system_info", "system_id", summary.getSystem().getSystemId()); + metrics.appendStaticKeyValue( + "data_summary_system_info", + "server_date", + summary.getSystem().getServerDate().toString()); } return metrics.getMetrics(); diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/utils/PrometheusTextBuilder.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/utils/PrometheusTextBuilder.java index baf71015d8ad..b89c64bd766c 100644 --- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/utils/PrometheusTextBuilder.java +++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/utils/PrometheusTextBuilder.java @@ -30,9 +30,8 @@ import java.util.Map; /** - * A simple utility class to build Prometheus text format metrics. Note that there is no validation - * of the input, so the user should make sure that the input is correct. The Prometheus text format - * is documented here: https://prometheus.io/docs/instrumenting/exposition_formats/ + * A simple utility class to build Prometheus text format metrics. The Prometheus text format is + * documented here: https://prometheus.io/docs/instrumenting/exposition_formats/ * * @author Jason P. Pickering */ @@ -41,34 +40,59 @@ public class PrometheusTextBuilder { private StringBuilder metrics = new StringBuilder(); public void helpLine(String metricName, String help) { - metrics.append("# HELP ").append(metricName).append(" ").append(help).append("%n"); + metrics.append(String.format("# HELP %s %s%n", metricName, help)); } + /** + * Appends a Prometheus metric type line to the metrics. + * + * @param metricName the name of the metric + * @param type the type of the metric (e.g., counter, gauge) + */ public void typeLine(String metricName, String type) { - metrics.append("# TYPE ").append(metricName).append(" ").append(type).append("%n"); + metrics.append(String.format("# TYPE %s %s%n", metricName, type)); } + /** + * Transform a Map into a Prometheus text format metric. Note that the key is assumed + * to be a string, and the value should be a number which is capable of being converted to a + * string. + * + * @param map the map containing the metrics data + * @param metricName the name of the metric + * @param keyName the name of the key in the metric + * @param help the help text for the metric + * @param type the type of the metric + */ public void updateMetricsFromMap( Map map, String metricName, String keyName, String help, String type) { helpLine(metricName, help); typeLine(metricName, type); map.forEach( (key, value) -> - metrics.append("%s{%s=\"%s\" } %s%n".formatted(metricName, keyName, key, value))); + metrics.append("%s{%s=\"%s\"} %s%n".formatted(metricName, keyName, key, value))); } + /** + * Appends a static key-value pair to the Prometheus metrics. This is most useful for representing + * labels in the Prometheus text format such as build time, version, etc. This will produce a + * metric with a static value of 1. + * + * @param metricName the name of the metric + * @param key the key for the metric + * @param value the value for the metric + */ public void appendStaticKeyValue(String metricName, String key, String value) { if (value != null) { - metrics - .append(metricName) - .append("{key=\"") - .append(key) - .append("\", value=\"") - .append(value) - .append("\"} 1%n"); + metrics.append(String.format("%s{key=\"%s\", value=\"%s\"} 1%n", metricName, key, value)); } } + /** + * Returns the Prometheus metrics as a string. + * + * @return the metrics in Prometheus text format + */ public String getMetrics() { return metrics.toString(); } diff --git a/dhis-2/dhis-web-api/src/test/java/org/hisp/dhis/webapi/utils/PrometheusTextBuilderTest.java b/dhis-2/dhis-web-api/src/test/java/org/hisp/dhis/webapi/utils/PrometheusTextBuilderTest.java new file mode 100644 index 000000000000..975373169d34 --- /dev/null +++ b/dhis-2/dhis-web-api/src/test/java/org/hisp/dhis/webapi/utils/PrometheusTextBuilderTest.java @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2004-2025, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.webapi.utils; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.Map; +import org.junit.jupiter.api.Test; + +/** + * Unit tests for the PrometheusTextBuilder class. + * + * @author Jason P. Pickering + */ +public class PrometheusTextBuilderTest { + @Test + void getMetricsReturnsEmptyStringWhenNoMetrics() { + PrometheusTextBuilder builder = new PrometheusTextBuilder(); + assertEquals("", builder.getMetrics()); + } + + @Test + void helpLineAppendsHelpText() { + PrometheusTextBuilder builder = new PrometheusTextBuilder(); + builder.helpLine("test_metric", "This is a test metric"); + assertEquals("# HELP test_metric This is a test metric\n", builder.getMetrics()); + } + + @Test + void typeLineAppendsTypeText() { + PrometheusTextBuilder builder = new PrometheusTextBuilder(); + builder.typeLine("test_metric", "counter"); + assertEquals("# TYPE test_metric counter\n", builder.getMetrics()); + } + + @Test + void updateMetricsFromMapAppendsMetrics() { + PrometheusTextBuilder builder = new PrometheusTextBuilder(); + Map map = Map.of("key1", 1); + builder.updateMetricsFromMap(map, "test_metric", "key", "Test help", "gauge"); + String expected = + "# HELP test_metric Test help\n" + + "# TYPE test_metric gauge\n" + + "test_metric{key=\"key1\"} 1\n"; + assertEquals(expected, builder.getMetrics()); + } + + @Test + void appendStaticKeyValueAppendsKeyValue() { + PrometheusTextBuilder builder = new PrometheusTextBuilder(); + builder.appendStaticKeyValue("test_metric", "key", "value"); + assertEquals("test_metric{key=\"key\", value=\"value\"} 1\n", builder.getMetrics()); + } + + @Test + void appendStaticKeyValueIgnoresNullValue() { + PrometheusTextBuilder builder = new PrometheusTextBuilder(); + builder.appendStaticKeyValue("test_metric", "key", null); + assertEquals("", builder.getMetrics()); + } +} From 474d0fc82e2d1e8e2e989d751650698bcddbac6c Mon Sep 17 00:00:00 2001 From: Jason Pickering Date: Thu, 23 Jan 2025 06:31:28 +0100 Subject: [PATCH 17/26] Remove code smells --- .../dhis/webapi/utils/PrometheusTextBuilderTest.java | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/dhis-2/dhis-web-api/src/test/java/org/hisp/dhis/webapi/utils/PrometheusTextBuilderTest.java b/dhis-2/dhis-web-api/src/test/java/org/hisp/dhis/webapi/utils/PrometheusTextBuilderTest.java index 975373169d34..7d93b2221fe5 100644 --- a/dhis-2/dhis-web-api/src/test/java/org/hisp/dhis/webapi/utils/PrometheusTextBuilderTest.java +++ b/dhis-2/dhis-web-api/src/test/java/org/hisp/dhis/webapi/utils/PrometheusTextBuilderTest.java @@ -37,7 +37,7 @@ * * @author Jason P. Pickering */ -public class PrometheusTextBuilderTest { +class PrometheusTextBuilderTest { @Test void getMetricsReturnsEmptyStringWhenNoMetrics() { PrometheusTextBuilder builder = new PrometheusTextBuilder(); @@ -63,10 +63,11 @@ void updateMetricsFromMapAppendsMetrics() { PrometheusTextBuilder builder = new PrometheusTextBuilder(); Map map = Map.of("key1", 1); builder.updateMetricsFromMap(map, "test_metric", "key", "Test help", "gauge"); - String expected = - "# HELP test_metric Test help\n" - + "# TYPE test_metric gauge\n" - + "test_metric{key=\"key1\"} 1\n"; + String expected = """ + # HELP test_metric Test help + # TYPE test_metric gauge + test_metric{key="key1"} 1 + """; assertEquals(expected, builder.getMetrics()); } From e396fa94313b65bab52f09169a7cceb180d34d19 Mon Sep 17 00:00:00 2001 From: Jason Pickering Date: Thu, 23 Jan 2025 06:32:04 +0100 Subject: [PATCH 18/26] Linting --- .../org/hisp/dhis/webapi/utils/PrometheusTextBuilderTest.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/dhis-2/dhis-web-api/src/test/java/org/hisp/dhis/webapi/utils/PrometheusTextBuilderTest.java b/dhis-2/dhis-web-api/src/test/java/org/hisp/dhis/webapi/utils/PrometheusTextBuilderTest.java index 7d93b2221fe5..810417f4dcd3 100644 --- a/dhis-2/dhis-web-api/src/test/java/org/hisp/dhis/webapi/utils/PrometheusTextBuilderTest.java +++ b/dhis-2/dhis-web-api/src/test/java/org/hisp/dhis/webapi/utils/PrometheusTextBuilderTest.java @@ -63,7 +63,8 @@ void updateMetricsFromMapAppendsMetrics() { PrometheusTextBuilder builder = new PrometheusTextBuilder(); Map map = Map.of("key1", 1); builder.updateMetricsFromMap(map, "test_metric", "key", "Test help", "gauge"); - String expected = """ + String expected = + """ # HELP test_metric Test help # TYPE test_metric gauge test_metric{key="key1"} 1 From 344e3e2b78d54b292a7e8c64a0267d258a60d544 Mon Sep 17 00:00:00 2001 From: Jason Pickering Date: Thu, 23 Jan 2025 07:27:33 +0100 Subject: [PATCH 19/26] More fixes --- .../controller/datasummary/DataSummaryController.java | 3 ++- .../org/hisp/dhis/webapi/utils/PrometheusTextBuilder.java | 6 ++++++ .../hisp/dhis/webapi/utils/PrometheusTextBuilderTest.java | 4 +++- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/datasummary/DataSummaryController.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/datasummary/DataSummaryController.java index 83833116e2bd..2755cd7b1021 100644 --- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/datasummary/DataSummaryController.java +++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/datasummary/DataSummaryController.java @@ -116,13 +116,14 @@ public DataSummaryController(DataStatisticsService dataStatisticsService) { // The key is the field name and the value is the field value metrics.helpLine("data_summary_system_info", "System information"); metrics.typeLine("data_summary_system_info", "gauge"); + if (summary.getSystem() != null) { metrics.appendStaticKeyValue( "data_summary_system_info", "version", summary.getSystem().getVersion()); metrics.appendStaticKeyValue( "data_summary_system_info", "revision", summary.getSystem().getRevision()); metrics.appendStaticKeyValue( - "data_summary_system_info", "build_time", summary.getSystem().getBuildTime().toString()); + "data_summary_system_info", "build_time", summary.getSystem().getBuildTime()); metrics.appendStaticKeyValue( "data_summary_system_info", "system_id", summary.getSystem().getSystemId()); metrics.appendStaticKeyValue( diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/utils/PrometheusTextBuilder.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/utils/PrometheusTextBuilder.java index b89c64bd766c..39650441c469 100644 --- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/utils/PrometheusTextBuilder.java +++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/utils/PrometheusTextBuilder.java @@ -88,6 +88,12 @@ public void appendStaticKeyValue(String metricName, String key, String value) { } } + public void appendStaticKeyValue(String metricName, String key, java.util.Date value) { + if (value != null) { + metrics.append(String.format("%s{key=\"%s\", value=\"%s\"} 1%n", metricName, key, value)); + } + } + /** * Returns the Prometheus metrics as a string. * diff --git a/dhis-2/dhis-web-api/src/test/java/org/hisp/dhis/webapi/utils/PrometheusTextBuilderTest.java b/dhis-2/dhis-web-api/src/test/java/org/hisp/dhis/webapi/utils/PrometheusTextBuilderTest.java index 810417f4dcd3..7f09164751a7 100644 --- a/dhis-2/dhis-web-api/src/test/java/org/hisp/dhis/webapi/utils/PrometheusTextBuilderTest.java +++ b/dhis-2/dhis-web-api/src/test/java/org/hisp/dhis/webapi/utils/PrometheusTextBuilderTest.java @@ -29,6 +29,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; +import java.util.Date; import java.util.Map; import org.junit.jupiter.api.Test; @@ -82,7 +83,8 @@ void appendStaticKeyValueAppendsKeyValue() { @Test void appendStaticKeyValueIgnoresNullValue() { PrometheusTextBuilder builder = new PrometheusTextBuilder(); - builder.appendStaticKeyValue("test_metric", "key", null); + builder.appendStaticKeyValue("test_metric", "key", (String) null); + builder.appendStaticKeyValue("test_metric", "key", (Date) null); assertEquals("", builder.getMetrics()); } } From ddb376491539cada15490b57710299f783fabe0f Mon Sep 17 00:00:00 2001 From: Jason Pickering Date: Thu, 23 Jan 2025 07:52:43 +0100 Subject: [PATCH 20/26] Fix test --- .../controller/DataSummaryControllerTest.java | 28 ++++--------------- .../datasummary/DataSummaryController.java | 2 +- 2 files changed, 7 insertions(+), 23 deletions(-) diff --git a/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/DataSummaryControllerTest.java b/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/DataSummaryControllerTest.java index 2929698f7df1..79536d7f9e95 100644 --- a/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/DataSummaryControllerTest.java +++ b/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/DataSummaryControllerTest.java @@ -40,43 +40,29 @@ class DataSummaryControllerTest extends PostgresControllerIntegrationTestBase { @Test void canGetPrometheusMetrics() { - // Send the GET request and check the status + HttpResponse response = GET("/api/dataSummary/metrics", HttpClientAdapter.Accept("text/plain")); assertEquals(HttpStatus.OK, response.status()); - - // Extract the response content String content = response.content("text/plain"); assertFalse(content.isEmpty(), "Response content should not be empty"); - - // Verify the presence of system information metrics assertTrue( - content.contains("# HELP data_summary_system_info DHIS2 System information"), + content.contains("# HELP data_summary_system_info"), "System information help text is missing"); assertTrue(content.contains("data_summary_system_info"), "Build time metrics are missing"); - - // Verify active users metrics assertTrue( - content.contains("# HELP data_summary_active_users Active users over days"), - "Active users help text is missing"); + content.contains("# HELP data_summary_active_users"), "Active users help text is missing"); assertTrue(content.contains("data_summary_active_user"), "Active users metric is missing"); - - // Verify object counts metrics assertTrue( - content.contains("# HELP data_summary_object_counts Count of objects by type"), + content.contains("# HELP data_summary_object_counts"), "Object counts help text is missing"); assertTrue(content.contains("data_summary_object_counts"), "Object counts metric is missing"); - - // Verify data value count metrics assertTrue( - content.contains("# HELP data_summary_data_value_count Data value counts over time"), + content.contains("# HELP data_summary_data_value_count"), "Data value count help text is missing"); assertTrue( content.contains("data_summary_data_value_count"), "Data value count metric is missing"); - - // Verify event count metrics assertTrue( - content.contains("# HELP data_summary_event_count Event counts over time"), - "Event count help text is missing"); + content.contains("# HELP data_summary_event_count"), "Event count help text is missing"); assertTrue(content.contains("data_summary_event_count"), "Event count metric is missing"); } @@ -89,14 +75,12 @@ void canGetSummaryStatistics() { assertFalse(content.isEmpty(), "Response content should not be empty"); assertTrue(content.has("system"), "System information is missing"); assertTrue(content.has("objectCounts"), "Object counts are missing"); - // All values in each of the object count keys should be integers content .get("objectCounts") .asMap(JsonValue.class) .values() .forEach(value -> assertTrue(value.isInteger(), "Object count values should be integers")); assertTrue(content.has("activeUsers"), "Active users are missing"); - // All values in each of the active user keys should be integers content .get("activeUsers") .asMap(JsonValue.class) diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/datasummary/DataSummaryController.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/datasummary/DataSummaryController.java index 2755cd7b1021..0fcaaa6bd220 100644 --- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/datasummary/DataSummaryController.java +++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/datasummary/DataSummaryController.java @@ -88,7 +88,7 @@ public DataSummaryController(DataStatisticsService dataStatisticsService) { summary.getActiveUsers(), "data_summary_active_users", "days", - "Count of active users", + "Count of active users by day", PROMETHEUS_GAUGE_NAME); metrics.updateMetricsFromMap( From aa25ca56f3f5d90da54a1f9960a6bd1afc4697e3 Mon Sep 17 00:00:00 2001 From: Jason Pickering Date: Thu, 23 Jan 2025 08:30:48 +0100 Subject: [PATCH 21/26] Remove server date from metrics as this is not particularly useful to log --- .../webapi/controller/datasummary/DataSummaryController.java | 4 ---- 1 file changed, 4 deletions(-) diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/datasummary/DataSummaryController.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/datasummary/DataSummaryController.java index 0fcaaa6bd220..5375869299e4 100644 --- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/datasummary/DataSummaryController.java +++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/datasummary/DataSummaryController.java @@ -126,10 +126,6 @@ public DataSummaryController(DataStatisticsService dataStatisticsService) { "data_summary_system_info", "build_time", summary.getSystem().getBuildTime()); metrics.appendStaticKeyValue( "data_summary_system_info", "system_id", summary.getSystem().getSystemId()); - metrics.appendStaticKeyValue( - "data_summary_system_info", - "server_date", - summary.getSystem().getServerDate().toString()); } return metrics.getMetrics(); From deadf9688de6bd77f31430661094392164abe010 Mon Sep 17 00:00:00 2001 From: Jason Pickering Date: Thu, 23 Jan 2025 09:32:16 +0100 Subject: [PATCH 22/26] Improve system info output --- .../controller/DataSummaryControllerTest.java | 32 +++++++++--- .../datasummary/DataSummaryController.java | 17 +------ .../webapi/utils/PrometheusTextBuilder.java | 34 +++++++------ .../utils/PrometheusTextBuilderTest.java | 49 ++++++++++++++++--- 4 files changed, 87 insertions(+), 45 deletions(-) diff --git a/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/DataSummaryControllerTest.java b/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/DataSummaryControllerTest.java index 79536d7f9e95..9c3140cef65d 100644 --- a/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/DataSummaryControllerTest.java +++ b/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/DataSummaryControllerTest.java @@ -45,25 +45,41 @@ void canGetPrometheusMetrics() { assertEquals(HttpStatus.OK, response.status()); String content = response.content("text/plain"); assertFalse(content.isEmpty(), "Response content should not be empty"); - assertTrue( - content.contains("# HELP data_summary_system_info"), - "System information help text is missing"); - assertTrue(content.contains("data_summary_system_info"), "Build time metrics are missing"); assertTrue( content.contains("# HELP data_summary_active_users"), "Active users help text is missing"); - assertTrue(content.contains("data_summary_active_user"), "Active users metric is missing"); + assertTrue( + content.lines().anyMatch(line -> line.startsWith("data_summary_active_user")), + "Active users metric is missing"); + ; assertTrue( content.contains("# HELP data_summary_object_counts"), "Object counts help text is missing"); - assertTrue(content.contains("data_summary_object_counts"), "Object counts metric is missing"); + assertTrue( + content.lines().anyMatch(line -> line.startsWith("data_summary_object_counts")), + "Object counts metric is missing"); assertTrue( content.contains("# HELP data_summary_data_value_count"), "Data value count help text is missing"); assertTrue( - content.contains("data_summary_data_value_count"), "Data value count metric is missing"); + content.lines().anyMatch(line -> line.startsWith("data_summary_data_value_count")), + "Data value count metric is missing"); assertTrue( content.contains("# HELP data_summary_event_count"), "Event count help text is missing"); - assertTrue(content.contains("data_summary_event_count"), "Event count metric is missing"); + assertTrue( + content.lines().anyMatch(line -> line.startsWith("data_summary_event_count")), + "Event count metric is missing"); + assertTrue( + content.contains("# HELP data_summary_build_info"), "Build info help text is missing"); + // data_summary_build_info should end with an integer representing the build time in seconds + // since epoch + assertTrue( + content.lines().anyMatch(line -> line.matches("data_summary_build_info\\{.*\\} \\d+")), + "Build info metric should end with an integer"); + assertTrue(content.contains("# HELP data_summary_system_id"), "System ID help text is missing"); + // data_summary_system_id metric should be a static value of 1 + assertTrue( + content.lines().anyMatch(line -> line.matches("data_summary_system_id\\{.*\\} 1")), + "System ID metric should be a static value of 1"); } @Test diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/datasummary/DataSummaryController.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/datasummary/DataSummaryController.java index 5375869299e4..63eea24841c6 100644 --- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/datasummary/DataSummaryController.java +++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/datasummary/DataSummaryController.java @@ -112,22 +112,7 @@ public DataSummaryController(DataStatisticsService dataStatisticsService) { "Count of updated events by day", PROMETHEUS_GAUGE_NAME); - // The system information is presented just as a static gauge with value 1.0 - // The key is the field name and the value is the field value - metrics.helpLine("data_summary_system_info", "System information"); - metrics.typeLine("data_summary_system_info", "gauge"); - - if (summary.getSystem() != null) { - metrics.appendStaticKeyValue( - "data_summary_system_info", "version", summary.getSystem().getVersion()); - metrics.appendStaticKeyValue( - "data_summary_system_info", "revision", summary.getSystem().getRevision()); - metrics.appendStaticKeyValue( - "data_summary_system_info", "build_time", summary.getSystem().getBuildTime()); - metrics.appendStaticKeyValue( - "data_summary_system_info", "system_id", summary.getSystem().getSystemId()); - } - + metrics.appendSystemInfoMetrics(summary.getSystem()); return metrics.getMetrics(); } } diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/utils/PrometheusTextBuilder.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/utils/PrometheusTextBuilder.java index 39650441c469..9978ad85162b 100644 --- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/utils/PrometheusTextBuilder.java +++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/utils/PrometheusTextBuilder.java @@ -28,6 +28,7 @@ package org.hisp.dhis.webapi.utils; import java.util.Map; +import org.hisp.dhis.common.Dhis2Info; /** * A simple utility class to build Prometheus text format metrics. The Prometheus text format is @@ -74,23 +75,28 @@ public void updateMetricsFromMap( } /** - * Appends a static key-value pair to the Prometheus metrics. This is most useful for representing - * labels in the Prometheus text format such as build time, version, etc. This will produce a - * metric with a static value of 1. + * Appends system information metrics to the Prometheus metrics. * - * @param metricName the name of the metric - * @param key the key for the metric - * @param value the value for the metric + * @param systemInfo the system information containing version, commit, revision, and system ID */ - public void appendStaticKeyValue(String metricName, String key, String value) { - if (value != null) { - metrics.append(String.format("%s{key=\"%s\", value=\"%s\"} 1%n", metricName, key, value)); - } - } + public void appendSystemInfoMetrics(Dhis2Info systemInfo) { + String metricName = "data_summary_build_info"; + if (systemInfo != null) { + helpLine(metricName, "Build information"); + typeLine(metricName, "gauge"); + Long buildTime = + systemInfo.getBuildTime() != null + ? systemInfo.getBuildTime().toInstant().getEpochSecond() + : 0L; // Convert to seconds// Convert to seconds + metrics.append( + String.format( + "%s{version=\"%s\", commit=\"%s\"} %s%n", + metricName, systemInfo.getVersion(), systemInfo.getRevision(), buildTime)); - public void appendStaticKeyValue(String metricName, String key, java.util.Date value) { - if (value != null) { - metrics.append(String.format("%s{key=\"%s\", value=\"%s\"} 1%n", metricName, key, value)); + helpLine("data_summary_system_id", "System ID"); + typeLine("data_summary_system_id", "gauge"); + metrics.append( + String.format("data_summary_system_id{system_id=\"%s\"} 1%n", systemInfo.getSystemId())); } } diff --git a/dhis-2/dhis-web-api/src/test/java/org/hisp/dhis/webapi/utils/PrometheusTextBuilderTest.java b/dhis-2/dhis-web-api/src/test/java/org/hisp/dhis/webapi/utils/PrometheusTextBuilderTest.java index 7f09164751a7..7c6f62860713 100644 --- a/dhis-2/dhis-web-api/src/test/java/org/hisp/dhis/webapi/utils/PrometheusTextBuilderTest.java +++ b/dhis-2/dhis-web-api/src/test/java/org/hisp/dhis/webapi/utils/PrometheusTextBuilderTest.java @@ -31,6 +31,7 @@ import java.util.Date; import java.util.Map; +import org.hisp.dhis.common.Dhis2Info; import org.junit.jupiter.api.Test; /** @@ -74,17 +75,51 @@ void updateMetricsFromMapAppendsMetrics() { } @Test - void appendStaticKeyValueAppendsKeyValue() { + void appendSystemInfoMetrics() { + Date currentEpoch = new Date(); + Long currentEpochSeconds = currentEpoch.getTime() / 1000; PrometheusTextBuilder builder = new PrometheusTextBuilder(); - builder.appendStaticKeyValue("test_metric", "key", "value"); - assertEquals("test_metric{key=\"key\", value=\"value\"} 1\n", builder.getMetrics()); + Dhis2Info systemInfo = new Dhis2Info(); + systemInfo.setVersion("2.36.3"); + systemInfo.setRevision("rev1"); + systemInfo.setBuildTime(currentEpoch); + systemInfo.setSystemId("fake-uuid"); + + builder.appendSystemInfoMetrics(systemInfo); + + String expected = + """ + # HELP data_summary_build_info Build information + # TYPE data_summary_build_info gauge + data_summary_build_info{version="2.36.3", commit="rev1"} %d + # HELP data_summary_system_id System ID + # TYPE data_summary_system_id gauge + data_summary_system_id{system_id="fake-uuid"} 1 + """ + .formatted(currentEpochSeconds); + assertEquals(expected, builder.getMetrics()); } @Test - void appendStaticKeyValueIgnoresNullValue() { + void appendSystemInfoMetricsWithNullBuildTime() { + PrometheusTextBuilder builder = new PrometheusTextBuilder(); - builder.appendStaticKeyValue("test_metric", "key", (String) null); - builder.appendStaticKeyValue("test_metric", "key", (Date) null); - assertEquals("", builder.getMetrics()); + Dhis2Info systemInfo = new Dhis2Info(); + systemInfo.setVersion("2.36.3"); + systemInfo.setRevision("rev1"); + systemInfo.setSystemId("fake-uuid"); + + builder.appendSystemInfoMetrics(systemInfo); + + String expected = + """ + # HELP data_summary_build_info Build information + # TYPE data_summary_build_info gauge + data_summary_build_info{version="2.36.3", commit="rev1"} 0 + # HELP data_summary_system_id System ID + # TYPE data_summary_system_id gauge + data_summary_system_id{system_id="fake-uuid"} 1 + """; + assertEquals(expected, builder.getMetrics()); } } From b3db952297911f4d62eb3d9aff60e23edbe7162a Mon Sep 17 00:00:00 2001 From: Jason Pickering Date: Thu, 23 Jan 2025 09:42:06 +0100 Subject: [PATCH 23/26] Linting --- .../hisp/dhis/webapi/controller/DataSummaryControllerTest.java | 1 - 1 file changed, 1 deletion(-) diff --git a/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/DataSummaryControllerTest.java b/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/DataSummaryControllerTest.java index 9c3140cef65d..46ad9f1a6d13 100644 --- a/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/DataSummaryControllerTest.java +++ b/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/DataSummaryControllerTest.java @@ -50,7 +50,6 @@ void canGetPrometheusMetrics() { assertTrue( content.lines().anyMatch(line -> line.startsWith("data_summary_active_user")), "Active users metric is missing"); - ; assertTrue( content.contains("# HELP data_summary_object_counts"), "Object counts help text is missing"); From 3922b23857b6a3937e887bcb56837c4fde9c3280 Mon Sep 17 00:00:00 2001 From: Jason Pickering Date: Thu, 23 Jan 2025 10:02:22 +0100 Subject: [PATCH 24/26] Simplification --- .../org/hisp/dhis/webapi/utils/PrometheusTextBuilder.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/utils/PrometheusTextBuilder.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/utils/PrometheusTextBuilder.java index 9978ad85162b..3fc428df3eba 100644 --- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/utils/PrometheusTextBuilder.java +++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/utils/PrometheusTextBuilder.java @@ -84,10 +84,10 @@ public void appendSystemInfoMetrics(Dhis2Info systemInfo) { if (systemInfo != null) { helpLine(metricName, "Build information"); typeLine(metricName, "gauge"); - Long buildTime = - systemInfo.getBuildTime() != null - ? systemInfo.getBuildTime().toInstant().getEpochSecond() - : 0L; // Convert to seconds// Convert to seconds + Long buildTime = 0L; + if (systemInfo.getBuildTime() != null) { + buildTime = systemInfo.getBuildTime().toInstant().getEpochSecond(); + } metrics.append( String.format( "%s{version=\"%s\", commit=\"%s\"} %s%n", From 3e9c8280771bb54c5328a566d3b89dbf16006ee3 Mon Sep 17 00:00:00 2001 From: Jason Pickering Date: Thu, 23 Jan 2025 10:45:13 +0100 Subject: [PATCH 25/26] Minor --- .../java/org/hisp/dhis/webapi/utils/PrometheusTextBuilder.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/utils/PrometheusTextBuilder.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/utils/PrometheusTextBuilder.java index 3fc428df3eba..81ec06fca266 100644 --- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/utils/PrometheusTextBuilder.java +++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/utils/PrometheusTextBuilder.java @@ -84,7 +84,7 @@ public void appendSystemInfoMetrics(Dhis2Info systemInfo) { if (systemInfo != null) { helpLine(metricName, "Build information"); typeLine(metricName, "gauge"); - Long buildTime = 0L; + long buildTime = 0L; if (systemInfo.getBuildTime() != null) { buildTime = systemInfo.getBuildTime().toInstant().getEpochSecond(); } From 562bdbc1fbffea25914cb76f71c6d321f2312070 Mon Sep 17 00:00:00 2001 From: Jason Pickering Date: Thu, 23 Jan 2025 14:27:47 +0100 Subject: [PATCH 26/26] Move method into controller --- .../datasummary/DataSummaryController.java | 29 ++++++++++- .../webapi/utils/PrometheusTextBuilder.java | 35 ++++--------- .../utils/PrometheusTextBuilderTest.java | 51 ------------------- 3 files changed, 37 insertions(+), 78 deletions(-) diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/datasummary/DataSummaryController.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/datasummary/DataSummaryController.java index 63eea24841c6..9192ca213455 100644 --- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/datasummary/DataSummaryController.java +++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/datasummary/DataSummaryController.java @@ -31,6 +31,7 @@ import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; import static org.springframework.http.MediaType.TEXT_PLAIN_VALUE; +import org.hisp.dhis.common.Dhis2Info; import org.hisp.dhis.common.DhisApiVersion; import org.hisp.dhis.common.OpenApi; import org.hisp.dhis.datastatistics.DataStatisticsService; @@ -70,6 +71,32 @@ public DataSummaryController(DataStatisticsService dataStatisticsService) { return dataStatisticsService.getSystemStatisticsSummary(); } + /** + * Appends system information metrics to the Prometheus metrics. + * + * @param systemInfo the system information containing version, commit, revision, and system ID + */ + public void appendSystemInfoMetrics(PrometheusTextBuilder metrics, Dhis2Info systemInfo) { + String metricName = "data_summary_build_info"; + if (systemInfo != null) { + metrics.helpLine(metricName, "Build information"); + metrics.typeLine(metricName, "gauge"); + long buildTime = 0L; + if (systemInfo.getBuildTime() != null) { + buildTime = systemInfo.getBuildTime().toInstant().getEpochSecond(); + } + metrics.append( + String.format( + "%s{version=\"%s\", commit=\"%s\"} %s%n", + metricName, systemInfo.getVersion(), systemInfo.getRevision(), buildTime)); + + metrics.helpLine("data_summary_system_id", "System ID"); + metrics.typeLine("data_summary_system_id", "gauge"); + metrics.append( + String.format("data_summary_system_id{system_id=\"%s\"} 1%n", systemInfo.getSystemId())); + } + } + @GetMapping(value = "/metrics", produces = TEXT_PLAIN_VALUE) @RequiresAuthority(anyOf = F_PERFORM_MAINTENANCE) public @ResponseBody String getPrometheusMetrics() { @@ -112,7 +139,7 @@ public DataSummaryController(DataStatisticsService dataStatisticsService) { "Count of updated events by day", PROMETHEUS_GAUGE_NAME); - metrics.appendSystemInfoMetrics(summary.getSystem()); + appendSystemInfoMetrics(metrics, summary.getSystem()); return metrics.getMetrics(); } } diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/utils/PrometheusTextBuilder.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/utils/PrometheusTextBuilder.java index 81ec06fca266..d06a2917972f 100644 --- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/utils/PrometheusTextBuilder.java +++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/utils/PrometheusTextBuilder.java @@ -28,7 +28,6 @@ package org.hisp.dhis.webapi.utils; import java.util.Map; -import org.hisp.dhis.common.Dhis2Info; /** * A simple utility class to build Prometheus text format metrics. The Prometheus text format is @@ -75,37 +74,21 @@ public void updateMetricsFromMap( } /** - * Appends system information metrics to the Prometheus metrics. + * Returns the Prometheus metrics as a string. * - * @param systemInfo the system information containing version, commit, revision, and system ID + * @return the metrics in Prometheus text format */ - public void appendSystemInfoMetrics(Dhis2Info systemInfo) { - String metricName = "data_summary_build_info"; - if (systemInfo != null) { - helpLine(metricName, "Build information"); - typeLine(metricName, "gauge"); - long buildTime = 0L; - if (systemInfo.getBuildTime() != null) { - buildTime = systemInfo.getBuildTime().toInstant().getEpochSecond(); - } - metrics.append( - String.format( - "%s{version=\"%s\", commit=\"%s\"} %s%n", - metricName, systemInfo.getVersion(), systemInfo.getRevision(), buildTime)); - - helpLine("data_summary_system_id", "System ID"); - typeLine("data_summary_system_id", "gauge"); - metrics.append( - String.format("data_summary_system_id{system_id=\"%s\"} 1%n", systemInfo.getSystemId())); - } + public String getMetrics() { + return metrics.toString(); } /** - * Returns the Prometheus metrics as a string. + * Appends a formatted string to the Prometheus metrics. This is not checked for correctness, so + * be sure you know what you are doing before using this method. * - * @return the metrics in Prometheus text format + * @param format the formatted string to append */ - public String getMetrics() { - return metrics.toString(); + public void append(String format) { + metrics.append(format); } } diff --git a/dhis-2/dhis-web-api/src/test/java/org/hisp/dhis/webapi/utils/PrometheusTextBuilderTest.java b/dhis-2/dhis-web-api/src/test/java/org/hisp/dhis/webapi/utils/PrometheusTextBuilderTest.java index 7c6f62860713..c584e5290d09 100644 --- a/dhis-2/dhis-web-api/src/test/java/org/hisp/dhis/webapi/utils/PrometheusTextBuilderTest.java +++ b/dhis-2/dhis-web-api/src/test/java/org/hisp/dhis/webapi/utils/PrometheusTextBuilderTest.java @@ -29,9 +29,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; -import java.util.Date; import java.util.Map; -import org.hisp.dhis.common.Dhis2Info; import org.junit.jupiter.api.Test; /** @@ -73,53 +71,4 @@ void updateMetricsFromMapAppendsMetrics() { """; assertEquals(expected, builder.getMetrics()); } - - @Test - void appendSystemInfoMetrics() { - Date currentEpoch = new Date(); - Long currentEpochSeconds = currentEpoch.getTime() / 1000; - PrometheusTextBuilder builder = new PrometheusTextBuilder(); - Dhis2Info systemInfo = new Dhis2Info(); - systemInfo.setVersion("2.36.3"); - systemInfo.setRevision("rev1"); - systemInfo.setBuildTime(currentEpoch); - systemInfo.setSystemId("fake-uuid"); - - builder.appendSystemInfoMetrics(systemInfo); - - String expected = - """ - # HELP data_summary_build_info Build information - # TYPE data_summary_build_info gauge - data_summary_build_info{version="2.36.3", commit="rev1"} %d - # HELP data_summary_system_id System ID - # TYPE data_summary_system_id gauge - data_summary_system_id{system_id="fake-uuid"} 1 - """ - .formatted(currentEpochSeconds); - assertEquals(expected, builder.getMetrics()); - } - - @Test - void appendSystemInfoMetricsWithNullBuildTime() { - - PrometheusTextBuilder builder = new PrometheusTextBuilder(); - Dhis2Info systemInfo = new Dhis2Info(); - systemInfo.setVersion("2.36.3"); - systemInfo.setRevision("rev1"); - systemInfo.setSystemId("fake-uuid"); - - builder.appendSystemInfoMetrics(systemInfo); - - String expected = - """ - # HELP data_summary_build_info Build information - # TYPE data_summary_build_info gauge - data_summary_build_info{version="2.36.3", commit="rev1"} 0 - # HELP data_summary_system_id System ID - # TYPE data_summary_system_id gauge - data_summary_system_id{system_id="fake-uuid"} 1 - """; - assertEquals(expected, builder.getMetrics()); - } }