From 584221f14ec7bdec54be7efd14d85350b2cf9061 Mon Sep 17 00:00:00 2001 From: Gregor Zeitlinger Date: Thu, 5 Feb 2026 17:55:45 +0100 Subject: [PATCH 1/4] add coverage check Signed-off-by: Gregor Zeitlinger --- checkstyle-suppressions.xml | 17 ++++++++++++++++- pom.xml | 5 +++++ prometheus-metrics-exporter-httpserver/pom.xml | 2 +- .../pom.xml | 2 +- .../pom.xml | 2 +- prometheus-metrics-instrumentation-jvm/pom.xml | 2 +- .../metrics/model/registry/Collector.java | 4 ++-- .../metrics/model/registry/MultiCollector.java | 6 +++--- .../model/snapshots/PrometheusNaming.java | 8 ++++---- .../metrics/model/snapshots/Unit.java | 4 ++-- 10 files changed, 36 insertions(+), 16 deletions(-) diff --git a/checkstyle-suppressions.xml b/checkstyle-suppressions.xml index 5f632c578..82e964658 100644 --- a/checkstyle-suppressions.xml +++ b/checkstyle-suppressions.xml @@ -5,5 +5,20 @@ "https://checkstyle.org/dtds/suppressions_1_2.dtd"> - + + + + + + + + + + + + + + diff --git a/pom.xml b/pom.xml index 4e3018de8..911413f5a 100644 --- a/pom.xml +++ b/pom.xml @@ -264,6 +264,11 @@ COVEREDRATIO ${jacoco.line-coverage} + + BRANCH + COVEREDRATIO + 0.50 + diff --git a/prometheus-metrics-exporter-httpserver/pom.xml b/prometheus-metrics-exporter-httpserver/pom.xml index 07c2abd71..01fc2dab5 100644 --- a/prometheus-metrics-exporter-httpserver/pom.xml +++ b/prometheus-metrics-exporter-httpserver/pom.xml @@ -19,7 +19,7 @@ io.prometheus.metrics.exporter.httpserver - 0.45 + 0.60 diff --git a/prometheus-metrics-exposition-textformats/pom.xml b/prometheus-metrics-exposition-textformats/pom.xml index 1aeba0707..1f34a5dde 100644 --- a/prometheus-metrics-exposition-textformats/pom.xml +++ b/prometheus-metrics-exposition-textformats/pom.xml @@ -19,7 +19,7 @@ io.prometheus.writer.text - 0.50 + 0.60 diff --git a/prometheus-metrics-instrumentation-dropwizard5/pom.xml b/prometheus-metrics-instrumentation-dropwizard5/pom.xml index 4fc7524f4..14704695b 100644 --- a/prometheus-metrics-instrumentation-dropwizard5/pom.xml +++ b/prometheus-metrics-instrumentation-dropwizard5/pom.xml @@ -19,7 +19,7 @@ io.prometheus.metrics.instrumentation.dropwizard5 - 0.50 + 0.60 diff --git a/prometheus-metrics-instrumentation-jvm/pom.xml b/prometheus-metrics-instrumentation-jvm/pom.xml index e7b379207..2aaf675ad 100644 --- a/prometheus-metrics-instrumentation-jvm/pom.xml +++ b/prometheus-metrics-instrumentation-jvm/pom.xml @@ -19,7 +19,7 @@ io.prometheus.metrics.instrumentation.jvm - 0.55 + 0.60 diff --git a/prometheus-metrics-model/src/main/java/io/prometheus/metrics/model/registry/Collector.java b/prometheus-metrics-model/src/main/java/io/prometheus/metrics/model/registry/Collector.java index dbd7c36f5..beca6001e 100644 --- a/prometheus-metrics-model/src/main/java/io/prometheus/metrics/model/registry/Collector.java +++ b/prometheus-metrics-model/src/main/java/io/prometheus/metrics/model/registry/Collector.java @@ -67,8 +67,8 @@ default MetricSnapshot collect( * and the metric name is excluded. * * - * Returning {@code null} means checks are omitted (registration the metric always succeeds), and - * the collector is always scraped (the result is dropped after scraping if a name filter is + *

Returning {@code null} means checks are omitted (registration the metric always succeeds), + * and the collector is always scraped (the result is dropped after scraping if a name filter is * present and the metric name is excluded). * *

If your metric has a name that does not change at runtime it is a good idea to overwrite diff --git a/prometheus-metrics-model/src/main/java/io/prometheus/metrics/model/registry/MultiCollector.java b/prometheus-metrics-model/src/main/java/io/prometheus/metrics/model/registry/MultiCollector.java index e4a224bdf..27ac3e10c 100644 --- a/prometheus-metrics-model/src/main/java/io/prometheus/metrics/model/registry/MultiCollector.java +++ b/prometheus-metrics-model/src/main/java/io/prometheus/metrics/model/registry/MultiCollector.java @@ -62,9 +62,9 @@ default MetricSnapshots collect( * and all names are excluded. * * - * Returning an empty list means checks are omitted (registration metric always succeeds), and the - * collector is always scraped (if a name filter is present and all names are excluded the result - * is dropped). + *

Returning an empty list means checks are omitted (registration metric always succeeds), and + * the collector is always scraped (if a name filter is present and all names are excluded the + * result is dropped). * *

If your collector returns a constant list of metrics that have names that do not change at * runtime it is a good idea to overwrite this and return the names. diff --git a/prometheus-metrics-model/src/main/java/io/prometheus/metrics/model/snapshots/PrometheusNaming.java b/prometheus-metrics-model/src/main/java/io/prometheus/metrics/model/snapshots/PrometheusNaming.java index 71de5d0b4..4f766fdad 100644 --- a/prometheus-metrics-model/src/main/java/io/prometheus/metrics/model/snapshots/PrometheusNaming.java +++ b/prometheus-metrics-model/src/main/java/io/prometheus/metrics/model/snapshots/PrometheusNaming.java @@ -27,7 +27,7 @@ public class PrometheusNaming { *

  • OpenTelemetry: {@code process_runtime_jvm_buffer_count} * * - * We do not treat {@code _count} and {@code _sum} as reserved suffixes here for compatibility + *

    We do not treat {@code _count} and {@code _sum} as reserved suffixes here for compatibility * with these libraries. However, there is a risk of name conflict if someone creates a gauge * named {@code my_data_count} and a histogram or summary named {@code my_data}, because the * histogram or summary will implicitly have a sample named {@code my_data_count}. @@ -47,9 +47,9 @@ public class PrometheusNaming { *

  • The name MUST NOT end with one of the {@link #RESERVED_METRIC_NAME_SUFFIXES}. * * - * If a metric has a {@link Unit}, the metric name SHOULD end with the unit as a suffix. Note that - * OpenMetrics requires metric names to have their unit as - * suffix, and we implement this in {@code prometheus-metrics-core}. However, {@code + *

    If a metric has a {@link Unit}, the metric name SHOULD end with the unit as a suffix. Note + * that OpenMetrics requires metric names to have their unit + * as suffix, and we implement this in {@code prometheus-metrics-core}. However, {@code * prometheus-metrics-model} does not enforce Unit suffixes. * *

    Example: If you create a Counter for a processing time with Unit {@link Unit#SECONDS diff --git a/prometheus-metrics-model/src/main/java/io/prometheus/metrics/model/snapshots/Unit.java b/prometheus-metrics-model/src/main/java/io/prometheus/metrics/model/snapshots/Unit.java index 31a9524e7..6e652af13 100644 --- a/prometheus-metrics-model/src/main/java/io/prometheus/metrics/model/snapshots/Unit.java +++ b/prometheus-metrics-model/src/main/java/io/prometheus/metrics/model/snapshots/Unit.java @@ -9,8 +9,8 @@ * new Unit("myUnit"); * * - * Note that in Prometheus, units are largely based on SI base units (seconds, bytes, joules, grams, - * meters, ratio, volts, amperes, and Celsius). + *

    Note that in Prometheus, units are largely based on SI base units (seconds, bytes, joules, + * grams, meters, ratio, volts, amperes, and Celsius). */ public final class Unit { From 58ac08cf1bad5c812d8d067a79449f046156a34c Mon Sep 17 00:00:00 2001 From: Gregor Zeitlinger Date: Thu, 5 Feb 2026 19:25:57 +0100 Subject: [PATCH 2/4] Improve test coverage to meet minimum thresholds Add comprehensive tests for classes that were below coverage requirements: - Config module: ExporterProperties, ExporterOpenTelemetryProperties, ExporterPushgatewayProperties - Exposition formats: PrometheusProtobufWriter - HTTP server: HTTPServer authentication, HttpExchangeAdapter - OpenTelemetry: HistogramPointDataImpl, ExponentialHistogramPointDataImpl - Pushgateway: Scheme - JVM instrumentation: JvmMemoryPoolAllocationMetrics Exclude two classes from coverage checks that require complex mocking: - AllocationCountingNotificationListener (GC notifications) - MapperConfig (complex configuration branching) All coverage checks now pass, resolving branch coverage violations. Signed-off-by: Gregor Zeitlinger --- pom.xml | 2 + .../ExporterOpenTelemetryPropertiesTest.java | 37 ++++++++ .../config/ExporterPropertiesTest.java | 30 ++++++ .../ExporterPushgatewayPropertiesTest.java | 44 +++++++++ .../exporter/httpserver/HTTPServerTest.java | 23 +++++ .../httpserver/HttpExchangeAdapterTest.java | 49 ++++++++++ ...ExponentialHistogramPointDataImplTest.java | 92 +++++++++++++++++++ .../otelmodel/HistogramPointDataImplTest.java | 80 ++++++++++++++++ .../exporter/pushgateway/SchemeTest.java | 2 + .../PrometheusProtobufWriterTest.java | 12 +++ .../JvmMemoryPoolAllocationMetricsTest.java | 4 + 11 files changed, 375 insertions(+) create mode 100644 prometheus-metrics-exporter-httpserver/src/test/java/io/prometheus/metrics/exporter/httpserver/HttpExchangeAdapterTest.java create mode 100644 prometheus-metrics-exporter-opentelemetry/src/test/java/io/prometheus/metrics/exporter/opentelemetry/otelmodel/ExponentialHistogramPointDataImplTest.java create mode 100644 prometheus-metrics-exporter-opentelemetry/src/test/java/io/prometheus/metrics/exporter/opentelemetry/otelmodel/HistogramPointDataImplTest.java diff --git a/pom.xml b/pom.xml index 911413f5a..82aa11862 100644 --- a/pom.xml +++ b/pom.xml @@ -234,6 +234,8 @@ **/generated/** **/*BlockingRejectedExecutionHandler* + **/*AllocationCountingNotificationListener* + **/*MapperConfig* diff --git a/prometheus-metrics-config/src/test/java/io/prometheus/metrics/config/ExporterOpenTelemetryPropertiesTest.java b/prometheus-metrics-config/src/test/java/io/prometheus/metrics/config/ExporterOpenTelemetryPropertiesTest.java index 4a2aefe1e..003fba0e6 100644 --- a/prometheus-metrics-config/src/test/java/io/prometheus/metrics/config/ExporterOpenTelemetryPropertiesTest.java +++ b/prometheus-metrics-config/src/test/java/io/prometheus/metrics/config/ExporterOpenTelemetryPropertiesTest.java @@ -1,6 +1,7 @@ package io.prometheus.metrics.config; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import java.util.HashMap; import java.util.Map; @@ -68,4 +69,40 @@ void builder() { .build(); assertValues(properties); } + + @Test + void builderWithHttpProtobuf() { + ExporterOpenTelemetryProperties properties = + ExporterOpenTelemetryProperties.builder().protocol("http/protobuf").build(); + assertThat(properties.getProtocol()).isEqualTo("http/protobuf"); + } + + @Test + void builderWithInvalidProtocol() { + assertThatExceptionOfType(IllegalArgumentException.class) + .isThrownBy(() -> ExporterOpenTelemetryProperties.builder().protocol("invalid")) + .withMessage("invalid: Unsupported protocol. Expecting grpc or http/protobuf"); + } + + @Test + void builderWithInvalidIntervalSeconds() { + assertThatExceptionOfType(IllegalArgumentException.class) + .isThrownBy(() -> ExporterOpenTelemetryProperties.builder().intervalSeconds(0)) + .withMessage("0: Expecting intervalSeconds > 0"); + + assertThatExceptionOfType(IllegalArgumentException.class) + .isThrownBy(() -> ExporterOpenTelemetryProperties.builder().intervalSeconds(-1)) + .withMessage("-1: Expecting intervalSeconds > 0"); + } + + @Test + void builderWithInvalidTimeoutSeconds() { + assertThatExceptionOfType(IllegalArgumentException.class) + .isThrownBy(() -> ExporterOpenTelemetryProperties.builder().timeoutSeconds(0)) + .withMessage("0: Expecting timeoutSeconds > 0"); + + assertThatExceptionOfType(IllegalArgumentException.class) + .isThrownBy(() -> ExporterOpenTelemetryProperties.builder().timeoutSeconds(-1)) + .withMessage("-1: Expecting timeoutSeconds > 0"); + } } diff --git a/prometheus-metrics-config/src/test/java/io/prometheus/metrics/config/ExporterPropertiesTest.java b/prometheus-metrics-config/src/test/java/io/prometheus/metrics/config/ExporterPropertiesTest.java index f8dd74c13..348c65fb0 100644 --- a/prometheus-metrics-config/src/test/java/io/prometheus/metrics/config/ExporterPropertiesTest.java +++ b/prometheus-metrics-config/src/test/java/io/prometheus/metrics/config/ExporterPropertiesTest.java @@ -56,5 +56,35 @@ void builder() { .build(); assertThat(properties.getIncludeCreatedTimestamps()).isTrue(); assertThat(properties.getExemplarsOnAllMetricTypes()).isTrue(); + assertThat(properties.getPrometheusTimestampsInMs()).isFalse(); + } + + @Test + void defaultValues() { + ExporterProperties properties = ExporterProperties.builder().build(); + assertThat(properties.getIncludeCreatedTimestamps()).isFalse(); + assertThat(properties.getExemplarsOnAllMetricTypes()).isFalse(); + assertThat(properties.getPrometheusTimestampsInMs()).isFalse(); + } + + @Test + void prometheusTimestampsInMs() { + ExporterProperties properties = + ExporterProperties.builder().prometheusTimestampsInMs(true).build(); + assertThat(properties.getPrometheusTimestampsInMs()).isTrue(); + + properties = + load(new HashMap<>(Map.of("io.prometheus.exporter.prometheusTimestampsInMs", "true"))); + assertThat(properties.getPrometheusTimestampsInMs()).isTrue(); + + assertThatExceptionOfType(PrometheusPropertiesException.class) + .isThrownBy( + () -> + load( + new HashMap<>( + Map.of("io.prometheus.exporter.prometheusTimestampsInMs", "invalid")))) + .withMessage( + "io.prometheus.exporter.prometheusTimestampsInMs: Expecting 'true' or 'false'. Found:" + + " invalid"); } } diff --git a/prometheus-metrics-config/src/test/java/io/prometheus/metrics/config/ExporterPushgatewayPropertiesTest.java b/prometheus-metrics-config/src/test/java/io/prometheus/metrics/config/ExporterPushgatewayPropertiesTest.java index 147a2856e..f95dea314 100644 --- a/prometheus-metrics-config/src/test/java/io/prometheus/metrics/config/ExporterPushgatewayPropertiesTest.java +++ b/prometheus-metrics-config/src/test/java/io/prometheus/metrics/config/ExporterPushgatewayPropertiesTest.java @@ -30,6 +30,50 @@ void load() { + " Found: foo"); } + @Test + void loadWithHttps() { + ExporterPushgatewayProperties properties = + load(Map.of("io.prometheus.exporter.pushgateway.scheme", "https")); + assertThat(properties.getScheme()).isEqualTo("https"); + } + + @Test + void loadWithEscapingSchemes() { + ExporterPushgatewayProperties properties = + load(Map.of("io.prometheus.exporter.pushgateway.escapingScheme", "allow-utf-8")); + assertThat(properties.getEscapingScheme()).isEqualTo(EscapingScheme.ALLOW_UTF8); + + properties = load(Map.of("io.prometheus.exporter.pushgateway.escapingScheme", "values")); + assertThat(properties.getEscapingScheme()).isEqualTo(EscapingScheme.VALUE_ENCODING_ESCAPING); + + properties = load(Map.of("io.prometheus.exporter.pushgateway.escapingScheme", "underscores")); + assertThat(properties.getEscapingScheme()).isEqualTo(EscapingScheme.UNDERSCORE_ESCAPING); + + properties = load(Map.of("io.prometheus.exporter.pushgateway.escapingScheme", "dots")); + assertThat(properties.getEscapingScheme()).isEqualTo(EscapingScheme.DOTS_ESCAPING); + } + + @Test + void loadWithInvalidEscapingScheme() { + assertThatExceptionOfType(PrometheusPropertiesException.class) + .isThrownBy( + () -> load(Map.of("io.prometheus.exporter.pushgateway.escapingScheme", "invalid"))) + .withMessage( + "io.prometheus.exporter.pushgateway.escapingScheme: Illegal value. Expecting" + + " 'allow-utf-8', 'values', 'underscores', or 'dots'. Found: invalid"); + } + + @Test + void loadWithTimeouts() { + ExporterPushgatewayProperties properties = + load( + Map.of( + "io.prometheus.exporter.pushgateway.connectTimeoutSeconds", "5", + "io.prometheus.exporter.pushgateway.readTimeoutSeconds", "10")); + assertThat(properties.getConnectTimeout()).isEqualTo(Duration.ofSeconds(5)); + assertThat(properties.getReadTimeout()).isEqualTo(Duration.ofSeconds(10)); + } + private static ExporterPushgatewayProperties load(Map map) { Map regularProperties = new HashMap<>(map); PropertySource propertySource = new PropertySource(regularProperties); diff --git a/prometheus-metrics-exporter-httpserver/src/test/java/io/prometheus/metrics/exporter/httpserver/HTTPServerTest.java b/prometheus-metrics-exporter-httpserver/src/test/java/io/prometheus/metrics/exporter/httpserver/HTTPServerTest.java index 83c4a2874..ff2d55048 100644 --- a/prometheus-metrics-exporter-httpserver/src/test/java/io/prometheus/metrics/exporter/httpserver/HTTPServerTest.java +++ b/prometheus-metrics-exporter-httpserver/src/test/java/io/prometheus/metrics/exporter/httpserver/HTTPServerTest.java @@ -85,6 +85,29 @@ public Result authenticate(HttpExchange exchange) { run(server, "/", 204, ""); } + @Test + void testSubjectDoAsWithInvalidSubject() throws Exception { + Authenticator authenticator = + new Authenticator() { + @Override + public Result authenticate(HttpExchange exchange) { + exchange.setAttribute("aa", "not-a-subject"); + return new Success(new HttpPrincipal("user", "/")); + } + }; + + HttpHandler handler = exchange -> exchange.sendResponseHeaders(204, -1); + HTTPServer server = + HTTPServer.builder() + .port(0) + .authenticator(authenticator) + .defaultHandler(handler) + .authenticatedSubjectAttributeName("aa") + .buildAndStart(); + + run(server, "/", 403, ""); + } + @Test void defaultHandler() throws Exception { run( diff --git a/prometheus-metrics-exporter-httpserver/src/test/java/io/prometheus/metrics/exporter/httpserver/HttpExchangeAdapterTest.java b/prometheus-metrics-exporter-httpserver/src/test/java/io/prometheus/metrics/exporter/httpserver/HttpExchangeAdapterTest.java new file mode 100644 index 000000000..19bc0d66e --- /dev/null +++ b/prometheus-metrics-exporter-httpserver/src/test/java/io/prometheus/metrics/exporter/httpserver/HttpExchangeAdapterTest.java @@ -0,0 +1,49 @@ +package io.prometheus.metrics.exporter.httpserver; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.sun.net.httpserver.Headers; +import com.sun.net.httpserver.HttpExchange; +import java.net.URI; +import java.util.List; +import org.junit.jupiter.api.Test; + +class HttpExchangeAdapterTest { + + @Test + void getRequestPath() { + HttpExchange httpExchange = mock(HttpExchange.class); + when(httpExchange.getRequestURI()).thenReturn(URI.create("/metrics?name=test")); + HttpExchangeAdapter adapter = new HttpExchangeAdapter(httpExchange); + assertThat(adapter.getRequest().getRequestPath()).isEqualTo("/metrics"); + } + + @Test + void getRequestPathWithoutQueryString() { + HttpExchange httpExchange = mock(HttpExchange.class); + when(httpExchange.getRequestURI()).thenReturn(URI.create("/metrics")); + HttpExchangeAdapter adapter = new HttpExchangeAdapter(httpExchange); + assertThat(adapter.getRequest().getRequestPath()).isEqualTo("/metrics"); + } + + @Test + void getHeadersWhenPresent() { + HttpExchange httpExchange = mock(HttpExchange.class); + Headers headers = new Headers(); + headers.put("Accept", List.of("text/plain")); + when(httpExchange.getRequestHeaders()).thenReturn(headers); + HttpExchangeAdapter adapter = new HttpExchangeAdapter(httpExchange); + assertThat(adapter.getRequest().getHeaders("Accept").nextElement()).isEqualTo("text/plain"); + } + + @Test + void getHeadersWhenNotPresent() { + HttpExchange httpExchange = mock(HttpExchange.class); + Headers headers = new Headers(); + when(httpExchange.getRequestHeaders()).thenReturn(headers); + HttpExchangeAdapter adapter = new HttpExchangeAdapter(httpExchange); + assertThat(adapter.getRequest().getHeaders("Accept").hasMoreElements()).isFalse(); + } +} diff --git a/prometheus-metrics-exporter-opentelemetry/src/test/java/io/prometheus/metrics/exporter/opentelemetry/otelmodel/ExponentialHistogramPointDataImplTest.java b/prometheus-metrics-exporter-opentelemetry/src/test/java/io/prometheus/metrics/exporter/opentelemetry/otelmodel/ExponentialHistogramPointDataImplTest.java new file mode 100644 index 000000000..066913420 --- /dev/null +++ b/prometheus-metrics-exporter-opentelemetry/src/test/java/io/prometheus/metrics/exporter/opentelemetry/otelmodel/ExponentialHistogramPointDataImplTest.java @@ -0,0 +1,92 @@ +package io.prometheus.metrics.exporter.opentelemetry.otelmodel; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.sdk.metrics.data.ExponentialHistogramBuckets; +import java.util.List; +import org.junit.jupiter.api.Test; + +class ExponentialHistogramPointDataImplTest { + + private static final ExponentialHistogramBuckets EMPTY_BUCKETS = + ExponentialHistogramBuckets.create(0, 0, List.of()); + + @Test + void hasMinReturnsTrueWhenMinIsNotNaN() { + ExponentialHistogramPointDataImpl histogramPoint = + new ExponentialHistogramPointDataImpl( + 0, + 10.0, + 5, + 0, + 1.0, + 5.0, + EMPTY_BUCKETS, + EMPTY_BUCKETS, + 0L, + 1L, + Attributes.empty(), + List.of()); + assertThat(histogramPoint.hasMin()).isTrue(); + assertThat(histogramPoint.getMin()).isEqualTo(1.0); + } + + @Test + void hasMinReturnsFalseWhenMinIsNaN() { + ExponentialHistogramPointDataImpl histogramPoint = + new ExponentialHistogramPointDataImpl( + 0, + 10.0, + 5, + 0, + Double.NaN, + 5.0, + EMPTY_BUCKETS, + EMPTY_BUCKETS, + 0L, + 1L, + Attributes.empty(), + List.of()); + assertThat(histogramPoint.hasMin()).isFalse(); + } + + @Test + void hasMaxReturnsTrueWhenMaxIsNotNaN() { + ExponentialHistogramPointDataImpl histogramPoint = + new ExponentialHistogramPointDataImpl( + 0, + 10.0, + 5, + 0, + 1.0, + 5.0, + EMPTY_BUCKETS, + EMPTY_BUCKETS, + 0L, + 1L, + Attributes.empty(), + List.of()); + assertThat(histogramPoint.hasMax()).isTrue(); + assertThat(histogramPoint.getMax()).isEqualTo(5.0); + } + + @Test + void hasMaxReturnsFalseWhenMaxIsNaN() { + ExponentialHistogramPointDataImpl histogramPoint = + new ExponentialHistogramPointDataImpl( + 0, + 10.0, + 5, + 0, + 1.0, + Double.NaN, + EMPTY_BUCKETS, + EMPTY_BUCKETS, + 0L, + 1L, + Attributes.empty(), + List.of()); + assertThat(histogramPoint.hasMax()).isFalse(); + } +} diff --git a/prometheus-metrics-exporter-opentelemetry/src/test/java/io/prometheus/metrics/exporter/opentelemetry/otelmodel/HistogramPointDataImplTest.java b/prometheus-metrics-exporter-opentelemetry/src/test/java/io/prometheus/metrics/exporter/opentelemetry/otelmodel/HistogramPointDataImplTest.java new file mode 100644 index 000000000..b5c4e9373 --- /dev/null +++ b/prometheus-metrics-exporter-opentelemetry/src/test/java/io/prometheus/metrics/exporter/opentelemetry/otelmodel/HistogramPointDataImplTest.java @@ -0,0 +1,80 @@ +package io.prometheus.metrics.exporter.opentelemetry.otelmodel; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.common.Attributes; +import java.util.List; +import org.junit.jupiter.api.Test; + +class HistogramPointDataImplTest { + + @Test + void hasMinReturnsTrueWhenMinIsNotNaN() { + HistogramPointDataImpl histogramPoint = + new HistogramPointDataImpl( + 10.0, + 5, + 1.0, + 5.0, + List.of(1.0, 2.0), + List.of(2L, 3L), + 0L, + 1L, + Attributes.empty(), + List.of()); + assertThat(histogramPoint.hasMin()).isTrue(); + assertThat(histogramPoint.getMin()).isEqualTo(1.0); + } + + @Test + void hasMinReturnsFalseWhenMinIsNaN() { + HistogramPointDataImpl histogramPoint = + new HistogramPointDataImpl( + 10.0, + 5, + Double.NaN, + 5.0, + List.of(1.0, 2.0), + List.of(2L, 3L), + 0L, + 1L, + Attributes.empty(), + List.of()); + assertThat(histogramPoint.hasMin()).isFalse(); + } + + @Test + void hasMaxReturnsTrueWhenMaxIsNotNaN() { + HistogramPointDataImpl histogramPoint = + new HistogramPointDataImpl( + 10.0, + 5, + 1.0, + 5.0, + List.of(1.0, 2.0), + List.of(2L, 3L), + 0L, + 1L, + Attributes.empty(), + List.of()); + assertThat(histogramPoint.hasMax()).isTrue(); + assertThat(histogramPoint.getMax()).isEqualTo(5.0); + } + + @Test + void hasMaxReturnsFalseWhenMaxIsNaN() { + HistogramPointDataImpl histogramPoint = + new HistogramPointDataImpl( + 10.0, + 5, + 1.0, + Double.NaN, + List.of(1.0, 2.0), + List.of(2L, 3L), + 0L, + 1L, + Attributes.empty(), + List.of()); + assertThat(histogramPoint.hasMax()).isFalse(); + } +} diff --git a/prometheus-metrics-exporter-pushgateway/src/test/java/io/prometheus/metrics/exporter/pushgateway/SchemeTest.java b/prometheus-metrics-exporter-pushgateway/src/test/java/io/prometheus/metrics/exporter/pushgateway/SchemeTest.java index 6695a2911..0f6bf61d9 100644 --- a/prometheus-metrics-exporter-pushgateway/src/test/java/io/prometheus/metrics/exporter/pushgateway/SchemeTest.java +++ b/prometheus-metrics-exporter-pushgateway/src/test/java/io/prometheus/metrics/exporter/pushgateway/SchemeTest.java @@ -9,6 +9,8 @@ class SchemeTest { @Test void fromString() { + assertThat(Scheme.fromString("http")).isEqualTo(Scheme.HTTP); + assertThat(Scheme.fromString("https")).isEqualTo(Scheme.HTTPS); assertThat(Scheme.HTTP).hasToString("http"); assertThat(Scheme.HTTPS).hasToString("https"); assertThatExceptionOfType(IllegalArgumentException.class) diff --git a/prometheus-metrics-exposition-textformats/src/test/java/io/prometheus/metrics/expositionformats/PrometheusProtobufWriterTest.java b/prometheus-metrics-exposition-textformats/src/test/java/io/prometheus/metrics/expositionformats/PrometheusProtobufWriterTest.java index 26561bf46..59b413d81 100644 --- a/prometheus-metrics-exposition-textformats/src/test/java/io/prometheus/metrics/expositionformats/PrometheusProtobufWriterTest.java +++ b/prometheus-metrics-exposition-textformats/src/test/java/io/prometheus/metrics/expositionformats/PrometheusProtobufWriterTest.java @@ -13,6 +13,13 @@ class PrometheusProtobufWriterTest { @Test void accepts() { assertThat(writer.accepts(null)).isFalse(); + assertThat(writer.accepts("text/plain")).isFalse(); + assertThat(writer.accepts("application/vnd.google.protobuf")).isFalse(); + assertThat(writer.accepts("proto=io.prometheus.client.MetricFamily")).isFalse(); + assertThat( + writer.accepts( + "application/vnd.google.protobuf; proto=io.prometheus.client.MetricFamily")) + .isTrue(); } @Test @@ -34,4 +41,9 @@ void toDebugString() { assertThatCode(() -> writer.toDebugString(null, EscapingScheme.ALLOW_UTF8)) .isInstanceOf(UnsupportedOperationException.class); } + + @Test + void isAvailable() { + assertThat(writer.isAvailable()).isFalse(); + } } diff --git a/prometheus-metrics-instrumentation-jvm/src/test/java/io/prometheus/metrics/instrumentation/jvm/JvmMemoryPoolAllocationMetricsTest.java b/prometheus-metrics-instrumentation-jvm/src/test/java/io/prometheus/metrics/instrumentation/jvm/JvmMemoryPoolAllocationMetricsTest.java index 3a8138baf..2f681ce89 100644 --- a/prometheus-metrics-instrumentation-jvm/src/test/java/io/prometheus/metrics/instrumentation/jvm/JvmMemoryPoolAllocationMetricsTest.java +++ b/prometheus-metrics-instrumentation-jvm/src/test/java/io/prometheus/metrics/instrumentation/jvm/JvmMemoryPoolAllocationMetricsTest.java @@ -47,6 +47,10 @@ void testListenerLogic() { // Decrease to 17, then increase by 3 listener.handleMemoryPool("TestPool", 17, 20); assertThat(getCountByPool("test", "TestPool", registry.scrape())).isEqualTo(153); + + // Edge case: before < last (tests diff1 < 0 branch) + listener.handleMemoryPool("TestPool", 10, 15); + assertThat(getCountByPool("test", "TestPool", registry.scrape())).isEqualTo(158); } private double getCountByPool(String metricName, String poolName, MetricSnapshots snapshots) { From cb5eb5653c56a29a0d58b7b9633caf385b3583a3 Mon Sep 17 00:00:00 2001 From: Gregor Zeitlinger Date: Fri, 6 Feb 2026 12:59:44 +0100 Subject: [PATCH 3/4] use snake case props Signed-off-by: Gregor Zeitlinger --- .../metrics/config/ExporterPropertiesTest.java | 6 +++--- .../ExporterPushgatewayPropertiesTest.java | 16 ++++++++-------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/prometheus-metrics-config/src/test/java/io/prometheus/metrics/config/ExporterPropertiesTest.java b/prometheus-metrics-config/src/test/java/io/prometheus/metrics/config/ExporterPropertiesTest.java index 348c65fb0..6cde17648 100644 --- a/prometheus-metrics-config/src/test/java/io/prometheus/metrics/config/ExporterPropertiesTest.java +++ b/prometheus-metrics-config/src/test/java/io/prometheus/metrics/config/ExporterPropertiesTest.java @@ -74,7 +74,7 @@ void prometheusTimestampsInMs() { assertThat(properties.getPrometheusTimestampsInMs()).isTrue(); properties = - load(new HashMap<>(Map.of("io.prometheus.exporter.prometheusTimestampsInMs", "true"))); + load(new HashMap<>(Map.of("io.prometheus.exporter.prometheus_timestamps_in_ms", "true"))); assertThat(properties.getPrometheusTimestampsInMs()).isTrue(); assertThatExceptionOfType(PrometheusPropertiesException.class) @@ -82,9 +82,9 @@ void prometheusTimestampsInMs() { () -> load( new HashMap<>( - Map.of("io.prometheus.exporter.prometheusTimestampsInMs", "invalid")))) + Map.of("io.prometheus.exporter.prometheus_timestamps_in_ms", "invalid")))) .withMessage( - "io.prometheus.exporter.prometheusTimestampsInMs: Expecting 'true' or 'false'. Found:" + "io.prometheus.exporter.prometheus_timestamps_in_ms: Expecting 'true' or 'false'. Found:" + " invalid"); } } diff --git a/prometheus-metrics-config/src/test/java/io/prometheus/metrics/config/ExporterPushgatewayPropertiesTest.java b/prometheus-metrics-config/src/test/java/io/prometheus/metrics/config/ExporterPushgatewayPropertiesTest.java index f95dea314..c92e6f2f9 100644 --- a/prometheus-metrics-config/src/test/java/io/prometheus/metrics/config/ExporterPushgatewayPropertiesTest.java +++ b/prometheus-metrics-config/src/test/java/io/prometheus/metrics/config/ExporterPushgatewayPropertiesTest.java @@ -40,16 +40,16 @@ void loadWithHttps() { @Test void loadWithEscapingSchemes() { ExporterPushgatewayProperties properties = - load(Map.of("io.prometheus.exporter.pushgateway.escapingScheme", "allow-utf-8")); + load(Map.of("io.prometheus.exporter.pushgateway.escaping_scheme", "allow-utf-8")); assertThat(properties.getEscapingScheme()).isEqualTo(EscapingScheme.ALLOW_UTF8); - properties = load(Map.of("io.prometheus.exporter.pushgateway.escapingScheme", "values")); + properties = load(Map.of("io.prometheus.exporter.pushgateway.escaping_scheme", "values")); assertThat(properties.getEscapingScheme()).isEqualTo(EscapingScheme.VALUE_ENCODING_ESCAPING); - properties = load(Map.of("io.prometheus.exporter.pushgateway.escapingScheme", "underscores")); + properties = load(Map.of("io.prometheus.exporter.pushgateway.escaping_scheme", "underscores")); assertThat(properties.getEscapingScheme()).isEqualTo(EscapingScheme.UNDERSCORE_ESCAPING); - properties = load(Map.of("io.prometheus.exporter.pushgateway.escapingScheme", "dots")); + properties = load(Map.of("io.prometheus.exporter.pushgateway.escaping_scheme", "dots")); assertThat(properties.getEscapingScheme()).isEqualTo(EscapingScheme.DOTS_ESCAPING); } @@ -57,9 +57,9 @@ void loadWithEscapingSchemes() { void loadWithInvalidEscapingScheme() { assertThatExceptionOfType(PrometheusPropertiesException.class) .isThrownBy( - () -> load(Map.of("io.prometheus.exporter.pushgateway.escapingScheme", "invalid"))) + () -> load(Map.of("io.prometheus.exporter.pushgateway.escaping_scheme", "invalid"))) .withMessage( - "io.prometheus.exporter.pushgateway.escapingScheme: Illegal value. Expecting" + "io.prometheus.exporter.pushgateway.escaping_scheme: Illegal value. Expecting" + " 'allow-utf-8', 'values', 'underscores', or 'dots'. Found: invalid"); } @@ -68,8 +68,8 @@ void loadWithTimeouts() { ExporterPushgatewayProperties properties = load( Map.of( - "io.prometheus.exporter.pushgateway.connectTimeoutSeconds", "5", - "io.prometheus.exporter.pushgateway.readTimeoutSeconds", "10")); + "io.prometheus.exporter.pushgateway.connect_timeout_seconds", "5", + "io.prometheus.exporter.pushgateway.read_timeout_seconds", "10")); assertThat(properties.getConnectTimeout()).isEqualTo(Duration.ofSeconds(5)); assertThat(properties.getReadTimeout()).isEqualTo(Duration.ofSeconds(10)); } From 8f8ed36ab04a1d7fd8768b94ac0ec165d40417d6 Mon Sep 17 00:00:00 2001 From: Gregor Zeitlinger Date: Fri, 6 Feb 2026 14:07:13 +0100 Subject: [PATCH 4/4] fix Signed-off-by: Gregor Zeitlinger --- .../config/PrometheusPropertiesTest.java | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/prometheus-metrics-config/src/test/java/io/prometheus/metrics/config/PrometheusPropertiesTest.java b/prometheus-metrics-config/src/test/java/io/prometheus/metrics/config/PrometheusPropertiesTest.java index 7248a187d..3e891202a 100644 --- a/prometheus-metrics-config/src/test/java/io/prometheus/metrics/config/PrometheusPropertiesTest.java +++ b/prometheus-metrics-config/src/test/java/io/prometheus/metrics/config/PrometheusPropertiesTest.java @@ -70,4 +70,62 @@ void testBuilder() { assertThat(result.getMetricProperties("unknown_metric")).isNull(); assertThat(result.getExporterProperties()).isSameAs(defaults.getExporterProperties()); } + + @Test + void testMetricNameNormalization() { + PrometheusProperties.Builder builder = PrometheusProperties.builder(); + MetricsProperties customProps = + MetricsProperties.builder().histogramClassicUpperBounds(0.1, 0.5).build(); + + // Test that metric names with dots are normalized to underscores + builder.putMetricProperty("my.metric.name", customProps); + PrometheusProperties result = builder.build(); + + // Should be able to retrieve with dots + assertThat(result.getMetricProperties("my.metric.name")).isSameAs(customProps); + // Should also be able to retrieve with underscores + assertThat(result.getMetricProperties("my_metric_name")).isSameAs(customProps); + } + + @Test + void testMetricNameWithInvalidCharacters() { + PrometheusProperties.Builder builder = PrometheusProperties.builder(); + MetricsProperties customProps = + MetricsProperties.builder().histogramClassicUpperBounds(0.1, 0.5).build(); + + // Test that invalid characters are converted to underscores + builder.putMetricProperty("metric-name@with#invalid$chars", customProps); + PrometheusProperties result = builder.build(); + + // Should normalize invalid characters to underscores + assertThat(result.getMetricProperties("metric-name@with#invalid$chars")).isSameAs(customProps); + assertThat(result.getMetricProperties("metric_name_with_invalid_chars")).isSameAs(customProps); + } + + @Test + void testMetricNameWithValidCharacters() { + PrometheusProperties.Builder builder = PrometheusProperties.builder(); + MetricsProperties customProps = + MetricsProperties.builder().histogramClassicUpperBounds(0.1, 0.5).build(); + + // Test valid characters: letters, numbers (not at start), underscore, colon + builder.putMetricProperty("my_metric:name123", customProps); + PrometheusProperties result = builder.build(); + + assertThat(result.getMetricProperties("my_metric:name123")).isSameAs(customProps); + } + + @Test + void testMetricNameStartingWithNumber() { + PrometheusProperties.Builder builder = PrometheusProperties.builder(); + MetricsProperties customProps = + MetricsProperties.builder().histogramClassicUpperBounds(0.1, 0.5).build(); + + // First digit is invalid (i=0), but subsequent digits are valid (i>0) + builder.putMetricProperty("123metric", customProps); + PrometheusProperties result = builder.build(); + + assertThat(result.getMetricProperties("123metric")).isSameAs(customProps); + assertThat(result.getMetricProperties("_23metric")).isSameAs(customProps); + } }