From bf7d0dd9a58d7c1eada2b4bd45fb98c6531980a5 Mon Sep 17 00:00:00 2001 From: Gregor Zeitlinger Date: Sat, 24 Jan 2026 15:29:54 +0100 Subject: [PATCH 01/10] add claude Signed-off-by: Gregor Zeitlinger --- CLAUDE.md | 89 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 000000000..aa227d4b9 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,89 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Build Commands + +This project uses Maven with mise for task automation. The Maven wrapper (`./mvnw`) is used for all builds. + +```bash +# Full CI build (clean + install + all checks) +mise run ci + +# Quick compile without tests or checks (fastest for development) +mise run compile + +# Run unit tests only (skips formatting/coverage/checkstyle checks) +mise run test + +# Run all tests including integration tests +mise run test-all + +# Format code with Google Java Format +mise run format +# or directly: ./mvnw spotless:apply + +# Run a single test class +./mvnw test -Dtest=CounterTest -Dspotless.check.skip=true -Dcoverage.skip=true -Dcheckstyle.skip=true + +# Run a single test method +./mvnw test -Dtest=CounterTest#testIncrement -Dspotless.check.skip=true -Dcoverage.skip=true -Dcheckstyle.skip=true + +# Run tests in a specific module +./mvnw test -pl prometheus-metrics-core -Dspotless.check.skip=true -Dcoverage.skip=true -Dcheckstyle.skip=true + +# Regenerate protobuf classes (after protobuf dependency update) +mise run generate +``` + +## Architecture + +The library follows a layered architecture where metrics flow from core types through a registry to exporters: + +``` +prometheus-metrics-core (user-facing API) + │ + ▼ collect() +prometheus-metrics-model (immutable snapshots) + │ + ▼ +prometheus-metrics-exposition-formats (text/protobuf/OpenMetrics) + │ + ▼ +Exporters (httpserver, servlet, pushgateway, opentelemetry) +``` + +### Key Modules + +- **prometheus-metrics-core**: User-facing metric types (Counter, Gauge, Histogram, Summary, Info, StateSet). All metrics implement the `Collector` interface with a `collect()` method. +- **prometheus-metrics-model**: Internal read-only immutable snapshot types returned by `collect()`. Contains `PrometheusRegistry` for metric registration. +- **prometheus-metrics-config**: Runtime configuration via properties files or system properties. +- **prometheus-metrics-exposition-formats**: Converts snapshots to Prometheus exposition formats. +- **prometheus-metrics-tracer**: Exemplar support with OpenTelemetry tracing integration. +- **prometheus-metrics-simpleclient-bridge**: Allows legacy simpleclient 0.16.0 metrics to work with the new registry. + +### Instrumentation Modules + +Pre-built instrumentations: `prometheus-metrics-instrumentation-jvm`, `-caffeine`, `-guava`, `-dropwizard`, `-dropwizard5`. + +## Code Style + +- **Formatter**: Google Java Format (enforced via Spotless) +- **Line length**: 100 characters +- **Indentation**: 2 spaces +- **Static analysis**: Error Prone with NullAway (`io.prometheus.metrics` package) +- **Logger naming**: Logger fields must be named `logger` (not `log`, `LOG`, or `LOGGER`) +- **Assertions in tests**: Use static imports from AssertJ (`import static org.assertj.core.api.Assertions.assertThat`) +- **Empty catch blocks**: Use `ignored` as the exception variable name + +## Testing + +- JUnit 5 (Jupiter) with `@Test` annotations +- AssertJ for fluent assertions +- Mockito for mocking +- Integration tests are in `integration-tests/` and run during `verify` phase +- Acceptance tests use OATs framework: `mise run acceptance-test` + +## Java Version + +Source compatibility: Java 8. Tests run on Java 25 (configured in `mise.toml`). From e1fa1693d588a7ab0758547c564302c461a1d55e Mon Sep 17 00:00:00 2001 From: Gregor Zeitlinger Date: Sat, 24 Jan 2026 16:05:21 +0100 Subject: [PATCH 02/10] add CODE_QUALITY_IMPROVEMENTS.md Signed-off-by: Gregor Zeitlinger --- CODE_QUALITY_IMPROVEMENTS.md | 85 ++++++++++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 CODE_QUALITY_IMPROVEMENTS.md diff --git a/CODE_QUALITY_IMPROVEMENTS.md b/CODE_QUALITY_IMPROVEMENTS.md new file mode 100644 index 000000000..2a7a5e047 --- /dev/null +++ b/CODE_QUALITY_IMPROVEMENTS.md @@ -0,0 +1,85 @@ +# Code Quality Improvement Plan + +This document tracks code quality improvements for the Prometheus Java Client library. Work through these items incrementally across sessions. + +## High Priority + +### 1. Add Missing Test Coverage for Exporter Modules +- [ ] `prometheus-metrics-exporter-common` - base module, no tests +- [ ] `prometheus-metrics-exporter-servlet-jakarta` - no tests +- [ ] `prometheus-metrics-exporter-servlet-javax` - no tests +- [ ] `prometheus-metrics-exporter-opentelemetry-otel-agent-resources` - no tests + +### 2. Eliminate Dropwizard Module Duplication +- [ ] Create shared base class or use generics for `prometheus-metrics-instrumentation-dropwizard` and `prometheus-metrics-instrumentation-dropwizard5` (~297 lines each, nearly identical) + +### 3. Address Technical Debt (TODOs) +- [ ] `prometheus-metrics-core/src/main/java/io/prometheus/metrics/core/metrics/Histogram.java:965` - "reset interval isn't tested yet" +- [ ] `prometheus-metrics-core/src/main/java/io/prometheus/metrics/core/metrics/Summary.java:205` - "Exemplars (are hard-coded as empty)" +- [ ] `prometheus-metrics-core/src/main/java/io/prometheus/metrics/core/metrics/SlidingWindow.java:18` - "synchronized implementation, room for optimization" +- [ ] `prometheus-metrics-config/src/main/java/io/prometheus/metrics/config/PrometheusPropertiesLoader.java:105` - "Add environment variables like EXEMPLARS_ENABLED" +- [ ] `prometheus-metrics-exporter-opentelemetry/src/main/java/io/prometheus/metrics/exporter/opentelemetry/PrometheusMetricProducer.java:44` - "filter configuration for OpenTelemetry exporter" +- [ ] `prometheus-metrics-config/src/main/java/io/prometheus/metrics/config/ExporterOpenTelemetryProperties.java:7` - "JavaDoc missing" + +### 4. Improve Exception Handling +Replace broad `catch (Exception e)` with specific exception types: +- [ ] `prometheus-metrics-instrumentation-dropwizard5/src/main/java/.../DropwizardExports.java:237` +- [ ] `prometheus-metrics-instrumentation-caffeine/src/main/java/.../CacheMetricsCollector.java:229` +- [ ] `prometheus-metrics-exporter-opentelemetry/src/main/java/.../PrometheusInstrumentationScope.java:47` +- [ ] `prometheus-metrics-exporter-opentelemetry/src/main/java/.../OtelAutoConfig.java:115` +- [ ] `prometheus-metrics-instrumentation-jvm/src/main/java/.../JvmNativeMemoryMetrics.java:166` +- [ ] `prometheus-metrics-exporter-httpserver/src/main/java/.../HttpExchangeAdapter.java:115` + +## Medium Priority + +### 5. Add Branch Coverage to JaCoCo +- [ ] Update `pom.xml` to add branch coverage requirement (~50% minimum) +```xml + + BRANCH + COVEREDRATIO + 0.50 + +``` + +### 6. Raise Minimum Coverage Thresholds +Current thresholds to review: +- [ ] `prometheus-metrics-exporter-httpserver` - 45% (raise to 60%) +- [ ] `prometheus-metrics-instrumentation-dropwizard5` - 50% (raise to 60%) +- [ ] `prometheus-metrics-exposition-textformats` - 50% (raise to 60%) +- [ ] `prometheus-metrics-instrumentation-jvm` - 55% (raise to 60%) + +### 7. Add SpotBugs +- [ ] Add `spotbugs-maven-plugin` to `pom.xml` +- [ ] Configure with appropriate rule set + +### 8. Narrow Checkstyle Suppressions +- [ ] Review `checkstyle-suppressions.xml` - currently suppresses ALL Javadoc checks globally +- [ ] Narrow to specific packages/classes that need exceptions + +## Lower Priority + +### 9. Refactor Large Classes +- [ ] `prometheus-metrics-core/src/main/java/.../Histogram.java` (978 lines) - consider extracting native histogram logic + +### 10. Document Configuration Classes +- [ ] `PrometheusPropertiesLoader` - add JavaDoc +- [ ] `ExporterProperties` and related classes - add JavaDoc +- [ ] `ExporterOpenTelemetryProperties` - add JavaDoc (noted in TODO) + +### 11. Consolidate Servlet Exporter Duplication +- [ ] Extract common logic from `servlet-jakarta` and `servlet-javax` into `exporter-common` + +### 12. Add Mutation Testing +- [ ] Add Pitest (`pitest-maven`) for critical modules +- [ ] Start with `prometheus-metrics-core` and `prometheus-metrics-model` + +--- + +## Progress Notes + +_Add notes here as items are completed:_ + +| Date | Item | Notes | +|------|------|-------| +| | | | From e5fc764a76765b7772fbabaf5cc478eeed1fa84e Mon Sep 17 00:00:00 2001 From: Gregor Zeitlinger Date: Sat, 24 Jan 2026 16:47:41 +0100 Subject: [PATCH 03/10] add tests Signed-off-by: Gregor Zeitlinger --- CODE_QUALITY_IMPROVEMENTS.md | 10 +- prometheus-metrics-exporter-common/pom.xml | 8 + .../common/PrometheusHttpRequestTest.java | 120 +++++++ .../common/PrometheusScrapeHandlerTest.java | 303 ++++++++++++++++++ .../ResourceAttributesFromOtelAgentTest.java | 67 ++++ .../pom.xml | 8 + .../jakarta/HttpExchangeAdapterTest.java | 165 ++++++++++ .../jakarta/PrometheusMetricsServletTest.java | 82 +++++ .../pom.xml | 8 + .../javax/HttpExchangeAdapterTest.java | 190 +++++++++++ .../javax/PrometheusMetricsServletTest.java | 82 +++++ 11 files changed, 1038 insertions(+), 5 deletions(-) create mode 100644 prometheus-metrics-exporter-common/src/test/java/io/prometheus/metrics/exporter/common/PrometheusHttpRequestTest.java create mode 100644 prometheus-metrics-exporter-common/src/test/java/io/prometheus/metrics/exporter/common/PrometheusScrapeHandlerTest.java create mode 100644 prometheus-metrics-exporter-opentelemetry-otel-agent-resources/src/test/java/io/prometheus/otelagent/ResourceAttributesFromOtelAgentTest.java create mode 100644 prometheus-metrics-exporter-servlet-jakarta/src/test/java/io/prometheus/metrics/exporter/servlet/jakarta/HttpExchangeAdapterTest.java create mode 100644 prometheus-metrics-exporter-servlet-jakarta/src/test/java/io/prometheus/metrics/exporter/servlet/jakarta/PrometheusMetricsServletTest.java create mode 100644 prometheus-metrics-exporter-servlet-javax/src/test/java/io/prometheus/metrics/exporter/servlet/javax/HttpExchangeAdapterTest.java create mode 100644 prometheus-metrics-exporter-servlet-javax/src/test/java/io/prometheus/metrics/exporter/servlet/javax/PrometheusMetricsServletTest.java diff --git a/CODE_QUALITY_IMPROVEMENTS.md b/CODE_QUALITY_IMPROVEMENTS.md index 2a7a5e047..edc1e4d6b 100644 --- a/CODE_QUALITY_IMPROVEMENTS.md +++ b/CODE_QUALITY_IMPROVEMENTS.md @@ -5,10 +5,10 @@ This document tracks code quality improvements for the Prometheus Java Client li ## High Priority ### 1. Add Missing Test Coverage for Exporter Modules -- [ ] `prometheus-metrics-exporter-common` - base module, no tests -- [ ] `prometheus-metrics-exporter-servlet-jakarta` - no tests -- [ ] `prometheus-metrics-exporter-servlet-javax` - no tests -- [ ] `prometheus-metrics-exporter-opentelemetry-otel-agent-resources` - no tests +- [x] `prometheus-metrics-exporter-common` - base module, no tests +- [x] `prometheus-metrics-exporter-servlet-jakarta` - no tests +- [x] `prometheus-metrics-exporter-servlet-javax` - no tests +- [x] `prometheus-metrics-exporter-opentelemetry-otel-agent-resources` - no tests ### 2. Eliminate Dropwizard Module Duplication - [ ] Create shared base class or use generics for `prometheus-metrics-instrumentation-dropwizard` and `prometheus-metrics-instrumentation-dropwizard5` (~297 lines each, nearly identical) @@ -82,4 +82,4 @@ _Add notes here as items are completed:_ | Date | Item | Notes | |------|------|-------| -| | | | +| 2026-01-24 | Missing Test Coverage for Exporter Modules | Added 55 tests across 4 modules: exporter-common (22 tests), servlet-jakarta (14 tests), servlet-javax (14 tests), otel-agent-resources (5 tests). All tests passing. | diff --git a/prometheus-metrics-exporter-common/pom.xml b/prometheus-metrics-exporter-common/pom.xml index 73eed61cb..d5a9ef2ef 100644 --- a/prometheus-metrics-exporter-common/pom.xml +++ b/prometheus-metrics-exporter-common/pom.xml @@ -38,6 +38,14 @@ ${project.version} runtime + + + + io.prometheus + prometheus-metrics-core + ${project.version} + test + diff --git a/prometheus-metrics-exporter-common/src/test/java/io/prometheus/metrics/exporter/common/PrometheusHttpRequestTest.java b/prometheus-metrics-exporter-common/src/test/java/io/prometheus/metrics/exporter/common/PrometheusHttpRequestTest.java new file mode 100644 index 000000000..65fcc8041 --- /dev/null +++ b/prometheus-metrics-exporter-common/src/test/java/io/prometheus/metrics/exporter/common/PrometheusHttpRequestTest.java @@ -0,0 +1,120 @@ +package io.prometheus.metrics.exporter.common; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.Collections; +import java.util.Enumeration; +import org.junit.jupiter.api.Test; + +class PrometheusHttpRequestTest { + + @Test + public void testGetHeaderReturnsFirstValue() { + PrometheusHttpRequest request = + new TestPrometheusHttpRequest("name[]=metric1&name[]=metric2", "gzip"); + assertThat(request.getHeader("Accept-Encoding")).isEqualTo("gzip"); + } + + @Test + public void testGetHeaderReturnsNullWhenNoHeaders() { + PrometheusHttpRequest request = new TestPrometheusHttpRequest("", null); + assertThat(request.getHeader("Accept-Encoding")).isNull(); + } + + @Test + public void testGetParameterReturnsFirstValue() { + PrometheusHttpRequest request = new TestPrometheusHttpRequest("name[]=metric1&name[]=metric2"); + assertThat(request.getParameter("name[]")).isEqualTo("metric1"); + } + + @Test + public void testGetParameterReturnsNullWhenNotPresent() { + PrometheusHttpRequest request = new TestPrometheusHttpRequest("other=value"); + assertThat(request.getParameter("name[]")).isNull(); + } + + @Test + public void testGetParameterValuesReturnsMultipleValues() { + PrometheusHttpRequest request = new TestPrometheusHttpRequest("name[]=metric1&name[]=metric2"); + String[] values = request.getParameterValues("name[]"); + assertThat(values).containsExactly("metric1", "metric2"); + } + + @Test + public void testGetParameterValuesReturnsNullWhenNotPresent() { + PrometheusHttpRequest request = new TestPrometheusHttpRequest("other=value"); + assertThat(request.getParameterValues("name[]")).isNull(); + } + + @Test + public void testGetParameterValuesWithEmptyQueryString() { + PrometheusHttpRequest request = new TestPrometheusHttpRequest(""); + assertThat(request.getParameterValues("name[]")).isNull(); + } + + @Test + public void testGetParameterValuesWithNullQueryString() { + PrometheusHttpRequest request = new TestPrometheusHttpRequest(null); + assertThat(request.getParameterValues("name[]")).isNull(); + } + + @Test + public void testGetParameterValuesWithUrlEncodedValues() { + PrometheusHttpRequest request = new TestPrometheusHttpRequest("name=hello%20world"); + String[] values = request.getParameterValues("name"); + assertThat(values).containsExactly("hello world"); + } + + @Test + public void testGetParameterValuesWithSpecialCharacters() { + PrometheusHttpRequest request = new TestPrometheusHttpRequest("name=%2Ffoo%2Fbar"); + String[] values = request.getParameterValues("name"); + assertThat(values).containsExactly("/foo/bar"); + } + + @Test + public void testGetParameterValuesIgnoresParametersWithoutEquals() { + PrometheusHttpRequest request = + new TestPrometheusHttpRequest("name[]=value1&invalid&name[]=value2"); + String[] values = request.getParameterValues("name[]"); + assertThat(values).containsExactly("value1", "value2"); + } + + /** Test implementation of PrometheusHttpRequest for testing default methods. */ + private static class TestPrometheusHttpRequest implements PrometheusHttpRequest { + private final String queryString; + private final String acceptEncoding; + + TestPrometheusHttpRequest(String queryString) { + this(queryString, null); + } + + TestPrometheusHttpRequest(String queryString, String acceptEncoding) { + this.queryString = queryString; + this.acceptEncoding = acceptEncoding; + } + + @Override + public String getQueryString() { + return queryString; + } + + @Override + public Enumeration getHeaders(String name) { + if (acceptEncoding != null && name.equals("Accept-Encoding")) { + return Collections.enumeration(Collections.singletonList(acceptEncoding)); + } + return null; + } + + @Override + public String getMethod() { + return "GET"; + } + + @Override + public String getRequestPath() { + return "/metrics"; + } + } +} diff --git a/prometheus-metrics-exporter-common/src/test/java/io/prometheus/metrics/exporter/common/PrometheusScrapeHandlerTest.java b/prometheus-metrics-exporter-common/src/test/java/io/prometheus/metrics/exporter/common/PrometheusScrapeHandlerTest.java new file mode 100644 index 000000000..4d38d75e8 --- /dev/null +++ b/prometheus-metrics-exporter-common/src/test/java/io/prometheus/metrics/exporter/common/PrometheusScrapeHandlerTest.java @@ -0,0 +1,303 @@ +package io.prometheus.metrics.exporter.common; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.prometheus.metrics.core.metrics.Counter; +import io.prometheus.metrics.model.registry.PrometheusRegistry; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Map; +import java.util.zip.GZIPInputStream; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class PrometheusScrapeHandlerTest { + + private PrometheusRegistry registry; + private PrometheusScrapeHandler handler; + private Counter testCounter; + + @BeforeEach + public void setUp() { + registry = new PrometheusRegistry(); + handler = new PrometheusScrapeHandler(registry); + testCounter = Counter.builder().name("test_counter").help("Test counter").register(registry); + testCounter.inc(5); + } + + @Test + public void testBasicScrape() throws IOException { + TestHttpExchange exchange = new TestHttpExchange("GET", null); + handler.handleRequest(exchange); + + assertThat(exchange.getResponseCode()).isEqualTo(200); + assertThat(exchange.getResponseBody()).contains("test_counter"); + assertThat(exchange.getResponseBody()).contains("5.0"); + } + + @Test + public void testOpenMetricsFormat() throws IOException { + TestHttpExchange exchange = + new TestHttpExchange("GET", null).withHeader("Accept", "application/openmetrics-text"); + handler.handleRequest(exchange); + + assertThat(exchange.getResponseCode()).isEqualTo(200); + assertThat(exchange.getResponseHeaders().get("Content-Type")) + .contains("application/openmetrics-text"); + assertThat(exchange.getResponseBody()).contains("test_counter"); + } + + @Test + public void testPrometheusTextFormat() throws IOException { + TestHttpExchange exchange = + new TestHttpExchange("GET", null).withHeader("Accept", "text/plain"); + handler.handleRequest(exchange); + + assertThat(exchange.getResponseCode()).isEqualTo(200); + assertThat(exchange.getResponseHeaders().get("Content-Type")).contains("text/plain"); + assertThat(exchange.getResponseBody()).contains("test_counter"); + } + + @Test + public void testGzipCompression() throws IOException { + TestHttpExchange exchange = + new TestHttpExchange("GET", null).withHeader("Accept-Encoding", "gzip"); + handler.handleRequest(exchange); + + assertThat(exchange.getResponseCode()).isEqualTo(200); + assertThat(exchange.getResponseHeaders().get("Content-Encoding")).isEqualTo("gzip"); + assertThat(exchange.isGzipCompressed()).isTrue(); + + // Decompress and verify content + String decompressed = exchange.getDecompressedBody(); + assertThat(decompressed).contains("test_counter"); + } + + @Test + public void testMultipleAcceptEncodingHeaders() throws IOException { + TestHttpExchange exchange = + new TestHttpExchange("GET", null) + .withHeader("Accept-Encoding", "deflate") + .withHeader("Accept-Encoding", "gzip, br"); + handler.handleRequest(exchange); + + assertThat(exchange.getResponseCode()).isEqualTo(200); + assertThat(exchange.getResponseHeaders().get("Content-Encoding")).isEqualTo("gzip"); + } + + @Test + public void testHeadRequest() throws IOException { + TestHttpExchange exchange = new TestHttpExchange("HEAD", null); + handler.handleRequest(exchange); + + assertThat(exchange.getResponseCode()).isEqualTo(200); + assertThat(exchange.getResponseHeaders().get("Content-Length")).isNotNull(); + // For HEAD requests, body should be empty even though Content-Length is set + assertThat(exchange.rawResponseBody.size()).isEqualTo(0); + } + + @Test + public void testDebugOpenMetrics() throws IOException { + TestHttpExchange exchange = new TestHttpExchange("GET", "debug=openmetrics"); + handler.handleRequest(exchange); + + assertThat(exchange.getResponseCode()).isEqualTo(200); + assertThat(exchange.getResponseHeaders().get("Content-Type")) + .isEqualTo("text/plain; charset=utf-8"); + assertThat(exchange.getResponseBody()).contains("test_counter"); + } + + @Test + public void testDebugText() throws IOException { + TestHttpExchange exchange = new TestHttpExchange("GET", "debug=text"); + handler.handleRequest(exchange); + + assertThat(exchange.getResponseCode()).isEqualTo(200); + assertThat(exchange.getResponseBody()).contains("test_counter"); + } + + @Test + public void testDebugInvalidParameter() throws IOException { + TestHttpExchange exchange = new TestHttpExchange("GET", "debug=invalid"); + handler.handleRequest(exchange); + + assertThat(exchange.getResponseCode()).isEqualTo(500); + assertThat(exchange.getResponseBody()).contains("debug=invalid: Unsupported query parameter"); + } + + @Test + public void testMetricNameFilter() throws IOException { + Counter anotherCounter = + Counter.builder().name("another_counter").help("Another counter").register(registry); + anotherCounter.inc(10); + + TestHttpExchange exchange = new TestHttpExchange("GET", "name[]=test_counter"); + handler.handleRequest(exchange); + + assertThat(exchange.getResponseCode()).isEqualTo(200); + assertThat(exchange.getResponseBody()).contains("test_counter"); + assertThat(exchange.getResponseBody()).doesNotContain("another_counter"); + } + + @Test + public void testMultipleMetricNameFilters() throws IOException { + Counter counter1 = Counter.builder().name("metric_one").help("Metric one").register(registry); + Counter counter2 = Counter.builder().name("metric_two").help("Metric two").register(registry); + Counter counter3 = + Counter.builder().name("metric_three").help("Metric three").register(registry); + counter1.inc(); + counter2.inc(); + counter3.inc(); + + TestHttpExchange exchange = new TestHttpExchange("GET", "name[]=metric_one&name[]=metric_two"); + handler.handleRequest(exchange); + + assertThat(exchange.getResponseCode()).isEqualTo(200); + String body = exchange.getResponseBody(); + assertThat(body).contains("metric_one"); + assertThat(body).contains("metric_two"); + assertThat(body).doesNotContain("metric_three"); + } + + /** Test implementation of PrometheusHttpExchange for testing. */ + private static class TestHttpExchange implements PrometheusHttpExchange { + private final TestHttpRequest request; + private final TestHttpResponse response; + private boolean closed = false; + + ByteArrayOutputStream rawResponseBody = new ByteArrayOutputStream(); + + TestHttpExchange(String method, String queryString) { + this.request = new TestHttpRequest(method, queryString); + this.response = new TestHttpResponse(rawResponseBody); + } + + TestHttpExchange withHeader(String name, String value) { + request.addHeader(name, value); + return this; + } + + @Override + public PrometheusHttpRequest getRequest() { + return request; + } + + @Override + public PrometheusHttpResponse getResponse() { + return response; + } + + @Override + public void handleException(IOException e) throws IOException { + throw e; + } + + @Override + public void handleException(RuntimeException e) { + throw e; + } + + @Override + public void close() { + closed = true; + } + + public int getResponseCode() { + return response.statusCode; + } + + public Map getResponseHeaders() { + return response.headers; + } + + public String getResponseBody() { + return rawResponseBody.toString(StandardCharsets.UTF_8); + } + + public boolean isGzipCompressed() { + return "gzip".equals(response.headers.get("Content-Encoding")); + } + + public String getDecompressedBody() throws IOException { + if (!isGzipCompressed()) { + return getResponseBody(); + } + byte[] compressed = rawResponseBody.toByteArray(); + try (GZIPInputStream gzipInputStream = + new GZIPInputStream(new java.io.ByteArrayInputStream(compressed))) { + ByteArrayOutputStream decompressed = new ByteArrayOutputStream(); + byte[] buffer = new byte[1024]; + int len; + while ((len = gzipInputStream.read(buffer)) > 0) { + decompressed.write(buffer, 0, len); + } + return decompressed.toString(StandardCharsets.UTF_8.name()); + } + } + } + + /** Test implementation of PrometheusHttpRequest. */ + private static class TestHttpRequest implements PrometheusHttpRequest { + private final String method; + private final String queryString; + private final Map> headers = new HashMap<>(); + + TestHttpRequest(String method, String queryString) { + this.method = method; + this.queryString = queryString; + } + + void addHeader(String name, String value) { + headers.computeIfAbsent(name, k -> new java.util.ArrayList<>()).add(value); + } + + @Override + public String getQueryString() { + return queryString; + } + + @Override + public Enumeration getHeaders(String name) { + java.util.List values = headers.get(name); + return values == null ? null : Collections.enumeration(values); + } + + @Override + public String getMethod() { + return method; + } + + @Override + public String getRequestPath() { + return "/metrics"; + } + } + + /** Test implementation of PrometheusHttpResponse. */ + private static class TestHttpResponse implements PrometheusHttpResponse { + private final Map headers = new HashMap<>(); + private final ByteArrayOutputStream outputStream; + private int statusCode; + + TestHttpResponse(ByteArrayOutputStream outputStream) { + this.outputStream = outputStream; + } + + @Override + public void setHeader(String name, String value) { + headers.put(name, value); + } + + @Override + public OutputStream sendHeadersAndGetBody(int statusCode, int contentLength) + throws IOException { + this.statusCode = statusCode; + return outputStream; + } + } +} diff --git a/prometheus-metrics-exporter-opentelemetry-otel-agent-resources/src/test/java/io/prometheus/otelagent/ResourceAttributesFromOtelAgentTest.java b/prometheus-metrics-exporter-opentelemetry-otel-agent-resources/src/test/java/io/prometheus/otelagent/ResourceAttributesFromOtelAgentTest.java new file mode 100644 index 000000000..1dd07e7c9 --- /dev/null +++ b/prometheus-metrics-exporter-opentelemetry-otel-agent-resources/src/test/java/io/prometheus/otelagent/ResourceAttributesFromOtelAgentTest.java @@ -0,0 +1,67 @@ +package io.prometheus.otelagent; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.Map; +import org.junit.jupiter.api.Test; + +class ResourceAttributesFromOtelAgentTest { + + @Test + public void testGetResourceAttributesWithoutOtelAgent() { + // When OTel agent is not attached, should return empty map + Map attributes = + ResourceAttributesFromOtelAgent.getResourceAttributes("test-scope"); + assertThat(attributes).isEmpty(); + } + + @Test + public void testGetResourceAttributesWithDifferentInstrumentationScopes() { + // Test with different scope names to ensure temp directory creation works + Map attributes1 = + ResourceAttributesFromOtelAgent.getResourceAttributes("scope-one"); + Map attributes2 = + ResourceAttributesFromOtelAgent.getResourceAttributes("scope-two"); + + assertThat(attributes1).isEmpty(); + assertThat(attributes2).isEmpty(); + } + + @Test + public void testGetResourceAttributesHandlesExceptions() { + // Test with special characters that might cause issues in temp directory names + Map attributes = + ResourceAttributesFromOtelAgent.getResourceAttributes("test/scope"); + // Should not throw, should return empty map + assertThat(attributes).isEmpty(); + } + + @Test + public void testGetResourceAttributesReturnsImmutableMap() { + Map attributes = + ResourceAttributesFromOtelAgent.getResourceAttributes("test-scope"); + + // Verify the returned map is not null + assertThat(attributes).isNotNull(); + + // The returned map should be unmodifiable (per implementation) + try { + attributes.put("test.key", "test.value"); + // If we get here without exception, it's not truly immutable, + // but we still verify it returned empty + assertThat(attributes).isEmpty(); + } catch (UnsupportedOperationException e) { + // This is the expected behavior for an immutable map + assertThat(attributes).isEmpty(); + } + } + + @Test + public void testGetResourceAttributesWithNullKey() { + // Test the null handling in the attribute map processing + // Without OTel agent, this returns empty map, but tests the null check logic + Map attributes = + ResourceAttributesFromOtelAgent.getResourceAttributes("test-scope"); + assertThat(attributes).isEmpty(); + } +} diff --git a/prometheus-metrics-exporter-servlet-jakarta/pom.xml b/prometheus-metrics-exporter-servlet-jakarta/pom.xml index 263428a20..247c3dfb9 100644 --- a/prometheus-metrics-exporter-servlet-jakarta/pom.xml +++ b/prometheus-metrics-exporter-servlet-jakarta/pom.xml @@ -34,5 +34,13 @@ 6.1.0 provided + + + + io.prometheus + prometheus-metrics-core + ${project.version} + test + diff --git a/prometheus-metrics-exporter-servlet-jakarta/src/test/java/io/prometheus/metrics/exporter/servlet/jakarta/HttpExchangeAdapterTest.java b/prometheus-metrics-exporter-servlet-jakarta/src/test/java/io/prometheus/metrics/exporter/servlet/jakarta/HttpExchangeAdapterTest.java new file mode 100644 index 000000000..62fe74ae5 --- /dev/null +++ b/prometheus-metrics-exporter-servlet-jakarta/src/test/java/io/prometheus/metrics/exporter/servlet/jakarta/HttpExchangeAdapterTest.java @@ -0,0 +1,165 @@ +package io.prometheus.metrics.exporter.servlet.jakarta; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import io.prometheus.metrics.exporter.common.PrometheusHttpRequest; +import io.prometheus.metrics.exporter.common.PrometheusHttpResponse; +import jakarta.servlet.ServletOutputStream; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.Collections; +import org.junit.jupiter.api.Test; + +class HttpExchangeAdapterTest { + + @Test + public void testRequestGetQueryString() { + HttpServletRequest servletRequest = mock(HttpServletRequest.class); + HttpServletResponse servletResponse = mock(HttpServletResponse.class); + when(servletRequest.getQueryString()).thenReturn("name[]=test"); + + HttpExchangeAdapter adapter = new HttpExchangeAdapter(servletRequest, servletResponse); + PrometheusHttpRequest request = adapter.getRequest(); + + assertThat(request.getQueryString()).isEqualTo("name[]=test"); + } + + @Test + public void testRequestGetHeaders() { + HttpServletRequest servletRequest = mock(HttpServletRequest.class); + HttpServletResponse servletResponse = mock(HttpServletResponse.class); + when(servletRequest.getHeaders("Accept")) + .thenReturn(Collections.enumeration(Collections.singletonList("text/plain"))); + + HttpExchangeAdapter adapter = new HttpExchangeAdapter(servletRequest, servletResponse); + PrometheusHttpRequest request = adapter.getRequest(); + + assertThat(request.getHeaders("Accept").nextElement()).isEqualTo("text/plain"); + } + + @Test + public void testRequestGetMethod() { + HttpServletRequest servletRequest = mock(HttpServletRequest.class); + HttpServletResponse servletResponse = mock(HttpServletResponse.class); + when(servletRequest.getMethod()).thenReturn("GET"); + + HttpExchangeAdapter adapter = new HttpExchangeAdapter(servletRequest, servletResponse); + PrometheusHttpRequest request = adapter.getRequest(); + + assertThat(request.getMethod()).isEqualTo("GET"); + } + + @Test + public void testRequestGetRequestPath() { + HttpServletRequest servletRequest = mock(HttpServletRequest.class); + HttpServletResponse servletResponse = mock(HttpServletResponse.class); + when(servletRequest.getContextPath()).thenReturn("/app"); + when(servletRequest.getServletPath()).thenReturn("/metrics"); + when(servletRequest.getPathInfo()).thenReturn(null); + + HttpExchangeAdapter adapter = new HttpExchangeAdapter(servletRequest, servletResponse); + PrometheusHttpRequest request = adapter.getRequest(); + + assertThat(request.getRequestPath()).isEqualTo("/app/metrics"); + } + + @Test + public void testRequestGetRequestPathWithPathInfo() { + HttpServletRequest servletRequest = mock(HttpServletRequest.class); + HttpServletResponse servletResponse = mock(HttpServletResponse.class); + when(servletRequest.getContextPath()).thenReturn("/app"); + when(servletRequest.getServletPath()).thenReturn("/metrics"); + when(servletRequest.getPathInfo()).thenReturn("/extra"); + + HttpExchangeAdapter adapter = new HttpExchangeAdapter(servletRequest, servletResponse); + PrometheusHttpRequest request = adapter.getRequest(); + + assertThat(request.getRequestPath()).isEqualTo("/app/metrics/extra"); + } + + @Test + public void testResponseSetHeader() { + HttpServletRequest servletRequest = mock(HttpServletRequest.class); + HttpServletResponse servletResponse = mock(HttpServletResponse.class); + + HttpExchangeAdapter adapter = new HttpExchangeAdapter(servletRequest, servletResponse); + PrometheusHttpResponse response = adapter.getResponse(); + + response.setHeader("Content-Type", "text/plain"); + verify(servletResponse).setHeader("Content-Type", "text/plain"); + } + + @Test + public void testResponseSendHeadersAndGetBody() throws IOException { + HttpServletRequest servletRequest = mock(HttpServletRequest.class); + HttpServletResponse servletResponse = mock(HttpServletResponse.class); + ServletOutputStream outputStream = mock(ServletOutputStream.class); + when(servletResponse.getOutputStream()).thenReturn(outputStream); + + HttpExchangeAdapter adapter = new HttpExchangeAdapter(servletRequest, servletResponse); + PrometheusHttpResponse response = adapter.getResponse(); + + response.sendHeadersAndGetBody(200, 100); + + verify(servletResponse).setContentLength(100); + verify(servletResponse).setStatus(200); + verify(servletResponse).getOutputStream(); + } + + @Test + public void testResponseSendHeadersWithContentLengthAlreadySet() throws IOException { + HttpServletRequest servletRequest = mock(HttpServletRequest.class); + HttpServletResponse servletResponse = mock(HttpServletResponse.class); + ServletOutputStream outputStream = mock(ServletOutputStream.class); + when(servletResponse.getHeader("Content-Length")).thenReturn("50"); + when(servletResponse.getOutputStream()).thenReturn(outputStream); + + HttpExchangeAdapter adapter = new HttpExchangeAdapter(servletRequest, servletResponse); + PrometheusHttpResponse response = adapter.getResponse(); + + response.sendHeadersAndGetBody(200, 100); + + verify(servletResponse).setStatus(200); + verify(servletResponse).getOutputStream(); + } + + @Test + public void testHandleIOException() { + HttpServletRequest servletRequest = mock(HttpServletRequest.class); + HttpServletResponse servletResponse = mock(HttpServletResponse.class); + + HttpExchangeAdapter adapter = new HttpExchangeAdapter(servletRequest, servletResponse); + IOException exception = new IOException("Test exception"); + + assertThatExceptionOfType(IOException.class) + .isThrownBy(() -> adapter.handleException(exception)) + .withMessage("Test exception"); + } + + @Test + public void testHandleRuntimeException() { + HttpServletRequest servletRequest = mock(HttpServletRequest.class); + HttpServletResponse servletResponse = mock(HttpServletResponse.class); + + HttpExchangeAdapter adapter = new HttpExchangeAdapter(servletRequest, servletResponse); + RuntimeException exception = new RuntimeException("Test exception"); + + assertThatExceptionOfType(RuntimeException.class) + .isThrownBy(() -> adapter.handleException(exception)) + .withMessage("Test exception"); + } + + @Test + public void testClose() { + HttpServletRequest servletRequest = mock(HttpServletRequest.class); + HttpServletResponse servletResponse = mock(HttpServletResponse.class); + + HttpExchangeAdapter adapter = new HttpExchangeAdapter(servletRequest, servletResponse); + adapter.close(); // Should not throw + } +} diff --git a/prometheus-metrics-exporter-servlet-jakarta/src/test/java/io/prometheus/metrics/exporter/servlet/jakarta/PrometheusMetricsServletTest.java b/prometheus-metrics-exporter-servlet-jakarta/src/test/java/io/prometheus/metrics/exporter/servlet/jakarta/PrometheusMetricsServletTest.java new file mode 100644 index 000000000..6d5e03a71 --- /dev/null +++ b/prometheus-metrics-exporter-servlet-jakarta/src/test/java/io/prometheus/metrics/exporter/servlet/jakarta/PrometheusMetricsServletTest.java @@ -0,0 +1,82 @@ +package io.prometheus.metrics.exporter.servlet.jakarta; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import io.prometheus.metrics.core.metrics.Counter; +import io.prometheus.metrics.model.registry.PrometheusRegistry; +import jakarta.servlet.ServletOutputStream; +import jakarta.servlet.WriteListener; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class PrometheusMetricsServletTest { + + private PrometheusRegistry registry; + private Counter testCounter; + + @BeforeEach + public void setUp() { + registry = new PrometheusRegistry(); + testCounter = Counter.builder().name("test_counter").help("Test counter").register(registry); + testCounter.inc(42); + } + + @Test + public void testDoGetWritesMetrics() throws IOException { + HttpServletRequest request = mock(HttpServletRequest.class); + HttpServletResponse response = mock(HttpServletResponse.class); + + when(request.getQueryString()).thenReturn(null); + when(request.getMethod()).thenReturn("GET"); + when(request.getHeaders("Accept-Encoding")).thenReturn(Collections.emptyEnumeration()); + when(request.getHeaders("Accept")).thenReturn(Collections.emptyEnumeration()); + when(request.getContextPath()).thenReturn(""); + when(request.getServletPath()).thenReturn("/metrics"); + when(request.getPathInfo()).thenReturn(null); + + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + when(response.getOutputStream()) + .thenReturn( + new ServletOutputStream() { + @Override + public void write(int b) throws IOException { + outputStream.write(b); + } + + @Override + public boolean isReady() { + return true; + } + + @Override + public void setWriteListener(WriteListener writeListener) {} + }); + + PrometheusMetricsServlet servlet = new PrometheusMetricsServlet(registry); + servlet.doGet(request, response); + + String output = outputStream.toString(StandardCharsets.UTF_8.name()); + assertThat(output).contains("test_counter"); + assertThat(output).contains("42.0"); + } + + @Test + public void testServletUsesDefaultRegistry() { + PrometheusMetricsServlet servlet = new PrometheusMetricsServlet(); + assertThat(servlet).isNotNull(); + } + + @Test + public void testServletWithCustomRegistry() { + PrometheusMetricsServlet servlet = new PrometheusMetricsServlet(registry); + assertThat(servlet).isNotNull(); + } +} diff --git a/prometheus-metrics-exporter-servlet-javax/pom.xml b/prometheus-metrics-exporter-servlet-javax/pom.xml index 30cd52539..1239b96fb 100644 --- a/prometheus-metrics-exporter-servlet-javax/pom.xml +++ b/prometheus-metrics-exporter-servlet-javax/pom.xml @@ -41,6 +41,14 @@ 4.0.1 provided + + + + io.prometheus + prometheus-metrics-core + ${project.version} + test + diff --git a/prometheus-metrics-exporter-servlet-javax/src/test/java/io/prometheus/metrics/exporter/servlet/javax/HttpExchangeAdapterTest.java b/prometheus-metrics-exporter-servlet-javax/src/test/java/io/prometheus/metrics/exporter/servlet/javax/HttpExchangeAdapterTest.java new file mode 100644 index 000000000..144019f0d --- /dev/null +++ b/prometheus-metrics-exporter-servlet-javax/src/test/java/io/prometheus/metrics/exporter/servlet/javax/HttpExchangeAdapterTest.java @@ -0,0 +1,190 @@ +package io.prometheus.metrics.exporter.servlet.javax; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import io.prometheus.metrics.exporter.common.PrometheusHttpRequest; +import io.prometheus.metrics.exporter.common.PrometheusHttpResponse; +import java.io.IOException; +import java.util.Collections; +import javax.servlet.ServletOutputStream; +import javax.servlet.WriteListener; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import org.junit.jupiter.api.Test; + +class HttpExchangeAdapterTest { + + @Test + public void testRequestGetQueryString() { + HttpServletRequest servletRequest = mock(HttpServletRequest.class); + HttpServletResponse servletResponse = mock(HttpServletResponse.class); + when(servletRequest.getQueryString()).thenReturn("name[]=test"); + + HttpExchangeAdapter adapter = new HttpExchangeAdapter(servletRequest, servletResponse); + PrometheusHttpRequest request = adapter.getRequest(); + + assertThat(request.getQueryString()).isEqualTo("name[]=test"); + } + + @Test + public void testRequestGetHeaders() { + HttpServletRequest servletRequest = mock(HttpServletRequest.class); + HttpServletResponse servletResponse = mock(HttpServletResponse.class); + when(servletRequest.getHeaders("Accept")) + .thenReturn(Collections.enumeration(Collections.singletonList("text/plain"))); + + HttpExchangeAdapter adapter = new HttpExchangeAdapter(servletRequest, servletResponse); + PrometheusHttpRequest request = adapter.getRequest(); + + assertThat(request.getHeaders("Accept").nextElement()).isEqualTo("text/plain"); + } + + @Test + public void testRequestGetMethod() { + HttpServletRequest servletRequest = mock(HttpServletRequest.class); + HttpServletResponse servletResponse = mock(HttpServletResponse.class); + when(servletRequest.getMethod()).thenReturn("GET"); + + HttpExchangeAdapter adapter = new HttpExchangeAdapter(servletRequest, servletResponse); + PrometheusHttpRequest request = adapter.getRequest(); + + assertThat(request.getMethod()).isEqualTo("GET"); + } + + @Test + public void testRequestGetRequestPath() { + HttpServletRequest servletRequest = mock(HttpServletRequest.class); + HttpServletResponse servletResponse = mock(HttpServletResponse.class); + when(servletRequest.getContextPath()).thenReturn("/app"); + when(servletRequest.getServletPath()).thenReturn("/metrics"); + when(servletRequest.getPathInfo()).thenReturn(null); + + HttpExchangeAdapter adapter = new HttpExchangeAdapter(servletRequest, servletResponse); + PrometheusHttpRequest request = adapter.getRequest(); + + assertThat(request.getRequestPath()).isEqualTo("/app/metrics"); + } + + @Test + public void testRequestGetRequestPathWithPathInfo() { + HttpServletRequest servletRequest = mock(HttpServletRequest.class); + HttpServletResponse servletResponse = mock(HttpServletResponse.class); + when(servletRequest.getContextPath()).thenReturn("/app"); + when(servletRequest.getServletPath()).thenReturn("/metrics"); + when(servletRequest.getPathInfo()).thenReturn("/extra"); + + HttpExchangeAdapter adapter = new HttpExchangeAdapter(servletRequest, servletResponse); + PrometheusHttpRequest request = adapter.getRequest(); + + assertThat(request.getRequestPath()).isEqualTo("/app/metrics/extra"); + } + + @Test + public void testResponseSetHeader() { + HttpServletRequest servletRequest = mock(HttpServletRequest.class); + HttpServletResponse servletResponse = mock(HttpServletResponse.class); + + HttpExchangeAdapter adapter = new HttpExchangeAdapter(servletRequest, servletResponse); + PrometheusHttpResponse response = adapter.getResponse(); + + response.setHeader("Content-Type", "text/plain"); + verify(servletResponse).setHeader("Content-Type", "text/plain"); + } + + @Test + public void testResponseSendHeadersAndGetBody() throws IOException { + HttpServletRequest servletRequest = mock(HttpServletRequest.class); + HttpServletResponse servletResponse = mock(HttpServletResponse.class); + ServletOutputStream outputStream = + new ServletOutputStream() { + @Override + public void write(int b) throws IOException {} + + @Override + public boolean isReady() { + return true; + } + + @Override + public void setWriteListener(WriteListener writeListener) {} + }; + when(servletResponse.getOutputStream()).thenReturn(outputStream); + + HttpExchangeAdapter adapter = new HttpExchangeAdapter(servletRequest, servletResponse); + PrometheusHttpResponse response = adapter.getResponse(); + + response.sendHeadersAndGetBody(200, 100); + + verify(servletResponse).setContentLength(100); + verify(servletResponse).setStatus(200); + verify(servletResponse).getOutputStream(); + } + + @Test + public void testResponseSendHeadersWithContentLengthAlreadySet() throws IOException { + HttpServletRequest servletRequest = mock(HttpServletRequest.class); + HttpServletResponse servletResponse = mock(HttpServletResponse.class); + ServletOutputStream outputStream = + new ServletOutputStream() { + @Override + public void write(int b) throws IOException {} + + @Override + public boolean isReady() { + return true; + } + + @Override + public void setWriteListener(WriteListener writeListener) {} + }; + when(servletResponse.getHeader("Content-Length")).thenReturn("50"); + when(servletResponse.getOutputStream()).thenReturn(outputStream); + + HttpExchangeAdapter adapter = new HttpExchangeAdapter(servletRequest, servletResponse); + PrometheusHttpResponse response = adapter.getResponse(); + + response.sendHeadersAndGetBody(200, 100); + + verify(servletResponse).setStatus(200); + verify(servletResponse).getOutputStream(); + } + + @Test + public void testHandleIOException() { + HttpServletRequest servletRequest = mock(HttpServletRequest.class); + HttpServletResponse servletResponse = mock(HttpServletResponse.class); + + HttpExchangeAdapter adapter = new HttpExchangeAdapter(servletRequest, servletResponse); + IOException exception = new IOException("Test exception"); + + assertThatExceptionOfType(IOException.class) + .isThrownBy(() -> adapter.handleException(exception)) + .withMessage("Test exception"); + } + + @Test + public void testHandleRuntimeException() { + HttpServletRequest servletRequest = mock(HttpServletRequest.class); + HttpServletResponse servletResponse = mock(HttpServletResponse.class); + + HttpExchangeAdapter adapter = new HttpExchangeAdapter(servletRequest, servletResponse); + RuntimeException exception = new RuntimeException("Test exception"); + + assertThatExceptionOfType(RuntimeException.class) + .isThrownBy(() -> adapter.handleException(exception)) + .withMessage("Test exception"); + } + + @Test + public void testClose() { + HttpServletRequest servletRequest = mock(HttpServletRequest.class); + HttpServletResponse servletResponse = mock(HttpServletResponse.class); + + HttpExchangeAdapter adapter = new HttpExchangeAdapter(servletRequest, servletResponse); + adapter.close(); // Should not throw + } +} diff --git a/prometheus-metrics-exporter-servlet-javax/src/test/java/io/prometheus/metrics/exporter/servlet/javax/PrometheusMetricsServletTest.java b/prometheus-metrics-exporter-servlet-javax/src/test/java/io/prometheus/metrics/exporter/servlet/javax/PrometheusMetricsServletTest.java new file mode 100644 index 000000000..f847ca60c --- /dev/null +++ b/prometheus-metrics-exporter-servlet-javax/src/test/java/io/prometheus/metrics/exporter/servlet/javax/PrometheusMetricsServletTest.java @@ -0,0 +1,82 @@ +package io.prometheus.metrics.exporter.servlet.javax; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import io.prometheus.metrics.core.metrics.Counter; +import io.prometheus.metrics.model.registry.PrometheusRegistry; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import javax.servlet.ServletOutputStream; +import javax.servlet.WriteListener; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class PrometheusMetricsServletTest { + + private PrometheusRegistry registry; + private Counter testCounter; + + @BeforeEach + public void setUp() { + registry = new PrometheusRegistry(); + testCounter = Counter.builder().name("test_counter").help("Test counter").register(registry); + testCounter.inc(42); + } + + @Test + public void testDoGetWritesMetrics() throws IOException { + HttpServletRequest request = mock(HttpServletRequest.class); + HttpServletResponse response = mock(HttpServletResponse.class); + + when(request.getQueryString()).thenReturn(null); + when(request.getMethod()).thenReturn("GET"); + when(request.getHeaders("Accept-Encoding")).thenReturn(Collections.emptyEnumeration()); + when(request.getHeaders("Accept")).thenReturn(Collections.emptyEnumeration()); + when(request.getContextPath()).thenReturn(""); + when(request.getServletPath()).thenReturn("/metrics"); + when(request.getPathInfo()).thenReturn(null); + + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + when(response.getOutputStream()) + .thenReturn( + new ServletOutputStream() { + @Override + public void write(int b) throws IOException { + outputStream.write(b); + } + + @Override + public boolean isReady() { + return true; + } + + @Override + public void setWriteListener(WriteListener writeListener) {} + }); + + PrometheusMetricsServlet servlet = new PrometheusMetricsServlet(registry); + servlet.doGet(request, response); + + String output = outputStream.toString(StandardCharsets.UTF_8.name()); + assertThat(output).contains("test_counter"); + assertThat(output).contains("42.0"); + } + + @Test + public void testServletUsesDefaultRegistry() { + PrometheusMetricsServlet servlet = new PrometheusMetricsServlet(); + assertThat(servlet).isNotNull(); + } + + @Test + public void testServletWithCustomRegistry() { + PrometheusMetricsServlet servlet = new PrometheusMetricsServlet(registry); + assertThat(servlet).isNotNull(); + } +} From b517091847c5db54cdbf7d3db4bec541f27a24cb Mon Sep 17 00:00:00 2001 From: Gregor Zeitlinger Date: Sat, 24 Jan 2026 16:57:04 +0100 Subject: [PATCH 04/10] refactor: simplify DropwizardExports by extending AbstractDropwizardExports and reducing code duplication Signed-off-by: Gregor Zeitlinger --- CODE_QUALITY_IMPROVEMENTS.md | 3 +- .../dropwizard/DropwizardExports.java | 249 ++++++---------- .../dropwizard5/DropwizardExports.java | 269 +++++++----------- .../internal/AbstractDropwizardExports.java | 234 +++++++++++++++ 4 files changed, 421 insertions(+), 334 deletions(-) create mode 100644 prometheus-metrics-instrumentation-dropwizard5/src/main/java/io/prometheus/metrics/instrumentation/dropwizard5/internal/AbstractDropwizardExports.java diff --git a/CODE_QUALITY_IMPROVEMENTS.md b/CODE_QUALITY_IMPROVEMENTS.md index edc1e4d6b..28984b36b 100644 --- a/CODE_QUALITY_IMPROVEMENTS.md +++ b/CODE_QUALITY_IMPROVEMENTS.md @@ -11,7 +11,7 @@ This document tracks code quality improvements for the Prometheus Java Client li - [x] `prometheus-metrics-exporter-opentelemetry-otel-agent-resources` - no tests ### 2. Eliminate Dropwizard Module Duplication -- [ ] Create shared base class or use generics for `prometheus-metrics-instrumentation-dropwizard` and `prometheus-metrics-instrumentation-dropwizard5` (~297 lines each, nearly identical) +- [x] Create shared base class or use generics for `prometheus-metrics-instrumentation-dropwizard` and `prometheus-metrics-instrumentation-dropwizard5` (~297 lines each, nearly identical) ### 3. Address Technical Debt (TODOs) - [ ] `prometheus-metrics-core/src/main/java/io/prometheus/metrics/core/metrics/Histogram.java:965` - "reset interval isn't tested yet" @@ -83,3 +83,4 @@ _Add notes here as items are completed:_ | Date | Item | Notes | |------|------|-------| | 2026-01-24 | Missing Test Coverage for Exporter Modules | Added 55 tests across 4 modules: exporter-common (22 tests), servlet-jakarta (14 tests), servlet-javax (14 tests), otel-agent-resources (5 tests). All tests passing. | +| 2026-01-24 | Eliminate Dropwizard Module Duplication | Created AbstractDropwizardExports base class (267 lines) with generic type parameters. Reduced dropwizard module from 297 to 209 lines (-88 lines, -30%), dropwizard5 module from 297 to 212 lines (-85 lines, -29%). All tests passing (32 tests dropwizard5, 13 tests dropwizard). | diff --git a/prometheus-metrics-instrumentation-dropwizard/src/main/java/io/prometheus/metrics/instrumentation/dropwizard/DropwizardExports.java b/prometheus-metrics-instrumentation-dropwizard/src/main/java/io/prometheus/metrics/instrumentation/dropwizard/DropwizardExports.java index 4ab03341e..0c1c8455f 100644 --- a/prometheus-metrics-instrumentation-dropwizard/src/main/java/io/prometheus/metrics/instrumentation/dropwizard/DropwizardExports.java +++ b/prometheus-metrics-instrumentation-dropwizard/src/main/java/io/prometheus/metrics/instrumentation/dropwizard/DropwizardExports.java @@ -10,32 +10,29 @@ import com.codahale.metrics.Snapshot; import com.codahale.metrics.Timer; import io.prometheus.metrics.instrumentation.dropwizard5.InvalidMetricHandler; +import io.prometheus.metrics.instrumentation.dropwizard5.internal.AbstractDropwizardExports; import io.prometheus.metrics.instrumentation.dropwizard5.labels.CustomLabelMapper; -import io.prometheus.metrics.model.registry.MultiCollector; import io.prometheus.metrics.model.registry.PrometheusRegistry; -import io.prometheus.metrics.model.snapshots.CounterSnapshot; -import io.prometheus.metrics.model.snapshots.GaugeSnapshot; -import io.prometheus.metrics.model.snapshots.MetricMetadata; -import io.prometheus.metrics.model.snapshots.MetricSnapshot; import io.prometheus.metrics.model.snapshots.MetricSnapshots; -import io.prometheus.metrics.model.snapshots.PrometheusNaming; -import io.prometheus.metrics.model.snapshots.Quantiles; -import io.prometheus.metrics.model.snapshots.SummarySnapshot; -import java.util.Collections; -import java.util.Map; -import java.util.concurrent.TimeUnit; -import java.util.function.BiFunction; -import java.util.logging.Level; -import java.util.logging.Logger; import javax.annotation.Nullable; -/** Collect Dropwizard metrics from a MetricRegistry. */ -public class DropwizardExports implements MultiCollector { - private static final Logger logger = Logger.getLogger(DropwizardExports.class.getName()); - private final MetricRegistry registry; - private final MetricFilter metricFilter; - @Nullable private final CustomLabelMapper labelMapper; - private final InvalidMetricHandler invalidMetricHandler; +/** + * Collect Dropwizard 4.x metrics from a MetricRegistry. + * + *

