From 66f4dc877736ebe05beeb72a24181a607f8a58e3 Mon Sep 17 00:00:00 2001 From: Gregor Zeitlinger Date: Fri, 22 Nov 2024 15:35:19 +0100 Subject: [PATCH] make Protobuf optional (#1190) * make protobuf optional Signed-off-by: Gregor Zeitlinger * make protobuf optional Signed-off-by: Gregor Zeitlinger * make protobuf optional Signed-off-by: Gregor Zeitlinger * make protobuf optional Signed-off-by: Gregor Zeitlinger * make protobuf optional Signed-off-by: Gregor Zeitlinger * make protobuf optional Signed-off-by: Gregor Zeitlinger * make protobuf optional Signed-off-by: Gregor Zeitlinger * plugin management Signed-off-by: Gregor Zeitlinger * smoke test Signed-off-by: Gregor Zeitlinger * smoke test Signed-off-by: Gregor Zeitlinger * smoke test Signed-off-by: Gregor Zeitlinger * smoke test Signed-off-by: Gregor Zeitlinger * release automation Signed-off-by: Gregor Zeitlinger * cache delegate Signed-off-by: Gregor Zeitlinger * annotations should be provided Signed-off-by: Gregor Zeitlinger * fix rebase Signed-off-by: Gregor Zeitlinger * fix rebase Signed-off-by: Gregor Zeitlinger * checkstyle Signed-off-by: Gregor Zeitlinger * add docs Signed-off-by: Gregor Zeitlinger --------- Signed-off-by: Gregor Zeitlinger --- .github/workflows/build.yml | 3 +- .github/workflows/native-tests.yml | 21 ++ .github/workflows/release.yml | 2 +- RELEASING.md | 10 - docs/content/exporters/formats.md | 27 +- docs/content/getting-started/quickstart.md | 8 + docs/content/internals/model.md | 10 +- integration-tests/it-common/pom.xml | 12 +- .../client/it/common/ExporterTest.java | 158 +++++++++++ .../it-exporter-no-protobuf/pom.xml | 69 +++++ .../exporter/httpserver/HTTPServerSample.java | 96 +++++++ .../it-exporter/it-exporter-test/pom.xml | 11 - .../metrics/it/exporter/test/ExporterIT.java | 248 ++++-------------- .../src/test/resources/debug-openmetrics.txt | 13 + .../src/test/resources/debug-protobuf.txt | 42 +++ .../src/test/resources/debug-text.txt | 10 + .../it-exporter/it-no-protobuf-test/pom.xml | 27 ++ .../metrics/it/noprotobuf/NoProtobufIT.java | 23 ++ integration-tests/it-exporter/pom.xml | 11 +- integration-tests/it-pushgateway/pom.xml | 1 - .../it-spring-boot-smoke-test/pom.xml | 98 +++++++ .../metrics/it/springboot/Application.java | 12 + .../src/main/resources/application.yaml | 5 + .../it/springboot/ApplicationTest.java | 35 +++ integration-tests/pom.xml | 8 +- pom.xml | 21 +- .../metrics/core/metrics/CounterTest.java | 9 +- .../metrics/core/metrics/HistogramTest.java | 12 +- .../metrics/core/metrics/InfoTest.java | 9 +- prometheus-metrics-exporter-common/pom.xml | 6 + .../common/PrometheusScrapeHandler.java | 13 +- .../pom.xml | 2 +- .../ResourceAttributesFromOtelAgent.java | 0 .../src/main/resources/lib/.gitignore | 0 .../pom.xml | 3 +- .../exporter/pushgateway/PushGateway.java | 29 +- .../generate-protobuf.sh | 2 +- prometheus-metrics-exposition-formats/pom.xml | 9 +- .../ExpositionFormatWriter.java | 14 - .../expositionformats/ProtobufUtil.java | 13 - .../PrometheusProtobufWriterImpl.java} | 27 +- .../internal/ProtobufUtil.java | 19 ++ .../ExpositionFormatsTest.java | 6 +- .../pom.xml | 36 +++ .../ExpositionFormatWriter.java | 32 +++ .../expositionformats/ExpositionFormats.java | 0 .../OpenMetricsTextFormatWriter.java | 0 .../PrometheusProtobufWriter.java | 73 ++++++ .../PrometheusTextFormatWriter.java | 0 .../expositionformats/TextFormatUtil.java | 6 - .../pom.xml | 2 +- .../pom.xml | 2 +- .../pom.xml | 2 +- .../pom.xml | 2 +- .../pom.xml | 2 +- scripts/build-release.sh | 11 + scripts/run-native-tests.sh | 7 + 57 files changed, 987 insertions(+), 342 deletions(-) create mode 100644 .github/workflows/native-tests.yml create mode 100644 integration-tests/it-common/src/test/java/io/prometheus/client/it/common/ExporterTest.java create mode 100644 integration-tests/it-exporter/it-exporter-no-protobuf/pom.xml create mode 100644 integration-tests/it-exporter/it-exporter-no-protobuf/src/main/java/io/prometheus/metrics/it/exporter/httpserver/HTTPServerSample.java create mode 100644 integration-tests/it-exporter/it-exporter-test/src/test/resources/debug-openmetrics.txt create mode 100644 integration-tests/it-exporter/it-exporter-test/src/test/resources/debug-protobuf.txt create mode 100644 integration-tests/it-exporter/it-exporter-test/src/test/resources/debug-text.txt create mode 100644 integration-tests/it-exporter/it-no-protobuf-test/pom.xml create mode 100644 integration-tests/it-exporter/it-no-protobuf-test/src/test/java/io/prometheus/metrics/it/noprotobuf/NoProtobufIT.java create mode 100644 integration-tests/it-spring-boot-smoke-test/pom.xml create mode 100644 integration-tests/it-spring-boot-smoke-test/src/main/java/io/prometheus/metrics/it/springboot/Application.java create mode 100644 integration-tests/it-spring-boot-smoke-test/src/main/resources/application.yaml create mode 100644 integration-tests/it-spring-boot-smoke-test/src/test/java/io/prometheus/metrics/it/springboot/ApplicationTest.java rename {otel-agent-resources => prometheus-metrics-exporter-opentelemetry-otel-agent-resources}/pom.xml (96%) rename {otel-agent-resources => prometheus-metrics-exporter-opentelemetry-otel-agent-resources}/src/main/java/io/prometheus/otelagent/ResourceAttributesFromOtelAgent.java (100%) rename {otel-agent-resources => prometheus-metrics-exporter-opentelemetry-otel-agent-resources}/src/main/resources/lib/.gitignore (100%) delete mode 100644 prometheus-metrics-exposition-formats/src/main/java/io/prometheus/metrics/expositionformats/ExpositionFormatWriter.java delete mode 100644 prometheus-metrics-exposition-formats/src/main/java/io/prometheus/metrics/expositionformats/ProtobufUtil.java rename prometheus-metrics-exposition-formats/src/main/java/io/prometheus/metrics/expositionformats/{PrometheusProtobufWriter.java => internal/PrometheusProtobufWriterImpl.java} (94%) create mode 100644 prometheus-metrics-exposition-formats/src/main/java/io/prometheus/metrics/expositionformats/internal/ProtobufUtil.java create mode 100644 prometheus-metrics-exposition-textformats/pom.xml create mode 100644 prometheus-metrics-exposition-textformats/src/main/java/io/prometheus/metrics/expositionformats/ExpositionFormatWriter.java rename {prometheus-metrics-exposition-formats => prometheus-metrics-exposition-textformats}/src/main/java/io/prometheus/metrics/expositionformats/ExpositionFormats.java (100%) rename {prometheus-metrics-exposition-formats => prometheus-metrics-exposition-textformats}/src/main/java/io/prometheus/metrics/expositionformats/OpenMetricsTextFormatWriter.java (100%) create mode 100644 prometheus-metrics-exposition-textformats/src/main/java/io/prometheus/metrics/expositionformats/PrometheusProtobufWriter.java rename {prometheus-metrics-exposition-formats => prometheus-metrics-exposition-textformats}/src/main/java/io/prometheus/metrics/expositionformats/PrometheusTextFormatWriter.java (100%) rename {prometheus-metrics-exposition-formats => prometheus-metrics-exposition-textformats}/src/main/java/io/prometheus/metrics/expositionformats/TextFormatUtil.java (90%) create mode 100755 scripts/build-release.sh create mode 100755 scripts/run-native-tests.sh diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a9175969b..dd0668345 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -25,7 +25,6 @@ jobs: PROTO_GENERATION: true REQUIRE_PROTO_UP_TO_DATE: true run: | - echo "Java version: $(java -version) in $(which java), Maven version: $(mvn -v)" - echo "JAVA_HOME: $JAVA_HOME" + mvn -v ./mvnw clean install ./mvnw javadoc:javadoc -P javadoc # just to check if javadoc is generated diff --git a/.github/workflows/native-tests.yml b/.github/workflows/native-tests.yml new file mode 100644 index 000000000..f632f730f --- /dev/null +++ b/.github/workflows/native-tests.yml @@ -0,0 +1,21 @@ +name: GraalVM Native Tests + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +jobs: + native-tests: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Set up JDK + uses: actions/setup-java@v4 + with: + java-version: '21' + distribution: graalvm + cache: 'maven' + - name: Run the Maven verify phase + run: ./scripts/run-native-tests.sh diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index be3f8ca80..7e7d81574 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -28,7 +28,7 @@ jobs: cache: 'maven' - name: Build with Maven - run: mvn -B package -P release -Dmaven.test.skip=true + run: ./scripts/build-release.sh ${{ github.ref_name }} - name: Set up Apache Maven Central uses: actions/setup-java@v4 diff --git a/RELEASING.md b/RELEASING.md index ffba31e95..af5d1b862 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -1,13 +1,3 @@ -## Update Version - -In a new PR, update the version in `pom.xml` using - -```shell -mvn versions:set -DnewVersion= -``` - -Commit the changes and open a PR. - ## Create a Release 1. Go to https://github.com/prometheus/client_java/releases diff --git a/docs/content/exporters/formats.md b/docs/content/exporters/formats.md index fc78ec963..79ae46e06 100644 --- a/docs/content/exporters/formats.md +++ b/docs/content/exporters/formats.md @@ -11,16 +11,35 @@ All exporters the following exposition formats: Moreover, gzip encoding is supported for each of these formats. -Scraping with a Prometheus server ---------------------------------- +## Scraping with a Prometheus server The Prometheus server sends an `Accept` header to specify which format is requested. By default, the Prometheus server will scrape OpenMetrics text format with gzip encoding. If the Prometheus server is started with `--enable-feature=native-histograms`, it will scrape Prometheus protobuf format instead. -Viewing with a Web Browser --------------------------- +## Viewing with a Web Browser If you view the `/metrics` endpoint with your Web browser you will see Prometheus text format. For quick debugging of the other formats, exporters provide a `debug` URL parameter: * `/metrics?debug=openmetrics`: View OpenMetrics text format. * `/metrics?debug=text`: View Prometheus text format. * `/metrics?debug=prometheus-protobuf`: View a text representation of the Prometheus protobuf format. + +## Exclude protobuf exposition format + +You can exclude the protobuf exposition format by including the +`prometheus-metrics-exposition-textformats` module and excluding the +`prometheus-metrics-exposition-formats` module in your build file. + +For example, in Maven: + +```xml + + io.prometheus + prometheus-metrics-exporter-httpserver + + + io.prometheus + prometheus-metrics-exposition-formats + + + +``` diff --git a/docs/content/getting-started/quickstart.md b/docs/content/getting-started/quickstart.md index d4598ee0a..59acd0701 100644 --- a/docs/content/getting-started/quickstart.md +++ b/docs/content/getting-started/quickstart.md @@ -45,6 +45,14 @@ implementation 'io.prometheus:prometheus-metrics-exporter-httpserver:$version' There are alternative exporters as well, for example if you are using a Servlet container like Tomcat or Undertow you might want to use `prometheus-exporter-servlet-jakarta` rather than a standalone HTTP server. +{{< hint type=note >}} + +If you do not use the protobuf exposition format, you can +[exclude](../../exporters/formats#exclude-protobuf-exposition-format) +it from the dependencies. + +{{< /hint >}} + # Dependency management A Bill of Material diff --git a/docs/content/internals/model.md b/docs/content/internals/model.md index e02517777..c1f1e7b6a 100644 --- a/docs/content/internals/model.md +++ b/docs/content/internals/model.md @@ -7,15 +7,13 @@ The illustration below shows the internal architecture of the Prometheus Java cl ![Internal architecture of the Prometheus Java client library](/client_java/images/model.png) -prometheus-metrics-core ------------------------ +## prometheus-metrics-core This is the user facing metrics library, implementing the core metric types, like [Counter](/client_java/api/io/prometheus/metrics/core/metrics/Counter.html), [Gauge](/client_java/api/io/prometheus/metrics/core/metrics/Gauge.html) [Histogram](/client_java/api/io/prometheus/metrics/core/metrics/Histogram.html), and so on. All metric types implement the [Collector](/client_java/api/io/prometheus/metrics/model/registry/Collector.html) interface, i.e. they provide a [collect()](/client_java/api/io/prometheus/metrics/model/registry/Collector.html#collect()) method to produce snapshots. -prometheus-metrics-model ------------------------- +## prometheus-metrics-model The model is an internal library, implementing read-only immutable snapshots. These snapshots are returned by the [Collector.collect()](/client_java/api/io/prometheus/metrics/model/registry/Collector.html#collect()) method. @@ -23,9 +21,9 @@ There is no need for users to use `prometheus-metrics-model` directly. Users sho However, maintainers of 3rd party metrics libraries might want to use `prometheus-metrics-model` if they want to add Prometheus exposition formats to their metrics library. -exporters and exposition formats --------------------------------- +## Exporters and exposition formats The `prometheus-metrics-exposition-formats` module converts snapshots to Prometheus exposition formats, like text format, OpenMetrics text format, or Prometheus protobuf format. The exporters like `prometheus-metrics-exporter-httpserver` or `prometheus-metrics-exporter-servlet-jakarta` use this to convert snapshots into the right format depending on the `Accept` header in the scrape request. + diff --git a/integration-tests/it-common/pom.xml b/integration-tests/it-common/pom.xml index 70a2c91b5..6c7044978 100644 --- a/integration-tests/it-common/pom.xml +++ b/integration-tests/it-common/pom.xml @@ -1,5 +1,6 @@ - + 4.0.0 @@ -11,11 +12,18 @@ it-common Integration Tests - Common Utilities - http://github.com/prometheus/client_java Common utilities for integration tests + + + io.prometheus + prometheus-metrics-exposition-formats + ${project.version} + + + diff --git a/integration-tests/it-common/src/test/java/io/prometheus/client/it/common/ExporterTest.java b/integration-tests/it-common/src/test/java/io/prometheus/client/it/common/ExporterTest.java new file mode 100644 index 000000000..e1b194d7c --- /dev/null +++ b/integration-tests/it-common/src/test/java/io/prometheus/client/it/common/ExporterTest.java @@ -0,0 +1,158 @@ +package io.prometheus.client.it.common; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; + +import io.prometheus.metrics.expositionformats.generated.com_google_protobuf_4_28_3.Metrics; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.URISyntaxException; +import java.net.URL; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.zip.GZIPInputStream; +import org.apache.commons.io.IOUtils; +import org.junit.jupiter.api.AfterEach; +import org.testcontainers.containers.BindMode; +import org.testcontainers.containers.GenericContainer; + +public abstract class ExporterTest { + private final GenericContainer sampleAppContainer; + private final Volume sampleAppVolume; + protected final String sampleApp; + + public ExporterTest(String sampleApp) throws IOException, URISyntaxException { + this.sampleApp = sampleApp; + this.sampleAppVolume = + Volume.create("it-exporter") + .copy("../../it-" + sampleApp + "/target/" + sampleApp + ".jar"); + this.sampleAppContainer = + new GenericContainer<>("openjdk:17") + .withFileSystemBind(sampleAppVolume.getHostPath(), "/app", BindMode.READ_ONLY) + .withWorkingDirectory("/app") + .withLogConsumer(LogConsumer.withPrefix(sampleApp)) + .withExposedPorts(9400); + } + + // @BeforeEach? + protected void start() { + start("success"); + } + + protected void start(String outcome) { + sampleAppContainer + .withCommand("java", "-jar", "/app/" + sampleApp + ".jar", "9400", outcome) + .start(); + } + + @AfterEach + public void tearDown() throws IOException { + sampleAppContainer.stop(); + sampleAppVolume.remove(); + } + + public static void assertContentType(String expected, String actual) { + if (!expected.replace(" ", "").equals(actual)) { + assertThat(actual).isEqualTo(expected); + } + } + + protected Response scrape(String method, String queryString, String... requestHeaders) + throws IOException { + return scrape( + method, + new URL( + "http://localhost:" + + sampleAppContainer.getMappedPort(9400) + + "/metrics?" + + queryString), + requestHeaders); + } + + public static Response scrape(String method, URL url, String... requestHeaders) + throws IOException { + long timeoutMillis = TimeUnit.SECONDS.toMillis(5); + HttpURLConnection con = (HttpURLConnection) url.openConnection(); + con.setRequestMethod(method); + for (int i = 0; i < requestHeaders.length; i += 2) { + con.setRequestProperty(requestHeaders[i], requestHeaders[i + 1]); + } + long start = System.currentTimeMillis(); + Exception exception = null; + while (System.currentTimeMillis() - start < timeoutMillis) { + try { + if (con.getResponseCode() == 200) { + return new Response( + con.getResponseCode(), + con.getHeaderFields(), + IOUtils.toByteArray(con.getInputStream())); + } else { + return new Response( + con.getResponseCode(), + con.getHeaderFields(), + IOUtils.toByteArray(con.getErrorStream())); + } + } catch (Exception e) { + exception = e; + try { + Thread.sleep(100); + } catch (InterruptedException ignored) { + // ignore + } + } + } + if (exception != null) { + exception.printStackTrace(); + } + fail("timeout while getting metrics from " + url); + return null; // will not happen + } + + public static class Response { + public final int status; + private final Map headers; + public final byte[] body; + + private Response(int status, Map> headers, byte[] body) { + this.status = status; + this.headers = new HashMap<>(headers.size()); + this.body = body; + for (Map.Entry> entry : headers.entrySet()) { + if (entry.getKey() + != null) { // HttpUrlConnection uses pseudo key "null" for the status line + this.headers.put(entry.getKey().toLowerCase(Locale.ROOT), entry.getValue().get(0)); + } + } + } + + public String getHeader(String name) { + // HTTP headers are case-insensitive + return headers.get(name.toLowerCase(Locale.ROOT)); + } + + public String stringBody() { + return new String(body, UTF_8); + } + + public String gzipBody() throws IOException { + return new String( + IOUtils.toByteArray(new GZIPInputStream(new ByteArrayInputStream(body))), UTF_8); + } + + public List protoBody() throws IOException { + List metrics = new ArrayList<>(); + InputStream in = new ByteArrayInputStream(body); + while (in.available() > 0) { + metrics.add(Metrics.MetricFamily.parseDelimitedFrom(in)); + } + return metrics; + } + } +} diff --git a/integration-tests/it-exporter/it-exporter-no-protobuf/pom.xml b/integration-tests/it-exporter/it-exporter-no-protobuf/pom.xml new file mode 100644 index 000000000..67e20e8e2 --- /dev/null +++ b/integration-tests/it-exporter/it-exporter-no-protobuf/pom.xml @@ -0,0 +1,69 @@ + + + 4.0.0 + + + io.prometheus + it-exporter + 1.3.3 + + + it-exporter-no-protobuf + + Integration Tests - HTTPServer Exporter Sample - no protobuf + + HTTPServer Sample for the Exporter Integration Test without protobuf + + + + + io.prometheus + prometheus-metrics-exporter-httpserver + ${project.version} + + + io.prometheus + prometheus-metrics-exposition-formats + + + + + io.prometheus + prometheus-metrics-core + ${project.version} + + + io.prometheus + prometheus-metrics-exposition-formats + + + + + + + exporter-no-protobuf + + + org.apache.maven.plugins + maven-shade-plugin + + + package + + shade + + + + + io.prometheus.metrics.it.exporter.httpserver.HTTPServerSample + + + + + + + + + diff --git a/integration-tests/it-exporter/it-exporter-no-protobuf/src/main/java/io/prometheus/metrics/it/exporter/httpserver/HTTPServerSample.java b/integration-tests/it-exporter/it-exporter-no-protobuf/src/main/java/io/prometheus/metrics/it/exporter/httpserver/HTTPServerSample.java new file mode 100644 index 000000000..dba07e51f --- /dev/null +++ b/integration-tests/it-exporter/it-exporter-no-protobuf/src/main/java/io/prometheus/metrics/it/exporter/httpserver/HTTPServerSample.java @@ -0,0 +1,96 @@ +package io.prometheus.metrics.it.exporter.httpserver; + +import io.prometheus.metrics.core.metrics.Counter; +import io.prometheus.metrics.core.metrics.Gauge; +import io.prometheus.metrics.core.metrics.Info; +import io.prometheus.metrics.exporter.httpserver.HTTPServer; +import io.prometheus.metrics.model.registry.Collector; +import io.prometheus.metrics.model.registry.PrometheusRegistry; +import io.prometheus.metrics.model.snapshots.Unit; +import java.io.IOException; + +public class HTTPServerSample { + + enum Mode { + success, + error + } + + public static void main(String[] args) throws IOException, InterruptedException { + + if (args.length != 2) { + System.err.println("Usage: java -jar exporter-httpserver-sample.jar "); + System.err.println("Where mode is \"success\" or \"error\"."); + System.exit(1); + } + + int port = parsePortOrExit(args[0]); + Mode mode = parseModeOrExit(args[1]); + + run(mode, port); + } + + private static void run(Mode mode, int port) throws IOException, InterruptedException { + Counter counter = + Counter.builder() + .name("uptime_seconds_total") + .help("total number of seconds since this application was started") + .unit(Unit.SECONDS) + .register(); + counter.inc(17); + + Info info = + Info.builder() + .name("integration_test_info") + .help("Info metric on this integration test") + .labelNames("test_name") + .register(); + info.addLabelValues("exporter-httpserver-sample"); + + Gauge gauge = + Gauge.builder() + .name("temperature_celsius") + .help("Temperature in Celsius") + .unit(Unit.CELSIUS) + .labelNames("location") + .register(); + gauge.labelValues("inside").set(23.0); + gauge.labelValues("outside").set(27.0); + + if (mode == Mode.error) { + Collector failingCollector = + () -> { + throw new RuntimeException("Simulating an error."); + }; + + PrometheusRegistry.defaultRegistry.register(failingCollector); + } + + HTTPServer server = HTTPServer.builder().port(port).buildAndStart(); + + System.out.println( + "HTTPServer listening on port http://localhost:" + server.getPort() + "/metrics"); + Thread.currentThread().join(); // wait forever + } + + private static int parsePortOrExit(String port) { + try { + return Integer.parseInt(port); + } catch (NumberFormatException e) { + System.err.println("\"" + port + "\": Invalid port number."); + System.exit(1); + } + return 0; // this won't happen + } + + private static Mode parseModeOrExit(String mode) { + try { + return Mode.valueOf(mode); + } catch (IllegalArgumentException e) { + System.err.println( + "\"" + mode + "\": Invalid mode. Legal values are \"success\" and \"error\"."); + System.exit(1); + } + return null; // this won't happen + } +} diff --git a/integration-tests/it-exporter/it-exporter-test/pom.xml b/integration-tests/it-exporter/it-exporter-test/pom.xml index 5adba2699..4e72d1db9 100644 --- a/integration-tests/it-exporter/it-exporter-test/pom.xml +++ b/integration-tests/it-exporter/it-exporter-test/pom.xml @@ -17,17 +17,6 @@ - - io.prometheus - prometheus-metrics-exposition-formats - ${project.version} - - - commons-io - commons-io - 2.18.0 - test - io.prometheus it-common diff --git a/integration-tests/it-exporter/it-exporter-test/src/test/java/io/prometheus/metrics/it/exporter/test/ExporterIT.java b/integration-tests/it-exporter/it-exporter-test/src/test/java/io/prometheus/metrics/it/exporter/test/ExporterIT.java index da653d1b0..4819ed924 100644 --- a/integration-tests/it-exporter/it-exporter-test/src/test/java/io/prometheus/metrics/it/exporter/test/ExporterIT.java +++ b/integration-tests/it-exporter/it-exporter-test/src/test/java/io/prometheus/metrics/it/exporter/test/ExporterIT.java @@ -2,62 +2,27 @@ import static java.nio.charset.StandardCharsets.UTF_8; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.fail; -import io.prometheus.client.it.common.LogConsumer; -import io.prometheus.client.it.common.Volume; +import com.google.common.io.Resources; +import io.prometheus.client.it.common.ExporterTest; import io.prometheus.metrics.expositionformats.generated.com_google_protobuf_4_28_3.Metrics; -import java.io.ByteArrayInputStream; import java.io.IOException; -import java.io.InputStream; -import java.io.UnsupportedEncodingException; -import java.net.HttpURLConnection; import java.net.URISyntaxException; -import java.net.URL; import java.net.URLEncoder; -import java.util.ArrayList; -import java.util.HashMap; import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.concurrent.TimeUnit; -import java.util.zip.GZIPInputStream; -import org.apache.commons.io.IOUtils; -import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; -import org.testcontainers.containers.BindMode; -import org.testcontainers.containers.GenericContainer; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; -abstract class ExporterIT { - - private final GenericContainer sampleAppContainer; - private final Volume sampleAppVolume; - private final String sampleApp; +abstract class ExporterIT extends ExporterTest { public ExporterIT(String sampleApp) throws IOException, URISyntaxException { - this.sampleApp = sampleApp; - this.sampleAppVolume = - Volume.create("it-exporter") - .copy("../../it-" + sampleApp + "/target/" + sampleApp + ".jar"); - this.sampleAppContainer = - new GenericContainer<>("openjdk:17") - .withFileSystemBind(sampleAppVolume.getHostPath(), "/app", BindMode.READ_ONLY) - .withWorkingDirectory("/app") - .withLogConsumer(LogConsumer.withPrefix(sampleApp)) - .withExposedPorts(9400); - } - - @AfterEach - public void tearDown() throws IOException { - sampleAppContainer.stop(); - sampleAppVolume.remove(); + super(sampleApp); } @Test public void testOpenMetricsTextFormat() throws IOException { - sampleAppContainer - .withCommand("java", "-jar", "/app/" + sampleApp + ".jar", "9400", "success") - .start(); + start(); Response response = scrape("GET", "", "Accept", "application/openmetrics-text; version=1.0.0; charset=utf-8"); assertThat(response.status).isEqualTo(200); @@ -68,8 +33,7 @@ public void testOpenMetricsTextFormat() throws IOException { assertThat(response.getHeader("Transfer-Encoding")).isNull(); assertThat(response.getHeader("Content-Length")) .isEqualTo(Integer.toString(response.body.length)); - String bodyString = new String(response.body, UTF_8); - assertThat(bodyString) + assertThat(response.stringBody()) .contains("integration_test_info{test_name=\"" + sampleApp + "\"} 1") .contains("temperature_celsius{location=\"inside\"} 23.0") .contains("temperature_celsius{location=\"outside\"} 27.0") @@ -80,9 +44,7 @@ public void testOpenMetricsTextFormat() throws IOException { @Test public void testPrometheusTextFormat() throws IOException { - sampleAppContainer - .withCommand("java", "-jar", "/app/" + sampleApp + ".jar", "9400", "success") - .start(); + start(); Response response = scrape("GET", ""); assertThat(response.status).isEqualTo(200); assertContentType( @@ -91,8 +53,7 @@ public void testPrometheusTextFormat() throws IOException { assertThat(response.getHeader("Transfer-Encoding")).isNull(); assertThat(response.getHeader("Content-Length")) .isEqualTo(Integer.toString(response.body.length)); - String bodyString = new String(response.body, UTF_8); - assertThat(bodyString) + assertThat(response.stringBody()) .contains("integration_test_info{test_name=\"" + sampleApp + "\"} 1") .contains("temperature_celsius{location=\"inside\"} 23.0") .contains("temperature_celsius{location=\"outside\"} 27.0") @@ -103,9 +64,7 @@ public void testPrometheusTextFormat() throws IOException { @Test public void testPrometheusProtobufFormat() throws IOException { - sampleAppContainer - .withCommand("java", "-jar", "/app/" + sampleApp + ".jar", "9400", "success") - .start(); + start(); Response response = scrape( "GET", @@ -120,23 +79,36 @@ public void testPrometheusProtobufFormat() throws IOException { assertThat(response.getHeader("Transfer-Encoding")).isNull(); assertThat(response.getHeader("Content-Length")) .isEqualTo(Integer.toString(response.body.length)); - List metrics = new ArrayList<>(); - InputStream in = new ByteArrayInputStream(response.body); - while (in.available() > 0) { - metrics.add(Metrics.MetricFamily.parseDelimitedFrom(in)); - } - assertThat(metrics.size()).isEqualTo(3); + List metrics = response.protoBody(); + assertThat(metrics).hasSize(3); // metrics are sorted by name assertThat(metrics.get(0).getName()).isEqualTo("integration_test_info"); assertThat(metrics.get(1).getName()).isEqualTo("temperature_celsius"); assertThat(metrics.get(2).getName()).isEqualTo("uptime_seconds_total"); } + @ParameterizedTest + @CsvSource({ + "openmetrics, debug-openmetrics.txt", + "text, debug-text.txt", + "prometheus-protobuf, debug-protobuf.txt", + }) + public void testPrometheusProtobufDebugFormat(String format, String expected) throws IOException { + start(); + Response response = scrape("GET", "debug=" + format); + assertThat(response.status).isEqualTo(200); + assertContentType( + "text/plain;charset=utf-8", response.getHeader("Content-Type").replace(" ", "")); + assertThat(response.stringBody().trim()) + .isEqualTo( + Resources.toString(Resources.getResource(expected), UTF_8) + .trim() + .replace("", sampleApp)); + } + @Test public void testCompression() throws IOException { - sampleAppContainer - .withCommand("java", "-jar", "/app/" + sampleApp + ".jar", "9400", "success") - .start(); + start(); Response response = scrape( "GET", @@ -159,21 +131,15 @@ public void testCompression() throws IOException { assertContentType( "application/openmetrics-text; version=1.0.0; charset=utf-8", response.getHeader("Content-Type")); - String body = - new String( - IOUtils.toByteArray(new GZIPInputStream(new ByteArrayInputStream(response.body))), - UTF_8); - assertThat(body).contains("uptime_seconds_total 17.0"); + assertThat(response.gzipBody()).contains("uptime_seconds_total 17.0"); } @Test public void testErrorHandling() throws IOException { - sampleAppContainer - .withCommand("java", "-jar", "/app/" + sampleApp + ".jar", "9400", "error") - .start(); + start("error"); Response response = scrape("GET", ""); assertThat(response.status).isEqualTo(500); - assertThat(new String(response.body, UTF_8)).contains("Simulating an error."); + assertThat(response.stringBody()).contains("Simulating an error."); } protected boolean headReturnsContentLength() { @@ -182,12 +148,10 @@ protected boolean headReturnsContentLength() { @Test public void testHeadRequest() throws IOException { - sampleAppContainer - .withCommand("java", "-jar", "/app/" + sampleApp + ".jar", "9400", "success") - .start(); + start(); Response fullResponse = scrape("GET", ""); int size = fullResponse.body.length; - assertThat(size > 0).isTrue(); + assertThat(size).isGreaterThan(0); Response headResponse = scrape("HEAD", ""); assertThat(headResponse.status).isEqualTo(200); if (headReturnsContentLength()) { @@ -195,28 +159,23 @@ public void testHeadRequest() throws IOException { } else { assertThat(headResponse.getHeader("Content-Length")).isNull(); } - assertThat(headResponse.body.length).isZero(); + assertThat(headResponse.body).isEmpty(); } @Test public void testDebug() throws IOException { - sampleAppContainer - .withCommand("java", "-jar", "/app/" + sampleApp + ".jar", "9400", "success") - .start(); + start(); Response response = scrape("GET", "debug=openmetrics"); assertThat(response.status).isEqualTo(200); assertContentType("text/plain; charset=utf-8", response.getHeader("Content-Type")); - String bodyString = new String(response.body, UTF_8); - assertThat(bodyString) + assertThat(response.stringBody()) .contains("uptime_seconds_total 17.0") .contains("# UNIT uptime_seconds seconds"); } @Test public void testNameFilter() throws IOException { - sampleAppContainer - .withCommand("java", "-jar", "/app/" + sampleApp + ".jar", "9400", "success") - .start(); + start(); Response response = scrape( "GET", @@ -227,8 +186,7 @@ public void testNameFilter() throws IOException { assertContentType( "application/openmetrics-text; version=1.0.0; charset=utf-8", response.getHeader("Content-Type")); - String bodyString = new String(response.body, UTF_8); - assertThat(bodyString) + assertThat(response.stringBody()) .contains("integration_test_info{test_name=\"" + sampleApp + "\"} 1") .contains("uptime_seconds_total 17.0") .doesNotContain("temperature_celsius"); @@ -236,9 +194,7 @@ public void testNameFilter() throws IOException { @Test public void testEmptyResponseOpenMetrics() throws IOException { - sampleAppContainer - .withCommand("java", "-jar", "/app/" + sampleApp + ".jar", "9400", "success") - .start(); + start(); Response response = scrape( "GET", @@ -251,14 +207,12 @@ public void testEmptyResponseOpenMetrics() throws IOException { response.getHeader("Content-Type")); assertThat(response.getHeader("Content-Length")) .isEqualTo(Integer.toString(response.body.length)); - assertThat(new String(response.body, UTF_8)).isEqualTo("# EOF\n"); + assertThat(response.stringBody()).isEqualTo("# EOF\n"); } @Test public void testEmptyResponseText() throws IOException { - sampleAppContainer - .withCommand("java", "-jar", "/app/" + sampleApp + ".jar", "9400", "success") - .start(); + start(); Response response = scrape("GET", nameParam("none_existing")); assertThat(response.status).isEqualTo(200); assertContentType( @@ -267,14 +221,12 @@ public void testEmptyResponseText() throws IOException { != null) { // HTTPServer does not send a zero content length, which is ok assertThat(response.getHeader("Content-Length")).isEqualTo("0"); } - assertThat(response.body.length).isZero(); + assertThat(response.body).isEmpty(); } @Test public void testEmptyResponseProtobuf() throws IOException { - sampleAppContainer - .withCommand("java", "-jar", "/app/" + sampleApp + ".jar", "9400", "success") - .start(); + start(); Response response = scrape( "GET", @@ -285,14 +237,12 @@ public void testEmptyResponseProtobuf() throws IOException { assertContentType( "application/vnd.google.protobuf; proto=io.prometheus.client.MetricFamily; encoding=delimited", response.getHeader("Content-Type")); - assertThat(response.body.length).isZero(); + assertThat(response.body).isEmpty(); } @Test public void testEmptyResponseGzipOpenMetrics() throws IOException { - sampleAppContainer - .withCommand("java", "-jar", "/app/" + sampleApp + ".jar", "9400", "success") - .start(); + start(); Response response = scrape( "GET", @@ -303,113 +253,27 @@ public void testEmptyResponseGzipOpenMetrics() throws IOException { "gzip"); assertThat(response.status).isEqualTo(200); assertThat(response.getHeader("Content-Encoding")).isEqualTo("gzip"); - String body = - new String( - IOUtils.toByteArray(new GZIPInputStream(new ByteArrayInputStream(response.body))), - UTF_8); - assertThat(body).isEqualTo("# EOF\n"); + assertThat(response.gzipBody()).isEqualTo("# EOF\n"); } @Test public void testEmptyResponseGzipText() throws IOException { - sampleAppContainer - .withCommand("java", "-jar", "/app/" + sampleApp + ".jar", "9400", "success") - .start(); + start(); Response response = scrape("GET", nameParam("none_existing"), "Accept-Encoding", "gzip"); assertThat(response.status).isEqualTo(200); assertThat(response.getHeader("Content-Encoding")).isEqualTo("gzip"); - String body = - new String( - IOUtils.toByteArray(new GZIPInputStream(new ByteArrayInputStream(response.body))), - UTF_8); - assertThat(body.length()).isZero(); + assertThat(response.gzipBody()).isEmpty(); } - private String nameParam(String name) throws UnsupportedEncodingException { - return URLEncoder.encode("name[]", UTF_8.name()) + "=" + URLEncoder.encode(name, UTF_8.name()); + private String nameParam(String name) { + return URLEncoder.encode("name[]", UTF_8) + "=" + URLEncoder.encode(name, UTF_8); } @Test public void testDebugUnknown() throws IOException { - sampleAppContainer - .withCommand("java", "-jar", "/app/" + sampleApp + ".jar", "9400", "success") - .start(); + start(); Response response = scrape("GET", "debug=unknown"); assertThat(response.status).isEqualTo(500); assertContentType("text/plain; charset=utf-8", response.getHeader("Content-Type")); } - - private void assertContentType(String expected, String actual) { - if (!expected.replace(" ", "").equals(actual)) { - assertThat(actual).isEqualTo(expected); - } - } - - private Response scrape(String method, String queryString, String... requestHeaders) - throws IOException { - long timeoutMillis = TimeUnit.SECONDS.toMillis(5); - URL url = - new URL( - "http://localhost:" - + sampleAppContainer.getMappedPort(9400) - + "/metrics?" - + queryString); - HttpURLConnection con = (HttpURLConnection) url.openConnection(); - con.setRequestMethod(method); - for (int i = 0; i < requestHeaders.length; i += 2) { - con.setRequestProperty(requestHeaders[i], requestHeaders[i + 1]); - } - long start = System.currentTimeMillis(); - Exception exception = null; - while (System.currentTimeMillis() - start < timeoutMillis) { - try { - if (con.getResponseCode() == 200) { - return new Response( - con.getResponseCode(), - con.getHeaderFields(), - IOUtils.toByteArray(con.getInputStream())); - } else { - return new Response( - con.getResponseCode(), - con.getHeaderFields(), - IOUtils.toByteArray(con.getErrorStream())); - } - } catch (Exception e) { - exception = e; - try { - Thread.sleep(100); - } catch (InterruptedException ignored) { - // ignore - } - } - } - if (exception != null) { - exception.printStackTrace(); - } - fail("timeout while getting metrics from " + url); - return null; // will not happen - } - - private static class Response { - private final int status; - private final Map headers; - private final byte[] body; - - private Response(int status, Map> headers, byte[] body) { - this.status = status; - this.headers = new HashMap<>(headers.size()); - this.body = body; - for (Map.Entry> entry : headers.entrySet()) { - if (entry.getKey() - != null) { // HttpUrlConnection uses pseudo key "null" for the status line - this.headers.put(entry.getKey().toLowerCase(Locale.ROOT), entry.getValue().get(0)); - } - } - } - - private String getHeader(String name) { - // HTTP headers are case-insensitive - return headers.get(name.toLowerCase(Locale.ROOT)); - } - } } diff --git a/integration-tests/it-exporter/it-exporter-test/src/test/resources/debug-openmetrics.txt b/integration-tests/it-exporter/it-exporter-test/src/test/resources/debug-openmetrics.txt new file mode 100644 index 000000000..667cfc87c --- /dev/null +++ b/integration-tests/it-exporter/it-exporter-test/src/test/resources/debug-openmetrics.txt @@ -0,0 +1,13 @@ +# TYPE integration_test info +# HELP integration_test Info metric on this integration test +integration_test_info{test_name=""} 1 +# TYPE temperature_celsius gauge +# UNIT temperature_celsius celsius +# HELP temperature_celsius Temperature in Celsius +temperature_celsius{location="inside"} 23.0 +temperature_celsius{location="outside"} 27.0 +# TYPE uptime_seconds counter +# UNIT uptime_seconds seconds +# HELP uptime_seconds total number of seconds since this application was started +uptime_seconds_total 17.0 +# EOF diff --git a/integration-tests/it-exporter/it-exporter-test/src/test/resources/debug-protobuf.txt b/integration-tests/it-exporter/it-exporter-test/src/test/resources/debug-protobuf.txt new file mode 100644 index 000000000..1d7603c1b --- /dev/null +++ b/integration-tests/it-exporter/it-exporter-test/src/test/resources/debug-protobuf.txt @@ -0,0 +1,42 @@ +name: "integration_test_info" +help: "Info metric on this integration test" +type: GAUGE +metric { + label { + name: "test_name" + value: "" + } + gauge { + value: 1.0 + } +} +name: "temperature_celsius" +help: "Temperature in Celsius" +type: GAUGE +metric { + label { + name: "location" + value: "inside" + } + gauge { + value: 23.0 + } +} +metric { + label { + name: "location" + value: "outside" + } + gauge { + value: 27.0 + } +} +name: "uptime_seconds_total" +help: "total number of seconds since this application was started" +type: COUNTER +metric { + counter { + value: 17.0 + } +} + diff --git a/integration-tests/it-exporter/it-exporter-test/src/test/resources/debug-text.txt b/integration-tests/it-exporter/it-exporter-test/src/test/resources/debug-text.txt new file mode 100644 index 000000000..b57df80d9 --- /dev/null +++ b/integration-tests/it-exporter/it-exporter-test/src/test/resources/debug-text.txt @@ -0,0 +1,10 @@ +# HELP integration_test_info Info metric on this integration test +# TYPE integration_test_info gauge +integration_test_info{test_name=""} 1 +# HELP temperature_celsius Temperature in Celsius +# TYPE temperature_celsius gauge +temperature_celsius{location="inside"} 23.0 +temperature_celsius{location="outside"} 27.0 +# HELP uptime_seconds_total total number of seconds since this application was started +# TYPE uptime_seconds_total counter +uptime_seconds_total 17.0 diff --git a/integration-tests/it-exporter/it-no-protobuf-test/pom.xml b/integration-tests/it-exporter/it-no-protobuf-test/pom.xml new file mode 100644 index 000000000..f10cd3e16 --- /dev/null +++ b/integration-tests/it-exporter/it-no-protobuf-test/pom.xml @@ -0,0 +1,27 @@ + + + 4.0.0 + + + io.prometheus + it-exporter + 1.3.3 + + + it-no-protobuf-test + + Integration Test - No Protobuf + + Integration tests without protobuf + + + + + io.prometheus + it-common + test-jar + ${project.version} + test + + + diff --git a/integration-tests/it-exporter/it-no-protobuf-test/src/test/java/io/prometheus/metrics/it/noprotobuf/NoProtobufIT.java b/integration-tests/it-exporter/it-no-protobuf-test/src/test/java/io/prometheus/metrics/it/noprotobuf/NoProtobufIT.java new file mode 100644 index 000000000..cd534dcb9 --- /dev/null +++ b/integration-tests/it-exporter/it-no-protobuf-test/src/test/java/io/prometheus/metrics/it/noprotobuf/NoProtobufIT.java @@ -0,0 +1,23 @@ +package io.prometheus.metrics.it.noprotobuf; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.prometheus.client.it.common.ExporterTest; +import java.io.IOException; +import java.net.URISyntaxException; +import org.junit.jupiter.api.Test; + +class NoProtobufIT extends ExporterTest { + + public NoProtobufIT() throws IOException, URISyntaxException { + super("exporter-no-protobuf"); + } + + @Test + public void testPrometheusProtobufDebugFormat() throws IOException { + start(); + assertThat(scrape("GET", "debug=text").status).isEqualTo(200); + // protobuf is not supported + assertThat(scrape("GET", "debug=prometheus-protobuf").status).isEqualTo(500); + } +} diff --git a/integration-tests/it-exporter/pom.xml b/integration-tests/it-exporter/pom.xml index df4d0dfd2..4badf828d 100644 --- a/integration-tests/it-exporter/pom.xml +++ b/integration-tests/it-exporter/pom.xml @@ -12,23 +12,16 @@ pom Integration Tests - Exporter - http://github.com/prometheus/client_java Integration tests for the Exporter modules - - - fstab - Fabian Stäber - fabian@fstab.de - - - it-exporter-servlet-tomcat-sample it-exporter-servlet-jetty-sample it-exporter-httpserver-sample + it-exporter-no-protobuf it-exporter-test + it-no-protobuf-test diff --git a/integration-tests/it-pushgateway/pom.xml b/integration-tests/it-pushgateway/pom.xml index 6ea893eba..050d6c37a 100644 --- a/integration-tests/it-pushgateway/pom.xml +++ b/integration-tests/it-pushgateway/pom.xml @@ -11,7 +11,6 @@ it-pushgateway Integration Test - Pushgateway - http://github.com/prometheus/client_java Integration tests for the Pushgateway Exporter diff --git a/integration-tests/it-spring-boot-smoke-test/pom.xml b/integration-tests/it-spring-boot-smoke-test/pom.xml new file mode 100644 index 000000000..50e9b7669 --- /dev/null +++ b/integration-tests/it-spring-boot-smoke-test/pom.xml @@ -0,0 +1,98 @@ + + + + 4.0.0 + + + org.springframework.boot + spring-boot-starter-parent + 3.3.5 + + + + io.prometheus + it-spring-boot-smoke-test + 1.3.3 + + Integration Test - Spring Smoke Tests + + Spring Smoke Tests + + + 17 + + + + + + io.prometheus + prometheus-metrics-bom + ${project.version} + pom + import + + + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-actuator + + + io.micrometer + micrometer-registry-prometheus + runtime + + + + org.springframework.boot + spring-boot-starter-test + test + + + io.prometheus + it-common + test-jar + ${project.version} + test + + + + + + + org.graalvm.buildtools + native-maven-plugin + + + org.springframework.boot + spring-boot-maven-plugin + + + com.diffplug.spotless + spotless-maven-plugin + 2.43.0 + + + + + + + + verify + + check + + + + + + + + diff --git a/integration-tests/it-spring-boot-smoke-test/src/main/java/io/prometheus/metrics/it/springboot/Application.java b/integration-tests/it-spring-boot-smoke-test/src/main/java/io/prometheus/metrics/it/springboot/Application.java new file mode 100644 index 000000000..955ea4033 --- /dev/null +++ b/integration-tests/it-spring-boot-smoke-test/src/main/java/io/prometheus/metrics/it/springboot/Application.java @@ -0,0 +1,12 @@ +package io.prometheus.metrics.it.springboot; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class Application { + + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } +} diff --git a/integration-tests/it-spring-boot-smoke-test/src/main/resources/application.yaml b/integration-tests/it-spring-boot-smoke-test/src/main/resources/application.yaml new file mode 100644 index 000000000..fdcf1351d --- /dev/null +++ b/integration-tests/it-spring-boot-smoke-test/src/main/resources/application.yaml @@ -0,0 +1,5 @@ +management: + endpoints: + web: + exposure: + include: '*' diff --git a/integration-tests/it-spring-boot-smoke-test/src/test/java/io/prometheus/metrics/it/springboot/ApplicationTest.java b/integration-tests/it-spring-boot-smoke-test/src/test/java/io/prometheus/metrics/it/springboot/ApplicationTest.java new file mode 100644 index 000000000..3d893d894 --- /dev/null +++ b/integration-tests/it-spring-boot-smoke-test/src/test/java/io/prometheus/metrics/it/springboot/ApplicationTest.java @@ -0,0 +1,35 @@ +package io.prometheus.metrics.it.springboot; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.prometheus.client.it.common.ExporterTest; +import io.prometheus.metrics.expositionformats.generated.com_google_protobuf_4_28_3.Metrics; +import java.io.IOException; +import java.net.URL; +import java.util.List; +import java.util.Optional; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.autoconfigure.actuate.observability.AutoConfigureObservability; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT) +@AutoConfigureObservability +class ApplicationTest { + @Test + public void testPrometheusProtobufFormat() throws IOException { + ExporterTest.Response response = + ExporterTest.scrape( + "GET", + new URL("http://localhost:8080/actuator/prometheus"), + "Accept", + "application/vnd.google.protobuf; proto=io.prometheus.client.MetricFamily; encoding=delimited"); + assertThat(response.status).isEqualTo(200); + + List metrics = response.protoBody(); + Optional metric = + metrics.stream() + .filter(m -> m.getName().equals("application_started_time_seconds")) + .findFirst(); + assertThat(metric).isPresent(); + } +} diff --git a/integration-tests/pom.xml b/integration-tests/pom.xml index 2a75a4942..ece364ae9 100644 --- a/integration-tests/pom.xml +++ b/integration-tests/pom.xml @@ -13,7 +13,6 @@ pom Integration Tests - http://github.com/prometheus/client_java Integration tests for the Exporter modules @@ -26,6 +25,7 @@ it-common it-exporter it-pushgateway + it-spring-boot-smoke-test @@ -46,7 +46,13 @@ + + + commons-io + commons-io + 2.18.0 + org.testcontainers junit-jupiter diff --git a/pom.xml b/pom.xml index 4dc20a62d..ac7d26403 100644 --- a/pom.xml +++ b/pom.xml @@ -66,18 +66,19 @@ prometheus-metrics-model prometheus-metrics-tracer prometheus-metrics-exposition-formats + prometheus-metrics-exposition-textformats prometheus-metrics-exporter-common prometheus-metrics-exporter-servlet-jakarta prometheus-metrics-exporter-servlet-javax prometheus-metrics-exporter-httpserver prometheus-metrics-exporter-opentelemetry + prometheus-metrics-exporter-opentelemetry-otel-agent-resources prometheus-metrics-exporter-pushgateway prometheus-metrics-instrumentation-caffeine prometheus-metrics-instrumentation-jvm prometheus-metrics-instrumentation-dropwizard5 prometheus-metrics-instrumentation-guava prometheus-metrics-simpleclient-bridge - otel-agent-resources @@ -91,6 +92,14 @@ + + com.google.code.findbugs + jsr305 + 3.0.2 + provided + + + org.junit.jupiter junit-jupiter-engine @@ -200,6 +209,16 @@ maven-enforcer-plugin 3.5.0 + + org.codehaus.mojo + build-helper-maven-plugin + 3.6.0 + + + org.codehaus.mojo + exec-maven-plugin + 3.5.0 + com.diffplug.spotless spotless-maven-plugin diff --git a/prometheus-metrics-core/src/test/java/io/prometheus/metrics/core/metrics/CounterTest.java b/prometheus-metrics-core/src/test/java/io/prometheus/metrics/core/metrics/CounterTest.java index 801876b74..d2bf832d5 100644 --- a/prometheus-metrics-core/src/test/java/io/prometheus/metrics/core/metrics/CounterTest.java +++ b/prometheus-metrics-core/src/test/java/io/prometheus/metrics/core/metrics/CounterTest.java @@ -6,9 +6,9 @@ import static org.assertj.core.data.Offset.offset; import io.prometheus.metrics.core.exemplars.ExemplarSamplerConfigTestUtil; -import io.prometheus.metrics.expositionformats.PrometheusProtobufWriter; -import io.prometheus.metrics.expositionformats.TextFormatUtil; import io.prometheus.metrics.expositionformats.generated.com_google_protobuf_4_28_3.Metrics; +import io.prometheus.metrics.expositionformats.internal.PrometheusProtobufWriterImpl; +import io.prometheus.metrics.expositionformats.internal.ProtobufUtil; import io.prometheus.metrics.model.snapshots.CounterSnapshot; import io.prometheus.metrics.model.snapshots.Exemplar; import io.prometheus.metrics.model.snapshots.Label; @@ -112,8 +112,9 @@ public void testTotalStrippedFromName() { "my_counter_seconds", "my.counter.seconds" }) { Counter counter = Counter.builder().name(name).unit(Unit.SECONDS).build(); - Metrics.MetricFamily protobufData = new PrometheusProtobufWriter().convert(counter.collect()); - assertThat(TextFormatUtil.shortDebugString(protobufData)) + Metrics.MetricFamily protobufData = + new PrometheusProtobufWriterImpl().convert(counter.collect()); + assertThat(ProtobufUtil.shortDebugString(protobufData)) .isEqualTo( "name: \"my_counter_seconds_total\" type: COUNTER metric { counter { value: 0.0 } }"); } diff --git a/prometheus-metrics-core/src/test/java/io/prometheus/metrics/core/metrics/HistogramTest.java b/prometheus-metrics-core/src/test/java/io/prometheus/metrics/core/metrics/HistogramTest.java index 89a4e853c..5a8d7cda8 100644 --- a/prometheus-metrics-core/src/test/java/io/prometheus/metrics/core/metrics/HistogramTest.java +++ b/prometheus-metrics-core/src/test/java/io/prometheus/metrics/core/metrics/HistogramTest.java @@ -8,9 +8,9 @@ import io.prometheus.metrics.core.datapoints.DistributionDataPoint; import io.prometheus.metrics.core.exemplars.ExemplarSamplerConfigTestUtil; import io.prometheus.metrics.expositionformats.OpenMetricsTextFormatWriter; -import io.prometheus.metrics.expositionformats.PrometheusProtobufWriter; -import io.prometheus.metrics.expositionformats.TextFormatUtil; import io.prometheus.metrics.expositionformats.generated.com_google_protobuf_4_28_3.Metrics; +import io.prometheus.metrics.expositionformats.internal.PrometheusProtobufWriterImpl; +import io.prometheus.metrics.expositionformats.internal.ProtobufUtil; import io.prometheus.metrics.model.snapshots.ClassicHistogramBucket; import io.prometheus.metrics.model.snapshots.Exemplar; import io.prometheus.metrics.model.snapshots.Exemplars; @@ -89,10 +89,10 @@ private void run() throws NoSuchFieldException, IllegalAccessException { } } Metrics.MetricFamily protobufData = - new PrometheusProtobufWriter().convert(histogram.collect()); + new PrometheusProtobufWriterImpl().convert(histogram.collect()); String expectedWithMetadata = "name: \"test\" type: HISTOGRAM metric { histogram { " + expected + " } }"; - assertThat(TextFormatUtil.shortDebugString(protobufData)) + assertThat(ProtobufUtil.shortDebugString(protobufData)) .as("test \"" + name + "\" failed") .isEqualTo(expectedWithMetadata); } @@ -940,8 +940,8 @@ public void testDefaults() throws IOException { + "# EOF\n"; // protobuf - Metrics.MetricFamily protobufData = new PrometheusProtobufWriter().convert(snapshot); - assertThat(TextFormatUtil.shortDebugString(protobufData)).isEqualTo(expectedProtobuf); + Metrics.MetricFamily protobufData = new PrometheusProtobufWriterImpl().convert(snapshot); + assertThat(ProtobufUtil.shortDebugString(protobufData)).isEqualTo(expectedProtobuf); // text ByteArrayOutputStream out = new ByteArrayOutputStream(); diff --git a/prometheus-metrics-core/src/test/java/io/prometheus/metrics/core/metrics/InfoTest.java b/prometheus-metrics-core/src/test/java/io/prometheus/metrics/core/metrics/InfoTest.java index 3084cdb10..427b6a55f 100644 --- a/prometheus-metrics-core/src/test/java/io/prometheus/metrics/core/metrics/InfoTest.java +++ b/prometheus-metrics-core/src/test/java/io/prometheus/metrics/core/metrics/InfoTest.java @@ -4,9 +4,9 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import io.prometheus.metrics.expositionformats.OpenMetricsTextFormatWriter; -import io.prometheus.metrics.expositionformats.PrometheusProtobufWriter; -import io.prometheus.metrics.expositionformats.TextFormatUtil; import io.prometheus.metrics.expositionformats.generated.com_google_protobuf_4_28_3.Metrics; +import io.prometheus.metrics.expositionformats.internal.PrometheusProtobufWriterImpl; +import io.prometheus.metrics.expositionformats.internal.ProtobufUtil; import io.prometheus.metrics.model.snapshots.Labels; import io.prometheus.metrics.model.snapshots.MetricSnapshots; import io.prometheus.metrics.model.snapshots.Unit; @@ -27,8 +27,9 @@ public void testInfoStrippedFromName() { for (String labelName : new String[] {"my.key", "my_key"}) { Info info = Info.builder().name(name).labelNames(labelName).build(); info.addLabelValues("value"); - Metrics.MetricFamily protobufData = new PrometheusProtobufWriter().convert(info.collect()); - assertThat(TextFormatUtil.shortDebugString(protobufData)) + Metrics.MetricFamily protobufData = + new PrometheusProtobufWriterImpl().convert(info.collect()); + assertThat(ProtobufUtil.shortDebugString(protobufData)) .isEqualTo( "name: \"jvm_runtime_info\" type: GAUGE metric { label { name: \"my_key\" value: \"value\" } gauge { value: 1.0 } }"); } diff --git a/prometheus-metrics-exporter-common/pom.xml b/prometheus-metrics-exporter-common/pom.xml index 30b2cd9ae..cea87262b 100644 --- a/prometheus-metrics-exporter-common/pom.xml +++ b/prometheus-metrics-exporter-common/pom.xml @@ -26,10 +26,16 @@ prometheus-metrics-model ${project.version} + + io.prometheus + prometheus-metrics-exposition-textformats + ${project.version} + io.prometheus prometheus-metrics-exposition-formats ${project.version} + runtime diff --git a/prometheus-metrics-exporter-common/src/main/java/io/prometheus/metrics/exporter/common/PrometheusScrapeHandler.java b/prometheus-metrics-exporter-common/src/main/java/io/prometheus/metrics/exporter/common/PrometheusScrapeHandler.java index f871addfd..f978a9b36 100644 --- a/prometheus-metrics-exporter-common/src/main/java/io/prometheus/metrics/exporter/common/PrometheusScrapeHandler.java +++ b/prometheus-metrics-exporter-common/src/main/java/io/prometheus/metrics/exporter/common/PrometheusScrapeHandler.java @@ -11,8 +11,10 @@ import java.io.IOException; import java.io.OutputStream; import java.nio.charset.StandardCharsets; +import java.util.ArrayList; import java.util.Arrays; import java.util.Enumeration; +import java.util.List; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Predicate; import java.util.zip.GZIPOutputStream; @@ -23,7 +25,8 @@ public class PrometheusScrapeHandler { private final PrometheusRegistry registry; private final ExpositionFormats expositionFormats; private final Predicate nameFilter; - private AtomicInteger lastResponseSize = new AtomicInteger(2 << 9); // 0.5 MB + private final AtomicInteger lastResponseSize = new AtomicInteger(2 << 9); // 0.5 MB + private final List supportedFormats; public PrometheusScrapeHandler() { this(PrometheusProperties.get(), PrometheusRegistry.defaultRegistry); @@ -41,6 +44,10 @@ public PrometheusScrapeHandler(PrometheusProperties config, PrometheusRegistry r this.expositionFormats = ExpositionFormats.init(config.getExporterProperties()); this.registry = registry; this.nameFilter = makeNameFilter(config.getExporterFilterProperties()); + supportedFormats = new ArrayList<>(Arrays.asList("openmetrics", "text")); + if (expositionFormats.getPrometheusProtobufWriter().isAvailable()) { + supportedFormats.add("prometheus-protobuf"); + } } public void handleRequest(PrometheusHttpExchange exchange) throws IOException { @@ -137,9 +144,7 @@ private boolean writeDebugResponse(MetricSnapshots snapshots, PrometheusHttpExch return false; } else { response.setHeader("Content-Type", "text/plain; charset=utf-8"); - boolean supportedFormat = - Arrays.asList("openmetrics", "text", "prometheus-protobuf").contains(debugParam); - int responseStatus = supportedFormat ? 200 : 500; + int responseStatus = supportedFormats.contains(debugParam) ? 200 : 500; OutputStream body = response.sendHeadersAndGetBody(responseStatus, 0); switch (debugParam) { case "openmetrics": diff --git a/otel-agent-resources/pom.xml b/prometheus-metrics-exporter-opentelemetry-otel-agent-resources/pom.xml similarity index 96% rename from otel-agent-resources/pom.xml rename to prometheus-metrics-exporter-opentelemetry-otel-agent-resources/pom.xml index c4e9a4741..0d6f63623 100644 --- a/otel-agent-resources/pom.xml +++ b/prometheus-metrics-exporter-opentelemetry-otel-agent-resources/pom.xml @@ -9,7 +9,7 @@ 1.3.3 - otel-agent-resources + prometheus-metrics-exporter-opentelemetry-otel-agent-resources bundle OpenTelemetry Agent Resource Extractor diff --git a/otel-agent-resources/src/main/java/io/prometheus/otelagent/ResourceAttributesFromOtelAgent.java b/prometheus-metrics-exporter-opentelemetry-otel-agent-resources/src/main/java/io/prometheus/otelagent/ResourceAttributesFromOtelAgent.java similarity index 100% rename from otel-agent-resources/src/main/java/io/prometheus/otelagent/ResourceAttributesFromOtelAgent.java rename to prometheus-metrics-exporter-opentelemetry-otel-agent-resources/src/main/java/io/prometheus/otelagent/ResourceAttributesFromOtelAgent.java diff --git a/otel-agent-resources/src/main/resources/lib/.gitignore b/prometheus-metrics-exporter-opentelemetry-otel-agent-resources/src/main/resources/lib/.gitignore similarity index 100% rename from otel-agent-resources/src/main/resources/lib/.gitignore rename to prometheus-metrics-exporter-opentelemetry-otel-agent-resources/src/main/resources/lib/.gitignore diff --git a/prometheus-metrics-exporter-opentelemetry/pom.xml b/prometheus-metrics-exporter-opentelemetry/pom.xml index e8051258e..462ff19d1 100644 --- a/prometheus-metrics-exporter-opentelemetry/pom.xml +++ b/prometheus-metrics-exporter-opentelemetry/pom.xml @@ -41,7 +41,7 @@ io.prometheus - otel-agent-resources + prometheus-metrics-exporter-opentelemetry-otel-agent-resources ${project.version} @@ -112,7 +112,6 @@ org.codehaus.mojo build-helper-maven-plugin - 3.6.0 regex-property diff --git a/prometheus-metrics-exporter-pushgateway/src/main/java/io/prometheus/metrics/exporter/pushgateway/PushGateway.java b/prometheus-metrics-exporter-pushgateway/src/main/java/io/prometheus/metrics/exporter/pushgateway/PushGateway.java index 2bafc6aa2..6c89185f1 100644 --- a/prometheus-metrics-exporter-pushgateway/src/main/java/io/prometheus/metrics/exporter/pushgateway/PushGateway.java +++ b/prometheus-metrics-exporter-pushgateway/src/main/java/io/prometheus/metrics/exporter/pushgateway/PushGateway.java @@ -5,6 +5,7 @@ import io.prometheus.metrics.config.ExporterPushgatewayProperties; import io.prometheus.metrics.config.PrometheusProperties; import io.prometheus.metrics.config.PrometheusPropertiesException; +import io.prometheus.metrics.expositionformats.ExpositionFormatWriter; import io.prometheus.metrics.expositionformats.PrometheusProtobufWriter; import io.prometheus.metrics.expositionformats.PrometheusTextFormatWriter; import io.prometheus.metrics.model.registry.Collector; @@ -78,7 +79,7 @@ public class PushGateway { private static final int MILLISECONDS_PER_SECOND = 1000; private final URL url; - private final Format format; + private final ExpositionFormatWriter writer; private final Map requestHeaders; private final PrometheusRegistry registry; private final HttpConnectionFactory connectionFactory; @@ -90,10 +91,22 @@ private PushGateway( HttpConnectionFactory connectionFactory, Map requestHeaders) { this.registry = registry; - this.format = format; this.url = url; this.requestHeaders = Collections.unmodifiableMap(new HashMap<>(requestHeaders)); this.connectionFactory = connectionFactory; + writer = getWriter(format); + if (!writer.isAvailable()) { + throw new RuntimeException(writer.getClass() + " is not available"); + } + } + + private ExpositionFormatWriter getWriter(Format format) { + if (format == Format.PROMETHEUS_TEXT) { + return new PrometheusTextFormatWriter(false); + } else { + // use reflection to avoid a compile-time dependency on the expositionformats module + return new PrometheusProtobufWriter(); + } } /** @@ -174,11 +187,7 @@ private void doRequest(PrometheusRegistry registry, String method) throws IOExce try { HttpURLConnection connection = connectionFactory.create(url); requestHeaders.forEach(connection::setRequestProperty); - if (format == Format.PROMETHEUS_TEXT) { - connection.setRequestProperty("Content-Type", PrometheusTextFormatWriter.CONTENT_TYPE); - } else { - connection.setRequestProperty("Content-Type", PrometheusProtobufWriter.CONTENT_TYPE); - } + connection.setRequestProperty("Content-Type", writer.getContentType()); if (!method.equals("DELETE")) { connection.setDoOutput(true); } @@ -191,11 +200,7 @@ private void doRequest(PrometheusRegistry registry, String method) throws IOExce try { if (!method.equals("DELETE")) { OutputStream outputStream = connection.getOutputStream(); - if (format == Format.PROMETHEUS_TEXT) { - new PrometheusTextFormatWriter(false).write(outputStream, registry.scrape()); - } else { - new PrometheusProtobufWriter().write(outputStream, registry.scrape()); - } + writer.write(outputStream, registry.scrape()); outputStream.flush(); outputStream.close(); } diff --git a/prometheus-metrics-exposition-formats/generate-protobuf.sh b/prometheus-metrics-exposition-formats/generate-protobuf.sh index 866da038d..db27d9457 100755 --- a/prometheus-metrics-exposition-formats/generate-protobuf.sh +++ b/prometheus-metrics-exposition-formats/generate-protobuf.sh @@ -17,7 +17,7 @@ mkdir -p $TARGET_DIR rm -rf $PROTO_DIR || true mkdir -p $PROTO_DIR -OLD_PACKAGE=$(sed -nE 's/import (io.prometheus.metrics.expositionformats.generated.*).Metrics;/\1/p' src/main/java/io/prometheus/metrics/expositionformats/PrometheusProtobufWriter.java) +OLD_PACKAGE=$(sed -nE 's/import (io.prometheus.metrics.expositionformats.generated.*).Metrics;/\1/p' src/main/java/io/prometheus/metrics/expositionformats/internal/PrometheusProtobufWriterImpl.java) PACKAGE="io.prometheus.metrics.expositionformats.generated.com_google_protobuf_${PROTOBUF_VERSION_STRING}" if [[ $OLD_PACKAGE != "$PACKAGE" ]]; then diff --git a/prometheus-metrics-exposition-formats/pom.xml b/prometheus-metrics-exposition-formats/pom.xml index a6975525a..e27a73573 100644 --- a/prometheus-metrics-exposition-formats/pom.xml +++ b/prometheus-metrics-exposition-formats/pom.xml @@ -25,12 +25,7 @@ io.prometheus - prometheus-metrics-model - ${project.version} - - - io.prometheus - prometheus-metrics-config + prometheus-metrics-exposition-textformats ${project.version} @@ -45,7 +40,6 @@ org.codehaus.mojo build-helper-maven-plugin - 3.6.0 regex-property @@ -90,7 +84,6 @@ exec-maven-plugin org.codehaus.mojo - 3.5.0 Generate Protobuf diff --git a/prometheus-metrics-exposition-formats/src/main/java/io/prometheus/metrics/expositionformats/ExpositionFormatWriter.java b/prometheus-metrics-exposition-formats/src/main/java/io/prometheus/metrics/expositionformats/ExpositionFormatWriter.java deleted file mode 100644 index e693e0c15..000000000 --- a/prometheus-metrics-exposition-formats/src/main/java/io/prometheus/metrics/expositionformats/ExpositionFormatWriter.java +++ /dev/null @@ -1,14 +0,0 @@ -package io.prometheus.metrics.expositionformats; - -import io.prometheus.metrics.model.snapshots.MetricSnapshots; -import java.io.IOException; -import java.io.OutputStream; - -public interface ExpositionFormatWriter { - boolean accepts(String acceptHeader); - - /** Text formats use UTF-8 encoding. */ - void write(OutputStream out, MetricSnapshots metricSnapshots) throws IOException; - - String getContentType(); -} diff --git a/prometheus-metrics-exposition-formats/src/main/java/io/prometheus/metrics/expositionformats/ProtobufUtil.java b/prometheus-metrics-exposition-formats/src/main/java/io/prometheus/metrics/expositionformats/ProtobufUtil.java deleted file mode 100644 index 5b192c23d..000000000 --- a/prometheus-metrics-exposition-formats/src/main/java/io/prometheus/metrics/expositionformats/ProtobufUtil.java +++ /dev/null @@ -1,13 +0,0 @@ -package io.prometheus.metrics.expositionformats; - -import com.google.protobuf.Timestamp; - -public class ProtobufUtil { - - static Timestamp timestampFromMillis(long timestampMillis) { - return Timestamp.newBuilder() - .setSeconds(timestampMillis / 1000L) - .setNanos((int) (timestampMillis % 1000L * 1000000L)) - .build(); - } -} diff --git a/prometheus-metrics-exposition-formats/src/main/java/io/prometheus/metrics/expositionformats/PrometheusProtobufWriter.java b/prometheus-metrics-exposition-formats/src/main/java/io/prometheus/metrics/expositionformats/internal/PrometheusProtobufWriterImpl.java similarity index 94% rename from prometheus-metrics-exposition-formats/src/main/java/io/prometheus/metrics/expositionformats/PrometheusProtobufWriter.java rename to prometheus-metrics-exposition-formats/src/main/java/io/prometheus/metrics/expositionformats/internal/PrometheusProtobufWriterImpl.java index fb3b9412d..a52a83adf 100644 --- a/prometheus-metrics-exposition-formats/src/main/java/io/prometheus/metrics/expositionformats/PrometheusProtobufWriter.java +++ b/prometheus-metrics-exposition-formats/src/main/java/io/prometheus/metrics/expositionformats/internal/PrometheusProtobufWriterImpl.java @@ -1,8 +1,9 @@ -package io.prometheus.metrics.expositionformats; +package io.prometheus.metrics.expositionformats.internal; -import static io.prometheus.metrics.expositionformats.ProtobufUtil.timestampFromMillis; +import static io.prometheus.metrics.expositionformats.internal.ProtobufUtil.timestampFromMillis; import com.google.protobuf.TextFormat; +import io.prometheus.metrics.expositionformats.ExpositionFormatWriter; import io.prometheus.metrics.expositionformats.generated.com_google_protobuf_4_28_3.Metrics; import io.prometheus.metrics.model.snapshots.ClassicHistogramBuckets; import io.prometheus.metrics.model.snapshots.CounterSnapshot; @@ -24,33 +25,19 @@ import java.io.IOException; import java.io.OutputStream; -/** - * Write the Prometheus protobuf format as defined in github.com/prometheus/client_model. - * - *

As of today, this is the only exposition format that supports native histograms. - */ -public class PrometheusProtobufWriter implements ExpositionFormatWriter { - - public static final String CONTENT_TYPE = - "application/vnd.google.protobuf; proto=io.prometheus.client.MetricFamily; " - + "encoding=delimited"; +public class PrometheusProtobufWriterImpl implements ExpositionFormatWriter { @Override public boolean accepts(String acceptHeader) { - if (acceptHeader == null) { - return false; - } else { - return acceptHeader.contains("application/vnd.google.protobuf") - && acceptHeader.contains("proto=io.prometheus.client.MetricFamily"); - } + throw new IllegalStateException("use PrometheusProtobufWriter instead"); } @Override public String getContentType() { - return CONTENT_TYPE; + throw new IllegalStateException("use PrometheusProtobufWriter instead"); } + @Override public String toDebugString(MetricSnapshots metricSnapshots) { StringBuilder stringBuilder = new StringBuilder(); for (MetricSnapshot snapshot : metricSnapshots) { diff --git a/prometheus-metrics-exposition-formats/src/main/java/io/prometheus/metrics/expositionformats/internal/ProtobufUtil.java b/prometheus-metrics-exposition-formats/src/main/java/io/prometheus/metrics/expositionformats/internal/ProtobufUtil.java new file mode 100644 index 000000000..75e3d0ef2 --- /dev/null +++ b/prometheus-metrics-exposition-formats/src/main/java/io/prometheus/metrics/expositionformats/internal/ProtobufUtil.java @@ -0,0 +1,19 @@ +package io.prometheus.metrics.expositionformats.internal; + +import com.google.protobuf.MessageOrBuilder; +import com.google.protobuf.TextFormat; +import com.google.protobuf.Timestamp; + +public class ProtobufUtil { + + static Timestamp timestampFromMillis(long timestampMillis) { + return Timestamp.newBuilder() + .setSeconds(timestampMillis / 1000L) + .setNanos((int) (timestampMillis % 1000L * 1000000L)) + .build(); + } + + public static String shortDebugString(MessageOrBuilder protobufData) { + return TextFormat.printer().emittingSingleLine(true).printToString(protobufData); + } +} diff --git a/prometheus-metrics-exposition-formats/src/test/java/io/prometheus/metrics/expositionformats/ExpositionFormatsTest.java b/prometheus-metrics-exposition-formats/src/test/java/io/prometheus/metrics/expositionformats/ExpositionFormatsTest.java index e5cd3f089..1d98925e3 100644 --- a/prometheus-metrics-exposition-formats/src/test/java/io/prometheus/metrics/expositionformats/ExpositionFormatsTest.java +++ b/prometheus-metrics-exposition-formats/src/test/java/io/prometheus/metrics/expositionformats/ExpositionFormatsTest.java @@ -3,6 +3,8 @@ import static org.assertj.core.api.Assertions.assertThat; import io.prometheus.metrics.expositionformats.generated.com_google_protobuf_4_28_3.Metrics; +import io.prometheus.metrics.expositionformats.internal.PrometheusProtobufWriterImpl; +import io.prometheus.metrics.expositionformats.internal.ProtobufUtil; import io.prometheus.metrics.model.snapshots.ClassicHistogramBuckets; import io.prometheus.metrics.model.snapshots.CounterSnapshot; import io.prometheus.metrics.model.snapshots.CounterSnapshot.CounterDataPointSnapshot; @@ -2658,9 +2660,9 @@ private void assertPrometheusTextWithoutCreated(String expected, MetricSnapshot } private void assertPrometheusProtobuf(String expected, MetricSnapshot snapshot) { - PrometheusProtobufWriter writer = new PrometheusProtobufWriter(); + PrometheusProtobufWriterImpl writer = new PrometheusProtobufWriterImpl(); Metrics.MetricFamily protobufData = writer.convert(snapshot); - String actual = TextFormatUtil.shortDebugString(protobufData); + String actual = ProtobufUtil.shortDebugString(protobufData); assertThat(actual).isEqualTo(expected); } } diff --git a/prometheus-metrics-exposition-textformats/pom.xml b/prometheus-metrics-exposition-textformats/pom.xml new file mode 100644 index 000000000..161fb005f --- /dev/null +++ b/prometheus-metrics-exposition-textformats/pom.xml @@ -0,0 +1,36 @@ + + + 4.0.0 + + + io.prometheus + client_java + 1.3.3 + + + prometheus-metrics-exposition-textformats + bundle + + Prometheus Metrics Exposition Text Formats + + Prometheus exposition text formats. + + + + io.prometheus.writer.text + + + + + io.prometheus + prometheus-metrics-model + ${project.version} + + + io.prometheus + prometheus-metrics-config + ${project.version} + + + diff --git a/prometheus-metrics-exposition-textformats/src/main/java/io/prometheus/metrics/expositionformats/ExpositionFormatWriter.java b/prometheus-metrics-exposition-textformats/src/main/java/io/prometheus/metrics/expositionformats/ExpositionFormatWriter.java new file mode 100644 index 000000000..b472af0e1 --- /dev/null +++ b/prometheus-metrics-exposition-textformats/src/main/java/io/prometheus/metrics/expositionformats/ExpositionFormatWriter.java @@ -0,0 +1,32 @@ +package io.prometheus.metrics.expositionformats; + +import io.prometheus.metrics.model.snapshots.MetricSnapshots; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; + +public interface ExpositionFormatWriter { + boolean accepts(String acceptHeader); + + /** Text formats use UTF-8 encoding. */ + void write(OutputStream out, MetricSnapshots metricSnapshots) throws IOException; + + default String toDebugString(MetricSnapshots metricSnapshots) { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + try { + write(out, metricSnapshots); + return out.toString("UTF-8"); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + String getContentType(); + + /** + * Returns true if the writer is available. If false, the writer will throw an exception if used. + */ + default boolean isAvailable() { + return true; + } +} diff --git a/prometheus-metrics-exposition-formats/src/main/java/io/prometheus/metrics/expositionformats/ExpositionFormats.java b/prometheus-metrics-exposition-textformats/src/main/java/io/prometheus/metrics/expositionformats/ExpositionFormats.java similarity index 100% rename from prometheus-metrics-exposition-formats/src/main/java/io/prometheus/metrics/expositionformats/ExpositionFormats.java rename to prometheus-metrics-exposition-textformats/src/main/java/io/prometheus/metrics/expositionformats/ExpositionFormats.java diff --git a/prometheus-metrics-exposition-formats/src/main/java/io/prometheus/metrics/expositionformats/OpenMetricsTextFormatWriter.java b/prometheus-metrics-exposition-textformats/src/main/java/io/prometheus/metrics/expositionformats/OpenMetricsTextFormatWriter.java similarity index 100% rename from prometheus-metrics-exposition-formats/src/main/java/io/prometheus/metrics/expositionformats/OpenMetricsTextFormatWriter.java rename to prometheus-metrics-exposition-textformats/src/main/java/io/prometheus/metrics/expositionformats/OpenMetricsTextFormatWriter.java diff --git a/prometheus-metrics-exposition-textformats/src/main/java/io/prometheus/metrics/expositionformats/PrometheusProtobufWriter.java b/prometheus-metrics-exposition-textformats/src/main/java/io/prometheus/metrics/expositionformats/PrometheusProtobufWriter.java new file mode 100644 index 000000000..0572a99a7 --- /dev/null +++ b/prometheus-metrics-exposition-textformats/src/main/java/io/prometheus/metrics/expositionformats/PrometheusProtobufWriter.java @@ -0,0 +1,73 @@ +package io.prometheus.metrics.expositionformats; + +import io.prometheus.metrics.model.snapshots.MetricSnapshots; +import java.io.IOException; +import java.io.OutputStream; +import javax.annotation.Nullable; + +/** + * Write the Prometheus protobuf format as defined in github.com/prometheus/client_model. + * + *

As of today, this is the only exposition format that supports native histograms. + */ +public class PrometheusProtobufWriter implements ExpositionFormatWriter { + + @Nullable private static final ExpositionFormatWriter DELEGATE = createProtobufWriter(); + + public static final String CONTENT_TYPE = + "application/vnd.google.protobuf; proto=io.prometheus.client.MetricFamily; " + + "encoding=delimited"; + + @Nullable + private static ExpositionFormatWriter createProtobufWriter() { + try { + return Class.forName( + "io.prometheus.metrics.expositionformats.internal.PrometheusProtobufWriterImpl") + .asSubclass(ExpositionFormatWriter.class) + .getDeclaredConstructor() + .newInstance(); + } catch (Exception e) { + // not in classpath + return null; + } + } + + @Override + public boolean accepts(String acceptHeader) { + if (acceptHeader == null) { + return false; + } else { + return acceptHeader.contains("application/vnd.google.protobuf") + && acceptHeader.contains("proto=io.prometheus.client.MetricFamily"); + } + } + + @Override + public String getContentType() { + return CONTENT_TYPE; + } + + @Override + public boolean isAvailable() { + return DELEGATE != null; + } + + @Override + public String toDebugString(MetricSnapshots metricSnapshots) { + checkAvailable(); + return DELEGATE.toDebugString(metricSnapshots); + } + + @Override + public void write(OutputStream out, MetricSnapshots metricSnapshots) throws IOException { + checkAvailable(); + DELEGATE.write(out, metricSnapshots); + } + + private void checkAvailable() { + if (DELEGATE == null) { + throw new UnsupportedOperationException("Prometheus protobuf writer not available"); + } + } +} diff --git a/prometheus-metrics-exposition-formats/src/main/java/io/prometheus/metrics/expositionformats/PrometheusTextFormatWriter.java b/prometheus-metrics-exposition-textformats/src/main/java/io/prometheus/metrics/expositionformats/PrometheusTextFormatWriter.java similarity index 100% rename from prometheus-metrics-exposition-formats/src/main/java/io/prometheus/metrics/expositionformats/PrometheusTextFormatWriter.java rename to prometheus-metrics-exposition-textformats/src/main/java/io/prometheus/metrics/expositionformats/PrometheusTextFormatWriter.java diff --git a/prometheus-metrics-exposition-formats/src/main/java/io/prometheus/metrics/expositionformats/TextFormatUtil.java b/prometheus-metrics-exposition-textformats/src/main/java/io/prometheus/metrics/expositionformats/TextFormatUtil.java similarity index 90% rename from prometheus-metrics-exposition-formats/src/main/java/io/prometheus/metrics/expositionformats/TextFormatUtil.java rename to prometheus-metrics-exposition-textformats/src/main/java/io/prometheus/metrics/expositionformats/TextFormatUtil.java index c61a8f1b6..54daaaa3e 100644 --- a/prometheus-metrics-exposition-formats/src/main/java/io/prometheus/metrics/expositionformats/TextFormatUtil.java +++ b/prometheus-metrics-exposition-textformats/src/main/java/io/prometheus/metrics/expositionformats/TextFormatUtil.java @@ -1,7 +1,5 @@ package io.prometheus.metrics.expositionformats; -import com.google.protobuf.MessageOrBuilder; -import com.google.protobuf.TextFormat; import io.prometheus.metrics.model.snapshots.Labels; import java.io.IOException; import java.io.OutputStreamWriter; @@ -83,8 +81,4 @@ static void writeLabels( } writer.write('}'); } - - public static String shortDebugString(MessageOrBuilder protobufData) { - return TextFormat.printer().emittingSingleLine(true).printToString(protobufData); - } } diff --git a/prometheus-metrics-instrumentation-caffeine/pom.xml b/prometheus-metrics-instrumentation-caffeine/pom.xml index 56a7b1836..41a7a6e62 100644 --- a/prometheus-metrics-instrumentation-caffeine/pom.xml +++ b/prometheus-metrics-instrumentation-caffeine/pom.xml @@ -44,7 +44,7 @@ io.prometheus - prometheus-metrics-exposition-formats + prometheus-metrics-exposition-textformats ${project.version} test diff --git a/prometheus-metrics-instrumentation-dropwizard5/pom.xml b/prometheus-metrics-instrumentation-dropwizard5/pom.xml index d75143796..ae0d657bc 100644 --- a/prometheus-metrics-instrumentation-dropwizard5/pom.xml +++ b/prometheus-metrics-instrumentation-dropwizard5/pom.xml @@ -43,7 +43,7 @@ io.prometheus - prometheus-metrics-exposition-formats + prometheus-metrics-exposition-textformats ${project.version} test diff --git a/prometheus-metrics-instrumentation-guava/pom.xml b/prometheus-metrics-instrumentation-guava/pom.xml index c4427df91..6e816ee31 100644 --- a/prometheus-metrics-instrumentation-guava/pom.xml +++ b/prometheus-metrics-instrumentation-guava/pom.xml @@ -41,7 +41,7 @@ io.prometheus - prometheus-metrics-exposition-formats + prometheus-metrics-exposition-textformats ${project.version} test diff --git a/prometheus-metrics-instrumentation-jvm/pom.xml b/prometheus-metrics-instrumentation-jvm/pom.xml index ba433c0b7..a9edf9352 100644 --- a/prometheus-metrics-instrumentation-jvm/pom.xml +++ b/prometheus-metrics-instrumentation-jvm/pom.xml @@ -37,7 +37,7 @@ io.prometheus - prometheus-metrics-exposition-formats + prometheus-metrics-exposition-textformats ${project.version} test diff --git a/prometheus-metrics-simpleclient-bridge/pom.xml b/prometheus-metrics-simpleclient-bridge/pom.xml index 6d9ff2785..3112db2f9 100644 --- a/prometheus-metrics-simpleclient-bridge/pom.xml +++ b/prometheus-metrics-simpleclient-bridge/pom.xml @@ -41,7 +41,7 @@ io.prometheus - prometheus-metrics-exposition-formats + prometheus-metrics-exposition-textformats ${project.version} test diff --git a/scripts/build-release.sh b/scripts/build-release.sh new file mode 100755 index 000000000..ecbb07b8e --- /dev/null +++ b/scripts/build-release.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash + +set -euo pipefail + +TAG=$1 +VERSION=${TAG#v} + +mvn versions:set -DnewVersion=$VERSION +cd integration-tests/it-spring-boot-smoke-test +mvn versions:set -DnewVersion=$VERSION +mvn -B package -P release -Dmaven.test.skip=true diff --git a/scripts/run-native-tests.sh b/scripts/run-native-tests.sh new file mode 100755 index 000000000..535bc136c --- /dev/null +++ b/scripts/run-native-tests.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash + +set -euo pipefail + +./mvnw clean install -DskipTests +cd integration-tests/it-spring-boot-smoke-test +../../mvnw test -PnativeTest