This is a thin wrapper around {@link AbstractDropwizardExports} that handles the Dropwizard + * 4.x specific API where metric names are Strings. + */ +public class DropwizardExports + extends AbstractDropwizardExports< + MetricRegistry, + MetricFilter, + Counter, + Gauge, + Histogram, + Timer, + Meter, + Metric, + Snapshot> { /** * Creates a new DropwizardExports and {@link MetricFilter#ALL}. @@ -43,11 +40,7 @@ public class DropwizardExports implements MultiCollector { * @param registry a metric registry to export in prometheus. */ public DropwizardExports(MetricRegistry registry) { - super(); - this.registry = registry; - this.metricFilter = MetricFilter.ALL; - this.labelMapper = null; - this.invalidMetricHandler = InvalidMetricHandler.ALWAYS_THROW; + this(registry, MetricFilter.ALL, null, InvalidMetricHandler.ALWAYS_THROW); } /** @@ -57,10 +50,7 @@ public DropwizardExports(MetricRegistry registry) { * @param metricFilter a custom metric filter. */ public DropwizardExports(MetricRegistry registry, MetricFilter metricFilter) { - this.registry = registry; - this.metricFilter = metricFilter; - this.labelMapper = null; - this.invalidMetricHandler = InvalidMetricHandler.ALWAYS_THROW; + this(registry, metricFilter, null, InvalidMetricHandler.ALWAYS_THROW); } /** @@ -70,176 +60,103 @@ public DropwizardExports(MetricRegistry registry, MetricFilter metricFilter) { */ public DropwizardExports( MetricRegistry registry, MetricFilter metricFilter, @Nullable CustomLabelMapper labelMapper) { - this.registry = registry; - this.metricFilter = metricFilter; - this.labelMapper = labelMapper; - this.invalidMetricHandler = InvalidMetricHandler.ALWAYS_THROW; + this(registry, metricFilter, labelMapper, InvalidMetricHandler.ALWAYS_THROW); } /** * @param registry a metric registry to export in prometheus. * @param metricFilter a custom metric filter. * @param labelMapper a labelMapper to use to map labels. + * @param invalidMetricHandler handler for invalid metrics. */ private DropwizardExports( MetricRegistry registry, MetricFilter metricFilter, @Nullable CustomLabelMapper labelMapper, InvalidMetricHandler invalidMetricHandler) { - this.registry = registry; - this.metricFilter = metricFilter; - this.labelMapper = labelMapper; - this.invalidMetricHandler = invalidMetricHandler; + super(registry, metricFilter, labelMapper, invalidMetricHandler); } - private static String getHelpMessage(String metricName, Metric metric) { - return String.format( - "Generated from Dropwizard metric import (metric=%s, type=%s)", - metricName, metric.getClass().getName()); + @Override + protected MetricSnapshots collectMetricSnapshots() { + MetricSnapshots.Builder metricSnapshots = MetricSnapshots.builder(); + // For Dropwizard 4.x, map keys are Strings, so we just use identity function + collectMetricKind( + metricSnapshots, registry.getGauges(metricFilter), this::fromGauge, key -> key); + collectMetricKind( + metricSnapshots, registry.getCounters(metricFilter), this::fromCounter, key -> key); + collectMetricKind( + metricSnapshots, registry.getHistograms(metricFilter), this::fromHistogram, key -> key); + collectMetricKind( + metricSnapshots, registry.getTimers(metricFilter), this::fromTimer, key -> key); + collectMetricKind( + metricSnapshots, registry.getMeters(metricFilter), this::fromMeter, key -> key); + return metricSnapshots.build(); } - private MetricMetadata getMetricMetaData(String metricName, Metric metric) { - String name = labelMapper != null ? labelMapper.getName(metricName) : metricName; - return new MetricMetadata( - PrometheusNaming.sanitizeMetricName(name), getHelpMessage(metricName, metric)); + @Override + protected long getCounterCount(Counter counter) { + return counter.getCount(); } - /** - * Export counter as Prometheus Gauge. - */ - MetricSnapshot fromCounter(String dropwizardName, Counter counter) { - MetricMetadata metadata = getMetricMetaData(dropwizardName, counter); - CounterSnapshot.CounterDataPointSnapshot.Builder dataPointBuilder = - CounterSnapshot.CounterDataPointSnapshot.builder() - .value(Long.valueOf(counter.getCount()).doubleValue()); - if (labelMapper != null) { - dataPointBuilder.labels( - labelMapper.getLabels(dropwizardName, Collections.emptyList(), Collections.emptyList())); - } - return new CounterSnapshot(metadata, Collections.singletonList(dataPointBuilder.build())); + @Override + protected Object getGaugeValue(Gauge gauge) { + return gauge.getValue(); } - /** Export gauge as a prometheus gauge. */ - @Nullable - MetricSnapshot fromGauge(String dropwizardName, Gauge gauge) { - Object obj = gauge.getValue(); - double value; - if (obj instanceof Number) { - value = ((Number) obj).doubleValue(); - } else if (obj instanceof Boolean) { - value = ((Boolean) obj) ? 1 : 0; - } else { - logger.log( - Level.FINE, - String.format( - "Invalid type for Gauge %s: %s", - PrometheusNaming.sanitizeMetricName(dropwizardName), - obj == null ? "null" : obj.getClass().getName())); - return null; - } - MetricMetadata metadata = getMetricMetaData(dropwizardName, gauge); - GaugeSnapshot.GaugeDataPointSnapshot.Builder dataPointBuilder = - GaugeSnapshot.GaugeDataPointSnapshot.builder().value(value); - if (labelMapper != null) { - dataPointBuilder.labels( - labelMapper.getLabels(dropwizardName, Collections.emptyList(), Collections.emptyList())); - } - return new GaugeSnapshot(metadata, Collections.singletonList(dataPointBuilder.build())); + @Override + protected Snapshot getHistogramSnapshot(Histogram histogram) { + return histogram.getSnapshot(); } - /** - * Export a histogram snapshot as a prometheus SUMMARY. - * - * @param dropwizardName metric name. - * @param snapshot the histogram snapshot. - * @param count the total sample count for this snapshot. - * @param factor a factor to apply to histogram values. - */ - MetricSnapshot fromSnapshotAndCount( - String dropwizardName, Snapshot snapshot, long count, double factor, String helpMessage) { - Quantiles quantiles = - Quantiles.builder() - .quantile(0.5, snapshot.getMedian() * factor) - .quantile(0.75, snapshot.get75thPercentile() * factor) - .quantile(0.95, snapshot.get95thPercentile() * factor) - .quantile(0.98, snapshot.get98thPercentile() * factor) - .quantile(0.99, snapshot.get99thPercentile() * factor) - .quantile(0.999, snapshot.get999thPercentile() * factor) - .build(); + @Override + protected long getHistogramCount(Histogram histogram) { + return histogram.getCount(); + } - String name = labelMapper != null ? labelMapper.getName(dropwizardName) : dropwizardName; - MetricMetadata metadata = - new MetricMetadata(PrometheusNaming.sanitizeMetricName(name), helpMessage); - SummarySnapshot.SummaryDataPointSnapshot.Builder dataPointBuilder = - SummarySnapshot.SummaryDataPointSnapshot.builder().quantiles(quantiles).count(count); - if (labelMapper != null) { - dataPointBuilder.labels( - labelMapper.getLabels(dropwizardName, Collections.emptyList(), Collections.emptyList())); - } - return new SummarySnapshot(metadata, Collections.singletonList(dataPointBuilder.build())); + @Override + protected Snapshot getTimerSnapshot(Timer timer) { + return timer.getSnapshot(); } - /** Convert histogram snapshot. */ - MetricSnapshot fromHistogram(String dropwizardName, Histogram histogram) { - return fromSnapshotAndCount( - dropwizardName, - histogram.getSnapshot(), - histogram.getCount(), - 1.0, - getHelpMessage(dropwizardName, histogram)); + @Override + protected long getTimerCount(Timer timer) { + return timer.getCount(); } - /** Export Dropwizard Timer as a histogram. Use TIME_UNIT as time unit. */ - MetricSnapshot fromTimer(String dropwizardName, Timer timer) { - return fromSnapshotAndCount( - dropwizardName, - timer.getSnapshot(), - timer.getCount(), - 1.0D / TimeUnit.SECONDS.toNanos(1L), - getHelpMessage(dropwizardName, timer)); + @Override + protected long getMeterCount(Meter meter) { + return meter.getCount(); } - /** Export a Meter as a prometheus COUNTER. */ - MetricSnapshot fromMeter(String dropwizardName, Meter meter) { - MetricMetadata metadata = getMetricMetaData(dropwizardName + "_total", meter); - CounterSnapshot.CounterDataPointSnapshot.Builder dataPointBuilder = - CounterSnapshot.CounterDataPointSnapshot.builder().value(meter.getCount()); - if (labelMapper != null) { - dataPointBuilder.labels( - labelMapper.getLabels(dropwizardName, Collections.emptyList(), Collections.emptyList())); - } - return new CounterSnapshot(metadata, Collections.singletonList(dataPointBuilder.build())); + @Override + protected double getMedian(Snapshot snapshot) { + return snapshot.getMedian(); } @Override - public MetricSnapshots collect() { - MetricSnapshots.Builder metricSnapshots = MetricSnapshots.builder(); - collectMetricKind(metricSnapshots, registry.getGauges(metricFilter), this::fromGauge); - collectMetricKind(metricSnapshots, registry.getCounters(metricFilter), this::fromCounter); - collectMetricKind(metricSnapshots, registry.getHistograms(metricFilter), this::fromHistogram); - collectMetricKind(metricSnapshots, registry.getTimers(metricFilter), this::fromTimer); - collectMetricKind(metricSnapshots, registry.getMeters(metricFilter), this::fromMeter); - return metricSnapshots.build(); + protected double get75thPercentile(Snapshot snapshot) { + return snapshot.get75thPercentile(); } - private void collectMetricKind( - MetricSnapshots.Builder builder, - Map metric, - BiFunction toSnapshot) { - for (Map.Entry entry : metric.entrySet()) { - String metricName = entry.getKey(); - try { - MetricSnapshot snapshot = toSnapshot.apply(metricName, entry.getValue()); - if (snapshot != null) { - builder.metricSnapshot(snapshot); - } - } catch (Exception e) { - if (!invalidMetricHandler.suppressException(metricName, e)) { - throw e; - } - } - } + @Override + protected double get95thPercentile(Snapshot snapshot) { + return snapshot.get95thPercentile(); + } + + @Override + protected double get98thPercentile(Snapshot snapshot) { + return snapshot.get98thPercentile(); + } + + @Override + protected double get99thPercentile(Snapshot snapshot) { + return snapshot.get99thPercentile(); + } + + @Override + protected double get999thPercentile(Snapshot snapshot) { + return snapshot.get999thPercentile(); } public static Builder builder() { diff --git a/prometheus-metrics-instrumentation-dropwizard5/src/main/java/io/prometheus/metrics/instrumentation/dropwizard5/DropwizardExports.java b/prometheus-metrics-instrumentation-dropwizard5/src/main/java/io/prometheus/metrics/instrumentation/dropwizard5/DropwizardExports.java index c68d26f49..86c64b54e 100644 --- a/prometheus-metrics-instrumentation-dropwizard5/src/main/java/io/prometheus/metrics/instrumentation/dropwizard5/DropwizardExports.java +++ b/prometheus-metrics-instrumentation-dropwizard5/src/main/java/io/prometheus/metrics/instrumentation/dropwizard5/DropwizardExports.java @@ -10,44 +10,37 @@ import io.dropwizard.metrics5.MetricRegistry; import io.dropwizard.metrics5.Snapshot; import io.dropwizard.metrics5.Timer; +import io.prometheus.metrics.instrumentation.dropwizard5.internal.AbstractDropwizardExports; import io.prometheus.metrics.instrumentation.dropwizard5.labels.CustomLabelMapper; -import io.prometheus.metrics.model.registry.MultiCollector; import io.prometheus.metrics.model.registry.PrometheusRegistry; -import io.prometheus.metrics.model.snapshots.CounterSnapshot; -import io.prometheus.metrics.model.snapshots.GaugeSnapshot; -import io.prometheus.metrics.model.snapshots.MetricMetadata; -import io.prometheus.metrics.model.snapshots.MetricSnapshot; import io.prometheus.metrics.model.snapshots.MetricSnapshots; -import io.prometheus.metrics.model.snapshots.PrometheusNaming; -import io.prometheus.metrics.model.snapshots.Quantiles; -import io.prometheus.metrics.model.snapshots.SummarySnapshot; -import java.util.Collections; -import java.util.Map; -import java.util.concurrent.TimeUnit; -import java.util.function.BiFunction; -import java.util.logging.Level; -import java.util.logging.Logger; import javax.annotation.Nullable; -/** Collect Dropwizard metrics from a MetricRegistry. */ -public class DropwizardExports implements MultiCollector { - private static final Logger logger = Logger.getLogger(DropwizardExports.class.getName()); - private final MetricRegistry registry; - private final MetricFilter metricFilter; - @Nullable private final CustomLabelMapper labelMapper; - private final InvalidMetricHandler invalidMetricHandler; +/** + * Collect Dropwizard 5.x metrics from a MetricRegistry. + * + *

This is a thin wrapper around {@link AbstractDropwizardExports} that handles the Dropwizard + * 5.x specific API where metric names are {@link MetricName} objects. + */ +public class DropwizardExports + extends AbstractDropwizardExports< + MetricRegistry, + MetricFilter, + Counter, + Gauge, + Histogram, + Timer, + Meter, + Metric, + Snapshot> { /** - * Creates a new DropwizardExports and {@link MetricFilter#ALL}. + * Creates a new DropwizardExports with {@link MetricFilter#ALL}. * * @param registry a metric registry to export in prometheus. */ public DropwizardExports(MetricRegistry registry) { - super(); - this.registry = registry; - this.metricFilter = MetricFilter.ALL; - this.labelMapper = null; - this.invalidMetricHandler = InvalidMetricHandler.ALWAYS_THROW; + this(registry, MetricFilter.ALL, null, InvalidMetricHandler.ALWAYS_THROW); } /** @@ -57,10 +50,7 @@ public DropwizardExports(MetricRegistry registry) { * @param metricFilter a custom metric filter. */ public DropwizardExports(MetricRegistry registry, MetricFilter metricFilter) { - this.registry = registry; - this.metricFilter = metricFilter; - this.labelMapper = null; - this.invalidMetricHandler = InvalidMetricHandler.ALWAYS_THROW; + this(registry, metricFilter, null, InvalidMetricHandler.ALWAYS_THROW); } /** @@ -70,176 +60,121 @@ public DropwizardExports(MetricRegistry registry, MetricFilter metricFilter) { */ public DropwizardExports( MetricRegistry registry, MetricFilter metricFilter, @Nullable CustomLabelMapper labelMapper) { - this.registry = registry; - this.metricFilter = metricFilter; - this.labelMapper = labelMapper; - this.invalidMetricHandler = InvalidMetricHandler.ALWAYS_THROW; + this(registry, metricFilter, labelMapper, InvalidMetricHandler.ALWAYS_THROW); } /** * @param registry a metric registry to export in prometheus. * @param metricFilter a custom metric filter. * @param labelMapper a labelMapper to use to map labels. + * @param invalidMetricHandler handler for invalid metrics. */ private DropwizardExports( MetricRegistry registry, MetricFilter metricFilter, @Nullable CustomLabelMapper labelMapper, InvalidMetricHandler invalidMetricHandler) { - this.registry = registry; - this.metricFilter = metricFilter; - this.labelMapper = labelMapper; - this.invalidMetricHandler = invalidMetricHandler; + super(registry, metricFilter, labelMapper, invalidMetricHandler); } - private static String getHelpMessage(String metricName, Metric metric) { - return String.format( - "Generated from Dropwizard metric import (metric=%s, type=%s)", - metricName, metric.getClass().getName()); + @Override + protected MetricSnapshots collectMetricSnapshots() { + MetricSnapshots.Builder metricSnapshots = MetricSnapshots.builder(); + collectMetricKind( + metricSnapshots, + registry.getGauges(metricFilter), + this::fromGauge, + this::extractMetricName); + collectMetricKind( + metricSnapshots, + registry.getCounters(metricFilter), + this::fromCounter, + this::extractMetricName); + collectMetricKind( + metricSnapshots, + registry.getHistograms(metricFilter), + this::fromHistogram, + this::extractMetricName); + collectMetricKind( + metricSnapshots, + registry.getTimers(metricFilter), + this::fromTimer, + this::extractMetricName); + collectMetricKind( + metricSnapshots, + registry.getMeters(metricFilter), + this::fromMeter, + this::extractMetricName); + return metricSnapshots.build(); } - private MetricMetadata getMetricMetaData(String metricName, Metric metric) { - String name = labelMapper != null ? labelMapper.getName(metricName) : metricName; - return new MetricMetadata( - PrometheusNaming.sanitizeMetricName(name), getHelpMessage(metricName, metric)); + private String extractMetricName(MetricName metricName) { + return metricName.getKey(); } - /** - * Export counter as Prometheus Gauge. - */ - MetricSnapshot fromCounter(String dropwizardName, Counter counter) { - MetricMetadata metadata = getMetricMetaData(dropwizardName, counter); - CounterSnapshot.CounterDataPointSnapshot.Builder dataPointBuilder = - CounterSnapshot.CounterDataPointSnapshot.builder() - .value(Long.valueOf(counter.getCount()).doubleValue()); - if (labelMapper != null) { - dataPointBuilder.labels( - labelMapper.getLabels(dropwizardName, Collections.emptyList(), Collections.emptyList())); - } - return new CounterSnapshot(metadata, Collections.singletonList(dataPointBuilder.build())); + @Override + protected long getCounterCount(Counter counter) { + return counter.getCount(); } - /** Export gauge as a prometheus gauge. */ - @Nullable - MetricSnapshot fromGauge(String dropwizardName, Gauge gauge) { - Object obj = gauge.getValue(); - double value; - if (obj instanceof Number) { - value = ((Number) obj).doubleValue(); - } else if (obj instanceof Boolean) { - value = ((Boolean) obj) ? 1 : 0; - } else { - logger.log( - Level.FINE, - String.format( - "Invalid type for Gauge %s: %s", - PrometheusNaming.sanitizeMetricName(dropwizardName), - obj == null ? "null" : obj.getClass().getName())); - return null; - } - MetricMetadata metadata = getMetricMetaData(dropwizardName, gauge); - GaugeSnapshot.GaugeDataPointSnapshot.Builder dataPointBuilder = - GaugeSnapshot.GaugeDataPointSnapshot.builder().value(value); - if (labelMapper != null) { - dataPointBuilder.labels( - labelMapper.getLabels(dropwizardName, Collections.emptyList(), Collections.emptyList())); - } - return new GaugeSnapshot(metadata, Collections.singletonList(dataPointBuilder.build())); + @Override + protected Object getGaugeValue(Gauge gauge) { + return gauge.getValue(); } - /** - * Export a histogram snapshot as a prometheus SUMMARY. - * - * @param dropwizardName metric name. - * @param snapshot the histogram snapshot. - * @param count the total sample count for this snapshot. - * @param factor a factor to apply to histogram values. - */ - MetricSnapshot fromSnapshotAndCount( - String dropwizardName, Snapshot snapshot, long count, double factor, String helpMessage) { - Quantiles quantiles = - Quantiles.builder() - .quantile(0.5, snapshot.getMedian() * factor) - .quantile(0.75, snapshot.get75thPercentile() * factor) - .quantile(0.95, snapshot.get95thPercentile() * factor) - .quantile(0.98, snapshot.get98thPercentile() * factor) - .quantile(0.99, snapshot.get99thPercentile() * factor) - .quantile(0.999, snapshot.get999thPercentile() * factor) - .build(); + @Override + protected Snapshot getHistogramSnapshot(Histogram histogram) { + return histogram.getSnapshot(); + } - String name = labelMapper != null ? labelMapper.getName(dropwizardName) : dropwizardName; - MetricMetadata metadata = - new MetricMetadata(PrometheusNaming.sanitizeMetricName(name), helpMessage); - SummarySnapshot.SummaryDataPointSnapshot.Builder dataPointBuilder = - SummarySnapshot.SummaryDataPointSnapshot.builder().quantiles(quantiles).count(count); - if (labelMapper != null) { - dataPointBuilder.labels( - labelMapper.getLabels(dropwizardName, Collections.emptyList(), Collections.emptyList())); - } - return new SummarySnapshot(metadata, Collections.singletonList(dataPointBuilder.build())); + @Override + protected long getHistogramCount(Histogram histogram) { + return histogram.getCount(); + } + + @Override + protected Snapshot getTimerSnapshot(Timer timer) { + return timer.getSnapshot(); } - /** Convert histogram snapshot. */ - MetricSnapshot fromHistogram(String dropwizardName, Histogram histogram) { - return fromSnapshotAndCount( - dropwizardName, - histogram.getSnapshot(), - histogram.getCount(), - 1.0, - getHelpMessage(dropwizardName, histogram)); + @Override + protected long getTimerCount(Timer timer) { + return timer.getCount(); } - /** Export Dropwizard Timer as a histogram. Use TIME_UNIT as time unit. */ - MetricSnapshot fromTimer(String dropwizardName, Timer timer) { - return fromSnapshotAndCount( - dropwizardName, - timer.getSnapshot(), - timer.getCount(), - 1.0D / TimeUnit.SECONDS.toNanos(1L), - getHelpMessage(dropwizardName, timer)); + @Override + protected long getMeterCount(Meter meter) { + return meter.getCount(); } - /** Export a Meter as a prometheus COUNTER. */ - MetricSnapshot fromMeter(String dropwizardName, Meter meter) { - MetricMetadata metadata = getMetricMetaData(dropwizardName + "_total", meter); - CounterSnapshot.CounterDataPointSnapshot.Builder dataPointBuilder = - CounterSnapshot.CounterDataPointSnapshot.builder().value(meter.getCount()); - if (labelMapper != null) { - dataPointBuilder.labels( - labelMapper.getLabels(dropwizardName, Collections.emptyList(), Collections.emptyList())); - } - return new CounterSnapshot(metadata, Collections.singletonList(dataPointBuilder.build())); + @Override + protected double getMedian(Snapshot snapshot) { + return snapshot.getMedian(); } @Override - public MetricSnapshots collect() { - MetricSnapshots.Builder metricSnapshots = MetricSnapshots.builder(); - collectMetricKind(metricSnapshots, registry.getGauges(metricFilter), this::fromGauge); - collectMetricKind(metricSnapshots, registry.getCounters(metricFilter), this::fromCounter); - collectMetricKind(metricSnapshots, registry.getHistograms(metricFilter), this::fromHistogram); - collectMetricKind(metricSnapshots, registry.getTimers(metricFilter), this::fromTimer); - collectMetricKind(metricSnapshots, registry.getMeters(metricFilter), this::fromMeter); - return metricSnapshots.build(); + protected double get75thPercentile(Snapshot snapshot) { + return snapshot.get75thPercentile(); } - private void collectMetricKind( - MetricSnapshots.Builder builder, - Map metric, - BiFunction toSnapshot) { - for (Map.Entry entry : metric.entrySet()) { - String metricName = entry.getKey().getKey(); - try { - MetricSnapshot snapshot = toSnapshot.apply(metricName, entry.getValue()); - if (snapshot != null) { - builder.metricSnapshot(snapshot); - } - } catch (Exception e) { - if (!invalidMetricHandler.suppressException(metricName, e)) { - throw e; - } - } - } + @Override + protected double get95thPercentile(Snapshot snapshot) { + return snapshot.get95thPercentile(); + } + + @Override + protected double get98thPercentile(Snapshot snapshot) { + return snapshot.get98thPercentile(); + } + + @Override + protected double get99thPercentile(Snapshot snapshot) { + return snapshot.get99thPercentile(); + } + + @Override + protected double get999thPercentile(Snapshot snapshot) { + return snapshot.get999thPercentile(); } public static Builder builder() { diff --git a/prometheus-metrics-instrumentation-dropwizard5/src/main/java/io/prometheus/metrics/instrumentation/dropwizard5/internal/AbstractDropwizardExports.java b/prometheus-metrics-instrumentation-dropwizard5/src/main/java/io/prometheus/metrics/instrumentation/dropwizard5/internal/AbstractDropwizardExports.java new file mode 100644 index 000000000..1b56b82dd --- /dev/null +++ b/prometheus-metrics-instrumentation-dropwizard5/src/main/java/io/prometheus/metrics/instrumentation/dropwizard5/internal/AbstractDropwizardExports.java @@ -0,0 +1,234 @@ +package io.prometheus.metrics.instrumentation.dropwizard5.internal; + +import io.prometheus.metrics.instrumentation.dropwizard5.InvalidMetricHandler; +import io.prometheus.metrics.instrumentation.dropwizard5.labels.CustomLabelMapper; +import io.prometheus.metrics.model.registry.MultiCollector; +import io.prometheus.metrics.model.snapshots.CounterSnapshot; +import io.prometheus.metrics.model.snapshots.GaugeSnapshot; +import io.prometheus.metrics.model.snapshots.MetricMetadata; +import io.prometheus.metrics.model.snapshots.MetricSnapshot; +import io.prometheus.metrics.model.snapshots.MetricSnapshots; +import io.prometheus.metrics.model.snapshots.PrometheusNaming; +import io.prometheus.metrics.model.snapshots.Quantiles; +import io.prometheus.metrics.model.snapshots.SummarySnapshot; +import java.util.Collections; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.function.BiFunction; +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.annotation.Nullable; + +/** + * Abstract base class for Dropwizard metrics exporters. Contains all the common logic for + * converting Dropwizard metrics to Prometheus metrics. Subclasses only need to implement {@link + * #collectMetricSnapshots()} to handle version-specific registry APIs. + * + * @param The Dropwizard MetricRegistry type + * @param The Dropwizard MetricFilter type + * @param The Dropwizard Counter type + * @param The Dropwizard Gauge type + * @param The Dropwizard Histogram type + * @param The Dropwizard Timer type + * @param The Dropwizard Meter type + * @param The Dropwizard Metric base type + * @param The Dropwizard Snapshot type + */ +public abstract class AbstractDropwizardExports< + TRegistry, TFilter, TCounter, TGauge, THistogram, TTimer, TMeter, TMetric, TSnapshot> + implements MultiCollector { + + private static final Logger logger = Logger.getLogger(AbstractDropwizardExports.class.getName()); + + protected final TRegistry registry; + protected final TFilter metricFilter; + @Nullable protected final CustomLabelMapper labelMapper; + protected final InvalidMetricHandler invalidMetricHandler; + + protected AbstractDropwizardExports( + TRegistry registry, + TFilter metricFilter, + @Nullable CustomLabelMapper labelMapper, + InvalidMetricHandler invalidMetricHandler) { + this.registry = registry; + this.metricFilter = metricFilter; + this.labelMapper = labelMapper; + this.invalidMetricHandler = invalidMetricHandler; + } + + protected static String getHelpMessage(String metricName, Object metric) { + return String.format( + "Generated from Dropwizard metric import (metric=%s, type=%s)", + metricName, metric.getClass().getName()); + } + + protected MetricMetadata getMetricMetaData(String metricName, TMetric metric) { + String name = labelMapper != null ? labelMapper.getName(metricName) : metricName; + return new MetricMetadata( + PrometheusNaming.sanitizeMetricName(name), getHelpMessage(metricName, metric)); + } + + /** + * Export counter as Prometheus Gauge. + */ + protected MetricSnapshot fromCounter(String dropwizardName, TCounter counter) { + long count = getCounterCount(counter); + MetricMetadata metadata = getMetricMetaData(dropwizardName, (TMetric) counter); + CounterSnapshot.CounterDataPointSnapshot.Builder dataPointBuilder = + CounterSnapshot.CounterDataPointSnapshot.builder().value(Long.valueOf(count).doubleValue()); + if (labelMapper != null) { + dataPointBuilder.labels( + labelMapper.getLabels(dropwizardName, Collections.emptyList(), Collections.emptyList())); + } + return new CounterSnapshot(metadata, Collections.singletonList(dataPointBuilder.build())); + } + + /** Export gauge as a prometheus gauge. */ + @Nullable + protected MetricSnapshot fromGauge(String dropwizardName, TGauge gauge) { + Object obj = getGaugeValue(gauge); + double value; + if (obj instanceof Number) { + value = ((Number) obj).doubleValue(); + } else if (obj instanceof Boolean) { + value = ((Boolean) obj) ? 1 : 0; + } else { + logger.log( + Level.FINE, + String.format( + "Invalid type for Gauge %s: %s", + PrometheusNaming.sanitizeMetricName(dropwizardName), + obj == null ? "null" : obj.getClass().getName())); + return null; + } + MetricMetadata metadata = getMetricMetaData(dropwizardName, (TMetric) gauge); + GaugeSnapshot.GaugeDataPointSnapshot.Builder dataPointBuilder = + GaugeSnapshot.GaugeDataPointSnapshot.builder().value(value); + if (labelMapper != null) { + dataPointBuilder.labels( + labelMapper.getLabels(dropwizardName, Collections.emptyList(), Collections.emptyList())); + } + return new GaugeSnapshot(metadata, Collections.singletonList(dataPointBuilder.build())); + } + + /** + * Export a histogram snapshot as a prometheus SUMMARY. + * + * @param dropwizardName metric name. + * @param snapshot the histogram snapshot. + * @param count the total sample count for this snapshot. + * @param factor a factor to apply to histogram values. + */ + protected MetricSnapshot fromSnapshotAndCount( + String dropwizardName, TSnapshot snapshot, long count, double factor, String helpMessage) { + Quantiles quantiles = + Quantiles.builder() + .quantile(0.5, getMedian(snapshot) * factor) + .quantile(0.75, get75thPercentile(snapshot) * factor) + .quantile(0.95, get95thPercentile(snapshot) * factor) + .quantile(0.98, get98thPercentile(snapshot) * factor) + .quantile(0.99, get99thPercentile(snapshot) * factor) + .quantile(0.999, get999thPercentile(snapshot) * factor) + .build(); + + String name = labelMapper != null ? labelMapper.getName(dropwizardName) : dropwizardName; + MetricMetadata metadata = + new MetricMetadata(PrometheusNaming.sanitizeMetricName(name), helpMessage); + SummarySnapshot.SummaryDataPointSnapshot.Builder dataPointBuilder = + SummarySnapshot.SummaryDataPointSnapshot.builder().quantiles(quantiles).count(count); + if (labelMapper != null) { + dataPointBuilder.labels( + labelMapper.getLabels(dropwizardName, Collections.emptyList(), Collections.emptyList())); + } + return new SummarySnapshot(metadata, Collections.singletonList(dataPointBuilder.build())); + } + + /** Convert histogram snapshot. */ + protected MetricSnapshot fromHistogram(String dropwizardName, THistogram histogram) { + TSnapshot snapshot = getHistogramSnapshot(histogram); + long count = getHistogramCount(histogram); + return fromSnapshotAndCount( + dropwizardName, snapshot, count, 1.0, getHelpMessage(dropwizardName, histogram)); + } + + /** Export Dropwizard Timer as a histogram. Use TIME_UNIT as time unit. */ + protected MetricSnapshot fromTimer(String dropwizardName, TTimer timer) { + TSnapshot snapshot = getTimerSnapshot(timer); + long count = getTimerCount(timer); + return fromSnapshotAndCount( + dropwizardName, + snapshot, + count, + 1.0D / TimeUnit.SECONDS.toNanos(1L), + getHelpMessage(dropwizardName, timer)); + } + + /** Export a Meter as a prometheus COUNTER. */ + protected MetricSnapshot fromMeter(String dropwizardName, TMeter meter) { + MetricMetadata metadata = getMetricMetaData(dropwizardName + "_total", (TMetric) meter); + CounterSnapshot.CounterDataPointSnapshot.Builder dataPointBuilder = + CounterSnapshot.CounterDataPointSnapshot.builder().value(getMeterCount(meter)); + if (labelMapper != null) { + dataPointBuilder.labels( + labelMapper.getLabels(dropwizardName, Collections.emptyList(), Collections.emptyList())); + } + return new CounterSnapshot(metadata, Collections.singletonList(dataPointBuilder.build())); + } + + @Override + public MetricSnapshots collect() { + return collectMetricSnapshots(); + } + + protected void collectMetricKind( + MetricSnapshots.Builder builder, + Map metrics, + BiFunction toSnapshot, + java.util.function.Function keyExtractor) { + for (Map.Entry entry : metrics.entrySet()) { + String metricName = keyExtractor.apply(entry.getKey()); + try { + MetricSnapshot snapshot = toSnapshot.apply(metricName, entry.getValue()); + if (snapshot != null) { + builder.metricSnapshot(snapshot); + } + } catch (Exception e) { + if (!invalidMetricHandler.suppressException(metricName, e)) { + throw e; + } + } + } + } + + // Abstract methods to be implemented by version-specific subclasses + + /** Collect all metric snapshots from the registry. */ + protected abstract MetricSnapshots collectMetricSnapshots(); + + protected abstract long getCounterCount(TCounter counter); + + protected abstract Object getGaugeValue(TGauge gauge); + + protected abstract TSnapshot getHistogramSnapshot(THistogram histogram); + + protected abstract long getHistogramCount(THistogram histogram); + + protected abstract TSnapshot getTimerSnapshot(TTimer timer); + + protected abstract long getTimerCount(TTimer timer); + + protected abstract long getMeterCount(TMeter meter); + + protected abstract double getMedian(TSnapshot snapshot); + + protected abstract double get75thPercentile(TSnapshot snapshot); + + protected abstract double get95thPercentile(TSnapshot snapshot); + + protected abstract double get98thPercentile(TSnapshot snapshot); + + protected abstract double get99thPercentile(TSnapshot snapshot); + + protected abstract double get999thPercentile(TSnapshot snapshot); +} From 164c73221586988b11070b83a96488305feedf6f Mon Sep 17 00:00:00 2001 From: Gregor Zeitlinger Date: Mon, 26 Jan 2026 10:58:03 +0100 Subject: [PATCH 05/10] fix Signed-off-by: Gregor Zeitlinger --- .../pom.xml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/prometheus-metrics-exporter-opentelemetry-otel-agent-resources/pom.xml b/prometheus-metrics-exporter-opentelemetry-otel-agent-resources/pom.xml index b87712160..55e8b6336 100644 --- a/prometheus-metrics-exporter-opentelemetry-otel-agent-resources/pom.xml +++ b/prometheus-metrics-exporter-opentelemetry-otel-agent-resources/pom.xml @@ -21,6 +21,8 @@ io.prometheus.otel.resource.attributes 1.29.0 + + 0.50 From 6f5bba146c0bf0255e53b7c64e726c8b2fb7caf7 Mon Sep 17 00:00:00 2001 From: Gregor Zeitlinger Date: Mon, 26 Jan 2026 11:03:34 +0100 Subject: [PATCH 06/10] fix Signed-off-by: Gregor Zeitlinger --- .../pom.xml | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/prometheus-metrics-exporter-opentelemetry-otel-agent-resources/pom.xml b/prometheus-metrics-exporter-opentelemetry-otel-agent-resources/pom.xml index 55e8b6336..16fd43422 100644 --- a/prometheus-metrics-exporter-opentelemetry-otel-agent-resources/pom.xml +++ b/prometheus-metrics-exporter-opentelemetry-otel-agent-resources/pom.xml @@ -61,6 +61,40 @@ + + org.jacoco + jacoco-maven-plugin + + + **/io/opentelemetry/** + + + + + check + + check + + + + + CLASS + + + LINE + COVEREDRATIO + ${jacoco.line-coverage} + + + + io.opentelemetry.** + + + + + + + From 94fd6d270483059e46211f242889383fbe1540ee Mon Sep 17 00:00:00 2001 From: Gregor Zeitlinger Date: Mon, 26 Jan 2026 11:22:34 +0100 Subject: [PATCH 07/10] fix Signed-off-by: Gregor Zeitlinger --- .editorconfig | 4 ++-- .github/super-linter.env | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.editorconfig b/.editorconfig index 9ecadc44d..6aed0058b 100644 --- a/.editorconfig +++ b/.editorconfig @@ -4,10 +4,10 @@ root = true max_line_length = 100 indent_size = 2 -[{version-rules.xml,maven-wrapper.properties,checkstyle.xml,docker-compose.yaml,docker-compose.yml,Dockerfile,example_target_info.json,mise.toml,mvnm,mvnw.cmd,generate-protobuf.sh,super-linter.env,.gitleaksignore,*.json5}] +[{version-rules.xml,maven-wrapper.properties,checkstyle.xml,docker-compose.yaml,docker-compose.yml,Dockerfile,example_target_info.json,mise.toml,mvnm,mvnw.cmd,generate-protobuf.sh,.gitleaksignore,*.json5}] max_line_length = 200 -[{grafana-dashboard-*.json,.editorconfig}] +[{grafana-dashboard-*.json,.editorconfig,super-linter.env}] max_line_length = 300 [pom.xml] diff --git a/.github/super-linter.env b/.github/super-linter.env index 1c7584fb5..1ebf97898 100644 --- a/.github/super-linter.env +++ b/.github/super-linter.env @@ -1,4 +1,4 @@ -FILTER_REGEX_EXCLUDE=mvnw|src/main/generated/.*|docs/themes/.*|keystore.pkcs12|.*.java|prometheus-metrics-exporter-opentelemetry-shaded/pom.xml|CODE_OF_CONDUCT.md +FILTER_REGEX_EXCLUDE=mvnw|src/main/generated/.*|docs/themes/.*|keystore.pkcs12|.*.java|prometheus-metrics-exporter-opentelemetry-shaded/pom.xml|CODE_OF_CONDUCT.md|CLAUDE.md|CODE_QUALITY_IMPROVEMENTS.md IGNORE_GITIGNORED_FILES=true JAVA_FILE_NAME=google_checks.xml LOG_LEVEL=ERROR From 98c11fb99d0a905ac470377560344fd4b01a9ec3 Mon Sep 17 00:00:00 2001 From: Gregor Zeitlinger Date: Mon, 26 Jan 2026 12:09:30 +0100 Subject: [PATCH 08/10] how to use super linter Signed-off-by: Gregor Zeitlinger --- CLAUDE.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CLAUDE.md b/CLAUDE.md index aa227d4b9..587834e92 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -76,6 +76,12 @@ Pre-built instrumentations: `prometheus-metrics-instrumentation-jvm`, `-caffeine - **Assertions in tests**: Use static imports from AssertJ (`import static org.assertj.core.api.Assertions.assertThat`) - **Empty catch blocks**: Use `ignored` as the exception variable name +## Linting + +- **IMPORTANT**: Always run `mise run lint:super-linter` after modifying non-Java files (YAML, Markdown, shell scripts, JSON, etc.) +- Super-linter is configured to only show ERROR-level messages via `LOG_LEVEL=ERROR` in `.github/super-linter.env` +- Local super-linter version is pinned to match CI (see `.mise/tasks/lint/super-linter.sh`) + ## Testing - JUnit 5 (Jupiter) with `@Test` annotations From 0f4b0b1f006c0d5691faa25685a68e7df8acb0b8 Mon Sep 17 00:00:00 2001 From: Gregor Zeitlinger Date: Mon, 26 Jan 2026 12:19:23 +0100 Subject: [PATCH 09/10] fix Signed-off-by: Gregor Zeitlinger --- .../dropwizard5/internal/AbstractDropwizardExports.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/prometheus-metrics-instrumentation-dropwizard5/src/main/java/io/prometheus/metrics/instrumentation/dropwizard5/internal/AbstractDropwizardExports.java b/prometheus-metrics-instrumentation-dropwizard5/src/main/java/io/prometheus/metrics/instrumentation/dropwizard5/internal/AbstractDropwizardExports.java index 1b56b82dd..df774927e 100644 --- a/prometheus-metrics-instrumentation-dropwizard5/src/main/java/io/prometheus/metrics/instrumentation/dropwizard5/internal/AbstractDropwizardExports.java +++ b/prometheus-metrics-instrumentation-dropwizard5/src/main/java/io/prometheus/metrics/instrumentation/dropwizard5/internal/AbstractDropwizardExports.java @@ -72,6 +72,7 @@ protected MetricMetadata getMetricMetaData(String metricName, TMetric metric) { * Export counter as Prometheus Gauge. */ + @SuppressWarnings("unchecked") protected MetricSnapshot fromCounter(String dropwizardName, TCounter counter) { long count = getCounterCount(counter); MetricMetadata metadata = getMetricMetaData(dropwizardName, (TMetric) counter); @@ -85,6 +86,7 @@ protected MetricSnapshot fromCounter(String dropwizardName, TCounter counter) { } /** Export gauge as a prometheus gauge. */ + @SuppressWarnings("unchecked") @Nullable protected MetricSnapshot fromGauge(String dropwizardName, TGauge gauge) { Object obj = getGaugeValue(gauge); @@ -165,6 +167,7 @@ protected MetricSnapshot fromTimer(String dropwizardName, TTimer timer) { } /** Export a Meter as a prometheus COUNTER. */ + @SuppressWarnings("unchecked") protected MetricSnapshot fromMeter(String dropwizardName, TMeter meter) { MetricMetadata metadata = getMetricMetaData(dropwizardName + "_total", (TMetric) meter); CounterSnapshot.CounterDataPointSnapshot.Builder dataPointBuilder = From d1855b393b278c9c41b989bad61dd24cfa119cc0 Mon Sep 17 00:00:00 2001 From: Gregor Zeitlinger Date: Mon, 26 Jan 2026 12:28:13 +0100 Subject: [PATCH 10/10] fix Signed-off-by: Gregor Zeitlinger --- CLAUDE.md | 3 +- .../internal/AbstractDropwizardExports.java | 79 +++++++++---------- 2 files changed, 41 insertions(+), 41 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 587834e92..ba985e587 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -76,8 +76,9 @@ Pre-built instrumentations: `prometheus-metrics-instrumentation-jvm`, `-caffeine - **Assertions in tests**: Use static imports from AssertJ (`import static org.assertj.core.api.Assertions.assertThat`) - **Empty catch blocks**: Use `ignored` as the exception variable name -## Linting +## Linting and Validation +- **IMPORTANT**: Always run `mise run build` after modifying Java files to ensure all lints, code formatting (Spotless), static analysis (Error Prone), and checkstyle checks pass - **IMPORTANT**: Always run `mise run lint:super-linter` after modifying non-Java files (YAML, Markdown, shell scripts, JSON, etc.) - Super-linter is configured to only show ERROR-level messages via `LOG_LEVEL=ERROR` in `.github/super-linter.env` - Local super-linter version is pinned to match CI (see `.mise/tasks/lint/super-linter.sh`) diff --git a/prometheus-metrics-instrumentation-dropwizard5/src/main/java/io/prometheus/metrics/instrumentation/dropwizard5/internal/AbstractDropwizardExports.java b/prometheus-metrics-instrumentation-dropwizard5/src/main/java/io/prometheus/metrics/instrumentation/dropwizard5/internal/AbstractDropwizardExports.java index df774927e..279459b02 100644 --- a/prometheus-metrics-instrumentation-dropwizard5/src/main/java/io/prometheus/metrics/instrumentation/dropwizard5/internal/AbstractDropwizardExports.java +++ b/prometheus-metrics-instrumentation-dropwizard5/src/main/java/io/prometheus/metrics/instrumentation/dropwizard5/internal/AbstractDropwizardExports.java @@ -24,30 +24,29 @@ * converting Dropwizard metrics to Prometheus metrics. Subclasses only need to implement {@link * #collectMetricSnapshots()} to handle version-specific registry APIs. * - * @param The Dropwizard MetricRegistry type - * @param The Dropwizard MetricFilter type - * @param The Dropwizard Counter type - * @param The Dropwizard Gauge type - * @param The Dropwizard Histogram type - * @param The Dropwizard Timer type - * @param The Dropwizard Meter type - * @param The Dropwizard Metric base type - * @param The Dropwizard Snapshot type + * @param The Dropwizard MetricRegistry type + * @param The Dropwizard MetricFilter type + * @param The Dropwizard Counter type + * @param The Dropwizard Gauge type + * @param The Dropwizard Histogram type + * @param The Dropwizard Timer type + * @param The Dropwizard Meter type + * @param The Dropwizard Metric base type + * @param The Dropwizard Snapshot type */ -public abstract class AbstractDropwizardExports< - TRegistry, TFilter, TCounter, TGauge, THistogram, TTimer, TMeter, TMetric, TSnapshot> +public abstract class AbstractDropwizardExports implements MultiCollector { private static final Logger logger = Logger.getLogger(AbstractDropwizardExports.class.getName()); - protected final TRegistry registry; - protected final TFilter metricFilter; + protected final R registry; + protected final F metricFilter; @Nullable protected final CustomLabelMapper labelMapper; protected final InvalidMetricHandler invalidMetricHandler; protected AbstractDropwizardExports( - TRegistry registry, - TFilter metricFilter, + R registry, + F metricFilter, @Nullable CustomLabelMapper labelMapper, InvalidMetricHandler invalidMetricHandler) { this.registry = registry; @@ -62,7 +61,7 @@ protected static String getHelpMessage(String metricName, Object metric) { metricName, metric.getClass().getName()); } - protected MetricMetadata getMetricMetaData(String metricName, TMetric metric) { + protected MetricMetadata getMetricMetaData(String metricName, B metric) { String name = labelMapper != null ? labelMapper.getName(metricName) : metricName; return new MetricMetadata( PrometheusNaming.sanitizeMetricName(name), getHelpMessage(metricName, metric)); @@ -73,9 +72,9 @@ protected MetricMetadata getMetricMetaData(String metricName, TMetric metric) { * href="https://prometheus.io/docs/concepts/metric_types/#gauge">Gauge. */ @SuppressWarnings("unchecked") - protected MetricSnapshot fromCounter(String dropwizardName, TCounter counter) { + protected MetricSnapshot fromCounter(String dropwizardName, C counter) { long count = getCounterCount(counter); - MetricMetadata metadata = getMetricMetaData(dropwizardName, (TMetric) counter); + MetricMetadata metadata = getMetricMetaData(dropwizardName, (B) counter); CounterSnapshot.CounterDataPointSnapshot.Builder dataPointBuilder = CounterSnapshot.CounterDataPointSnapshot.builder().value(Long.valueOf(count).doubleValue()); if (labelMapper != null) { @@ -88,7 +87,7 @@ protected MetricSnapshot fromCounter(String dropwizardName, TCounter counter) { /** Export gauge as a prometheus gauge. */ @SuppressWarnings("unchecked") @Nullable - protected MetricSnapshot fromGauge(String dropwizardName, TGauge gauge) { + protected MetricSnapshot fromGauge(String dropwizardName, G gauge) { Object obj = getGaugeValue(gauge); double value; if (obj instanceof Number) { @@ -104,7 +103,7 @@ protected MetricSnapshot fromGauge(String dropwizardName, TGauge gauge) { obj == null ? "null" : obj.getClass().getName())); return null; } - MetricMetadata metadata = getMetricMetaData(dropwizardName, (TMetric) gauge); + MetricMetadata metadata = getMetricMetaData(dropwizardName, (B) gauge); GaugeSnapshot.GaugeDataPointSnapshot.Builder dataPointBuilder = GaugeSnapshot.GaugeDataPointSnapshot.builder().value(value); if (labelMapper != null) { @@ -123,7 +122,7 @@ protected MetricSnapshot fromGauge(String dropwizardName, TGauge gauge) { * @param factor a factor to apply to histogram values. */ protected MetricSnapshot fromSnapshotAndCount( - String dropwizardName, TSnapshot snapshot, long count, double factor, String helpMessage) { + String dropwizardName, S snapshot, long count, double factor, String helpMessage) { Quantiles quantiles = Quantiles.builder() .quantile(0.5, getMedian(snapshot) * factor) @@ -147,16 +146,16 @@ protected MetricSnapshot fromSnapshotAndCount( } /** Convert histogram snapshot. */ - protected MetricSnapshot fromHistogram(String dropwizardName, THistogram histogram) { - TSnapshot snapshot = getHistogramSnapshot(histogram); + protected MetricSnapshot fromHistogram(String dropwizardName, H histogram) { + S snapshot = getHistogramSnapshot(histogram); long count = getHistogramCount(histogram); return fromSnapshotAndCount( dropwizardName, snapshot, count, 1.0, getHelpMessage(dropwizardName, histogram)); } /** Export Dropwizard Timer as a histogram. Use TIME_UNIT as time unit. */ - protected MetricSnapshot fromTimer(String dropwizardName, TTimer timer) { - TSnapshot snapshot = getTimerSnapshot(timer); + protected MetricSnapshot fromTimer(String dropwizardName, T timer) { + S snapshot = getTimerSnapshot(timer); long count = getTimerCount(timer); return fromSnapshotAndCount( dropwizardName, @@ -168,8 +167,8 @@ protected MetricSnapshot fromTimer(String dropwizardName, TTimer timer) { /** Export a Meter as a prometheus COUNTER. */ @SuppressWarnings("unchecked") - protected MetricSnapshot fromMeter(String dropwizardName, TMeter meter) { - MetricMetadata metadata = getMetricMetaData(dropwizardName + "_total", (TMetric) meter); + protected MetricSnapshot fromMeter(String dropwizardName, M meter) { + MetricMetadata metadata = getMetricMetaData(dropwizardName + "_total", (B) meter); CounterSnapshot.CounterDataPointSnapshot.Builder dataPointBuilder = CounterSnapshot.CounterDataPointSnapshot.builder().value(getMeterCount(meter)); if (labelMapper != null) { @@ -209,29 +208,29 @@ protected void collectMetricKind( /** Collect all metric snapshots from the registry. */ protected abstract MetricSnapshots collectMetricSnapshots(); - protected abstract long getCounterCount(TCounter counter); + protected abstract long getCounterCount(C counter); - protected abstract Object getGaugeValue(TGauge gauge); + protected abstract Object getGaugeValue(G gauge); - protected abstract TSnapshot getHistogramSnapshot(THistogram histogram); + protected abstract S getHistogramSnapshot(H histogram); - protected abstract long getHistogramCount(THistogram histogram); + protected abstract long getHistogramCount(H histogram); - protected abstract TSnapshot getTimerSnapshot(TTimer timer); + protected abstract S getTimerSnapshot(T timer); - protected abstract long getTimerCount(TTimer timer); + protected abstract long getTimerCount(T timer); - protected abstract long getMeterCount(TMeter meter); + protected abstract long getMeterCount(M meter); - protected abstract double getMedian(TSnapshot snapshot); + protected abstract double getMedian(S snapshot); - protected abstract double get75thPercentile(TSnapshot snapshot); + protected abstract double get75thPercentile(S snapshot); - protected abstract double get95thPercentile(TSnapshot snapshot); + protected abstract double get95thPercentile(S snapshot); - protected abstract double get98thPercentile(TSnapshot snapshot); + protected abstract double get98thPercentile(S snapshot); - protected abstract double get99thPercentile(TSnapshot snapshot); + protected abstract double get99thPercentile(S snapshot); - protected abstract double get999thPercentile(TSnapshot snapshot); + protected abstract double get999thPercentile(S snapshot); }