```
-
-
## Programmatic
Today, most Servlet applications use an embedded Servlet container and configure Servlets
@@ -40,12 +36,12 @@ The API for that depends on the Servlet container.
The [examples](https://github.com/prometheus/client_java/tree/1.0.x/examples) directory has an
example of an embedded
[Tomcat](https://tomcat.apache.org/) container with the
-[PrometheusMetricsServlet](/client_java/api/io/prometheus/metrics/exporter/servlet/jakarta/PrometheusMetricsServlet.html)
+[PrometheusMetricsServlet](/client_java/api/io/prometheus/metrics/exporter/servlet/jakarta/PrometheusMetricsServlet.html)
configured.
## Spring
You can use
-the [PrometheusMetricsServlet](/client_java/api/io/prometheus/metrics/exporter/servlet/jakarta/PrometheusMetricsServlet.html)
+the [PrometheusMetricsServlet](/client_java/api/io/prometheus/metrics/exporter/servlet/jakarta/PrometheusMetricsServlet.html)
in Spring applications.
See [our Spring doc]({{< relref "spring.md" >}}).
diff --git a/docs/content/exporters/spring.md b/docs/content/exporters/spring.md
index fc0d946dd..80a7739fa 100644
--- a/docs/content/exporters/spring.md
+++ b/docs/content/exporters/spring.md
@@ -1,6 +1,6 @@
---
title: Spring
-weight: 5
+weight: 7
---
## Alternative: Use Spring's Built-in Metrics Library
@@ -26,7 +26,7 @@ Spring anyway. Maybe you want full support for all Prometheus metric types,
or you want to use the new Prometheus native histograms.
The easiest way to use the Prometheus metrics library in Spring is to configure the
-[PrometheusMetricsServlet](/client_java/api/io/prometheus/metrics/exporter/servlet/jakarta/PrometheusMetricsServlet.html)
+[PrometheusMetricsServlet](/client_java/api/io/prometheus/metrics/exporter/servlet/jakarta/PrometheusMetricsServlet.html)
to expose metrics.
Dependencies:
@@ -76,7 +76,7 @@ public class DemoApplication {
```
The important part are the last three lines: They configure the
-[PrometheusMetricsServlet](/client_java/api/io/prometheus/metrics/exporter/servlet/jakarta/PrometheusMetricsServlet.html)
+[PrometheusMetricsServlet](/client_java/api/io/prometheus/metrics/exporter/servlet/jakarta/PrometheusMetricsServlet.html)
to expose metrics on `/metrics`:
```java
diff --git a/docs/content/exporters/unicode.md b/docs/content/exporters/unicode.md
new file mode 100644
index 000000000..026292c39
--- /dev/null
+++ b/docs/content/exporters/unicode.md
@@ -0,0 +1,34 @@
+---
+title: Unicode
+weight: 2
+---
+
+{{< hint type=warning >}}
+Unicode support is experimental, because [OpenMetrics specification](https://openmetrics.io/) is not
+updated yet to support Unicode characters in metric and label names.
+{{< /hint >}}
+
+The Prometheus Java client library allows all Unicode characters, that can be encoded as UTF-8.
+
+At scrape time, some characters are replaced based on the `encoding` header according
+to
+the [Escaping scheme](https://github.com/prometheus/docs/blob/main/docs/instrumenting/escaping_schemes.md).
+
+For example, if you use the `underscores` escaping scheme, dots in metric and label names are
+replaced with underscores, so that the metric name `http.server.duration` becomes
+`http_server_duration`.
+
+Prometheus servers that do not support Unicode at all will not pass the `encoding` header, and the
+Prometheus Java client library will replace dots, as well as any character that is not in the legacy
+character set (`a-zA-Z0-9_:`), with underscores by default.
+
+When `escaping=allow-utf-8` is passed, add valid UTF-8 characters to the metric and label names
+without replacing them. This allows you to use dots in metric and label names, as well as
+other UTF-8 characters, without any replacements.
+
+## PushGateway
+
+When using the [Pushgateway]({{< relref "pushgateway.md" >}}), Unicode support has to be enabled
+explicitly by setting `io.prometheus.exporter.pushgateway.escapingScheme` to `allow-utf-8` in the
+Pushgateway configuration file - see
+[Pushgateway configuration]({{< relref "/config/config.md#exporter-pushgateway-properties" >}})
diff --git a/docs/content/getting-started/labels.md b/docs/content/getting-started/labels.md
index d056a6ce6..1cbae13d7 100644
--- a/docs/content/getting-started/labels.md
+++ b/docs/content/getting-started/labels.md
@@ -150,4 +150,4 @@ Counter counter = Counter.builder()
However, most use cases for `constLabels()` are better covered by target labels set by the scraping
Prometheus server,
or by one specific metric (e.g. a `build_info` or a `machine_role` metric). See also
-[target labels, not static scraped labels](https://prometheus.io/docs/instrumenting/writing_exporters/#target-labels-not-static-scraped-labels).
+[target labels, not static scraped labels](https://prometheus.io/docs/instrumenting/writing_exporters/#target-labels-not-static-scraped-labels).
diff --git a/docs/content/getting-started/metric-types.md b/docs/content/getting-started/metric-types.md
index 3bcda84fe..752205107 100644
--- a/docs/content/getting-started/metric-types.md
+++ b/docs/content/getting-started/metric-types.md
@@ -37,9 +37,13 @@ serviceTimeSeconds.inc(Unit.millisToSeconds(200));
The resulting counter has the value `0.2`. As `SECONDS` is the standard time unit in Prometheus, the
`Unit` utility class has methods to convert other time units to seconds.
-As defined in [OpenMetrics](https://openmetrics.io/), counter metric names must have the `_total`
-suffix. If you create a counter without the `_total` suffix the suffix will be appended
-automatically.
+For the default OpenMetrics 1.0 and Prometheus text formats, counters are exposed with the
+`_total` suffix. You can name a counter either `service_time_seconds` or
+`service_time_seconds_total`; the exposed name will be `service_time_seconds_total` in both cases.
+
+The experimental OpenMetrics 2.0 writer behaves differently: It preserves metric names instead of
+appending `_total` or unit suffixes automatically. In OpenMetrics 2.0, `_total` is recommended for
+counters, but not enforced by the Java client.
## Gauge
@@ -84,7 +88,7 @@ the Prometheus server.
format and ingest both, the classic and the native flavor. This is great for migrating from
classic histograms to native histograms.
-See [examples/example-native-histogram](https://github.com/prometheus/client_java/tree/1.0.x/examples/example-native-histogram)
+See [examples/example-native-histogram](https://github.com/prometheus/client_java/tree/1.0.x/examples/example-native-histogram)
for an example.
```java
@@ -109,9 +113,9 @@ most cases you don't need them, defaults are good. The following is an incomplet
most important options:
- `nativeOnly()` / `classicOnly()`: Create a histogram with one representation only.
-- `classicBuckets(...)`: Set the classic bucket boundaries. Default buckets are `.005`, `.01`,
- `.025`, `.05`, `.1`, `.25`, `.5`, `1`, `2.5`, `5`, `and 10`. The default bucket boundaries are
- designed for measuring request durations in seconds.
+- `classicUpperBounds(...)`: Set the classic bucket upper boundaries. Default bucket upper
+ boundaries are `.005`, `.01`, `.025`, `.05`, `.1`, `.25`, `.5`, `1`, `2.5`, `5`, `and 10`. The
+ default bucket boundaries are designed for measuring request durations in seconds.
- `nativeMaxNumberOfBuckets()`: Upper limit for the number of native histogram buckets.
Default is 160. When the maximum is reached, the native histogram automatically
reduces resolution to stay below the limit.
@@ -121,6 +125,98 @@ for [Histogram.Builder](/client_java/api/io/prometheus/metrics/core/metrics/Hist
for a complete list of options. Some options can be configured at runtime,
see [config]({{< relref "../config/config.md" >}}).
+### Custom Bucket Boundaries
+
+The default bucket boundaries are designed for measuring request durations in seconds. For other
+use cases, you may want to define custom bucket boundaries. The histogram builder provides three
+methods for this:
+
+
+
+**1. Arbitrary Custom Boundaries**
+
+Use `classicUpperBounds(...)` to specify arbitrary bucket boundaries:
+
+```java
+Histogram responseSize = Histogram.builder()
+ .name("http_response_size_bytes")
+ .help("HTTP response size in bytes")
+ .classicUpperBounds(100, 1000, 10000, 100000, 1000000) // bytes
+ .register();
+```
+
+**2. Linear Boundaries**
+
+Use `classicLinearUpperBounds(start, width, count)` for equal-width buckets:
+
+```java
+Histogram queueSize = Histogram.builder()
+ .name("queue_size")
+ .help("Number of items in queue")
+ .classicLinearUpperBounds(10, 10, 10) // 10, 20, 30, ..., 100
+ .register();
+```
+
+**3. Exponential Boundaries**
+
+
+
+Use `classicExponentialUpperBounds(start, factor, count)` for exponential growth:
+
+```java
+Histogram dataSize = Histogram.builder()
+ .name("data_size_bytes")
+ .help("Data size in bytes")
+ .classicExponentialUpperBounds(100, 10, 5) // 100, 1k, 10k, 100k, 1M
+ .register();
+```
+
+### Native Histograms with Custom Buckets (NHCB)
+
+Prometheus supports a special mode called Native Histograms with Custom Buckets (NHCB) that uses
+schema -53. In this mode, custom bucket boundaries from classic histograms are preserved when
+converting to native histograms.
+
+The Java client library automatically supports NHCB:
+
+1. By default, histograms maintain both classic (with custom buckets) and native representations
+2. The classic representation with custom buckets is exposed to Prometheus
+3. Prometheus servers can convert these to NHCB upon ingestion when configured with the
+ `convert_classic_histograms_to_nhcb` scrape option
+
+Example:
+
+```java
+// This histogram will work seamlessly with NHCB
+Histogram apiLatency = Histogram.builder()
+ .name("api_request_duration_seconds")
+ .help("API request duration")
+ .classicUpperBounds(0.01, 0.05, 0.1, 0.5, 1.0, 5.0, 10.0) // custom boundaries
+ .register();
+```
+
+On the Prometheus side, configure the scrape job:
+
+```yaml
+scrape_configs:
+ - job_name: "my-app"
+ scrape_protocols: ["PrometheusProto"]
+ convert_classic_histograms_to_nhcb: true
+ static_configs:
+ - targets: ["localhost:9400"]
+```
+
+{{< hint type=note >}}
+NHCB is useful when:
+
+- You need precise bucket boundaries for your specific use case
+- You're migrating from classic histograms and want to preserve bucket boundaries
+- Exponential bucketing from standard native histograms isn't a good fit for your distribution
+ {{< /hint >}}
+
+See [examples/example-custom-buckets](https://github.com/prometheus/client_java/tree/main/examples/example-custom-buckets)
+for a complete example with Prometheus and Grafana.
+
Histograms and summaries are both used for observing distributions. Therefore, the both implement
the `DistributionDataPoint` interface. Using the `DistributionDataPoint` interface directly gives
you the option to switch between histograms and summaries later with minimal code changes.
@@ -200,7 +296,7 @@ be changed with `maxAgeSeconds()` and `numberOfAgeBuckets()`.
Some options can be configured at runtime, see [config]({{< relref "../config/config.md" >}}).
In general you should prefer histograms over summaries. The Prometheus query language has a
-function [histogram_quantile()](https://prometheus.io/docs/prometheus/latest/querying/functions/#histogram_quantile)
+function [histogram_quantile()](https://prometheus.io/docs/prometheus/latest/querying/functions/#histogram_quantile)
for calculating quantiles from histograms. The advantage of query-time quantile calculation is that
you can aggregate histograms before calculating the quantile. With summaries you must use the
quantile with all its labels as it is.
@@ -226,16 +322,12 @@ info.setLabelValues(version, vendor, runtime);
The info above looks as follows in OpenMetrics text format:
-
-
```text
# TYPE jvm_runtime info
# HELP jvm_runtime JVM runtime info
jvm_runtime_info{runtime="OpenJDK Runtime Environment",vendor="Oracle Corporation",version="1.8.0_382-b05"} 1
```
-
-
The example is taken from the `prometheus-metrics-instrumentation-jvm` module, so if you have
`JvmMetrics` registered you should have a `jvm_runtime_info` metric out-of-the-box.
@@ -276,3 +368,6 @@ in the `prometheus-metrics-core` API.
However, `prometheus-metrics-model` implements the underlying data model for these types.
To use these types, you need to implement your own `Collector` where the `collect()` method returns
an `UnknownSnapshot` or a `HistogramSnapshot` with `.gaugeHistogram(true)`.
+If your custom collector does not implement `getMetricType()` and `getLabelNames()`, ensure it does
+not produce the same metric name and label set as another collector, or the exposition may contain
+duplicate time series.
diff --git a/docs/content/getting-started/multi-target.md b/docs/content/getting-started/multi-target.md
index 16f85ac40..cfa0b841f 100644
--- a/docs/content/getting-started/multi-target.md
+++ b/docs/content/getting-started/multi-target.md
@@ -11,8 +11,6 @@ To support multi-target pattern you can create a custom collector overriding the
method in ExtendedMultiCollector
see SampleExtendedMultiCollector in io.prometheus.metrics.examples.httpserver
-
-
```java
public class SampleExtendedMultiCollector extends ExtendedMultiCollector {
@@ -80,8 +78,6 @@ public class SampleExtendedMultiCollector extends ExtendedMultiCollector {
```
-
-
`PrometheusScrapeRequest` provides methods to access http-related infos from the request originally
received by the endpoint
diff --git a/docs/content/getting-started/performance.md b/docs/content/getting-started/performance.md
index 31b8de162..42b2a0a48 100644
--- a/docs/content/getting-started/performance.md
+++ b/docs/content/getting-started/performance.md
@@ -53,20 +53,20 @@ In performance critical applications we recommend to use either the classic repr
native representation, but not both.
You can either configure this in code for each histogram by
-calling [classicOnly()]()
-or [nativeOnly()](),
+calling [classicOnly()]()
+or [nativeOnly()](),
or you use the corresponding [config options]({{< relref "../config/config.md" >}}).
One way to do this is with system properties in the command line when you start your application
```sh
-java -Dio.prometheus.metrics.histogramClassicOnly=true my-app.jar
+java -Dio.prometheus.metrics.histogram_classic_only=true my-app.jar
```
or
```sh
-java -Dio.prometheus.metrics.histogramNativeOnly=true my-app.jar
+java -Dio.prometheus.metrics.histogram_native_only=true my-app.jar
```
If you don't want to add a command line parameter every time you start your application, you can add
@@ -75,13 +75,13 @@ that it gets packed into your JAR file). The `prometheus.properties` file should
line:
```properties
-io.prometheus.metrics.histogramClassicOnly=true
+io.prometheus.metrics.histogram_classic_only=true
```
or
```properties
-io.prometheus.metrics.histogramNativeOnly=true
+io.prometheus.metrics.histogram_native_only=true
```
Future releases will add more configuration options, like support for configuration via environment
diff --git a/docs/content/getting-started/quickstart.md b/docs/content/getting-started/quickstart.md
index 920d89b7f..635c63be0 100644
--- a/docs/content/getting-started/quickstart.md
+++ b/docs/content/getting-started/quickstart.md
@@ -63,7 +63,7 @@ it from the dependencies.
## Dependency management
A Bill of Material
-([BOM](https://maven.apache.org/guides/introduction/introduction-to-dependency-mechanism.html#bill-of-materials-bom-poms))
+([BOM](https://maven.apache.org/guides/introduction/introduction-to-dependency-mechanism.html#bill-of-materials-bom-poms))
ensures that versions of dependencies (including transitive ones) are aligned.
This is especially important when using Spring Boot, which manages some of the dependencies of the
project.
diff --git a/docs/content/getting-started/registry.md b/docs/content/getting-started/registry.md
index afebbb304..de437be23 100644
--- a/docs/content/getting-started/registry.md
+++ b/docs/content/getting-started/registry.md
@@ -6,7 +6,7 @@ weight: 2
In order to expose metrics, you need to register them with a `PrometheusRegistry`. We are using a
counter as an example here, but the `register()` method is the same for all metric types.
-## Registering a Metrics with the Default Registry
+## Registering a Metric with the Default Registry
```java
Counter eventsTotal = Counter.builder()
@@ -18,7 +18,7 @@ Counter eventsTotal = Counter.builder()
The `register()` call above builds the counter and registers it with the global static
`PrometheusRegistry.defaultRegistry`. Using the default registry is recommended.
-## Registering a Metrics with a Custom Registry
+## Registering a Metric with a Custom Registry
You can also register your metric with a custom registry:
@@ -78,12 +78,72 @@ Counter eventsTotal2 = Counter.builder()
.register(); // IllegalArgumentException, because a metric with that name is already registered
```
+## Suffix-Based Name Validation
+
+Suffix handling happens at scrape time. This makes metric names more flexible while keeping the
+exposed output unambiguous.
+
+The registry now tracks not only the metric names you register, but also the exposition names they
+would claim in OpenMetrics 1.x and Prometheus text format, such as `_total`, `_count`, `_sum`,
+`_bucket`, `_created`, and `_info`.
+
+This means names are accepted when they are safe, and combinations are rejected when they would
+collide at scrape time. The table below also shows the pre-1.6.0 behavior for comparison.
+
+| Example | Before 1.6.0 | Current behavior | Why |
+| --------------------------------------------- | ------------ | ---------------- | ------------------------------------------------------------------------------------------------- |
+| `Gauge("foo_total")` | Rejected | Allowed | Safe because `_total` suffix expansion applies to counters, not gauges. |
+| `Counter("events_total")` | Rejected | Allowed | Safe because the OM1 output is `events_total`; the writer avoids double-appending `_total`. |
+| `Gauge("foo_total")` + `Histogram("foo")` | Rejected | Allowed | Safe because the exposed names do not overlap: `foo_total` vs `foo_bucket`/`foo_count`/`foo_sum`. |
+| `Gauge("events_total")` + `Counter("events")` | Rejected | Rejected | Rejected because both would expose `events_total` in OM1. |
+| `Gauge("foo_count")` + `Histogram("foo")` | Allowed | Rejected | Rejected because both would claim `foo_count` at scrape time. |
+
+## Validation at registration only
+
+Validation of duplicate metric names and label schemas happens at registration time only.
+Built-in metrics (Counter, Gauge, Histogram, etc.) participate in this validation.
+
+Custom collectors that implement the `Collector` or `MultiCollector` interface can optionally
+expose their registration-time metadata so the registry can enforce consistency. The recommended
+way is to override `getMetricFamilyDescriptor()` (or `getMetricFamilyDescriptors()` on
+`MultiCollector`) and return a `MetricFamilyDescriptor` describing the metric name, type, label
+names, and metadata (help, unit) the collector will emit at scrape time.
+
+```java
+@Override
+public MetricFamilyDescriptor getMetricFamilyDescriptor() {
+ return MetricFamilyDescriptor.gauge("my_metric")
+ .help("Example metric")
+ .labelNames("region")
+ .build();
+}
+```
+
+The fragmented `getPrometheusName()`, `getMetricType()`, `getLabelNames()`, and `getMetadata()`
+methods (and their `MultiCollector` per-name variants) are deprecated. They remain bridged by a
+default implementation of `getMetricFamilyDescriptor()` for compatibility, so existing
+collectors keep working unchanged.
+
+**Validation is skipped when registration-time metadata is unavailable:** if
+`getMetricFamilyDescriptor()` returns `null` (the default when name or type is missing), the
+registry does not validate that collector. If two such collectors produce the same metric name and
+same label set at scrape time, the exposition output may contain duplicate time series and be
+invalid for Prometheus.
+
+This is also relevant for downstream adapter libraries that bridge to this registry. If an adapter
+implements `MultiCollector`, its registration-time metadata must match the metric families it will
+actually emit at scrape time. In practice, the `MetricFamilyDescriptor`s returned from
+`getMetricFamilyDescriptors()` need to describe the same names, types, labels, and suffix behavior
+as the eventual `MetricSnapshot` output. Otherwise an adapter may pass or fail collision checks
+differently after upgrading to a newer client_java release, even if its scrape output logic did not
+change.
+
## Unregistering a Metric
There is no automatic expiry of unused metrics (yet), once a metric is registered it will remain
registered forever.
-However, you can programmatically unregistered an obsolete metric like this:
+However, you can programmatically unregister an obsolete metric like this:
```java
PrometheusRegistry.defaultRegistry.unregister(eventsTotal);
diff --git a/docs/content/instrumentation/caffeine.md b/docs/content/instrumentation/caffeine.md
index 104a9b9fa..90a88c05f 100644
--- a/docs/content/instrumentation/caffeine.md
+++ b/docs/content/instrumentation/caffeine.md
@@ -99,7 +99,7 @@ Two metrics exist for observability specifically of caches that define a `weighe
```text
# TYPE caffeine_cache_eviction_weight counter
-# HELP caffeine_cache_eviction_weight Weight of evicted cache entries, doesn't include manually removed entries // editorconfig-checker-disable-line
+# HELP caffeine_cache_eviction_weight Weight of evicted cache entries, doesn't include manually removed entries
caffeine_cache_eviction_weight_total{cache="mycache"} 5.0
# TYPE caffeine_cache_weighted_size gauge
diff --git a/docs/content/instrumentation/jvm.md b/docs/content/instrumentation/jvm.md
index 804c1b09b..3a658c05b 100644
--- a/docs/content/instrumentation/jvm.md
+++ b/docs/content/instrumentation/jvm.md
@@ -3,6 +3,16 @@ title: JVM
weight: 1
---
+{{< hint type=note >}}
+
+Looking for JVM metrics that follow OTel semantic
+conventions? See
+[OTel JVM Runtime Metrics]({{< relref "../otel/jvm-runtime-metrics.md" >}})
+for an alternative based on OpenTelemetry's
+runtime-telemetry module.
+
+{{< /hint >}}
+
The JVM instrumentation module provides a variety of out-of-the-box JVM and process metrics. To use
it, add the following dependency:
@@ -44,9 +54,9 @@ register all JVM metrics, you can register each of these classes individually ra
## JVM Buffer Pool Metrics
JVM buffer pool metrics are provided by
-the [JvmBufferPoolMetrics](/client_java/api/io/prometheus/metrics/instrumentation/jvm/JvmBufferPoolMetrics.html)
+the [JvmBufferPoolMetrics](/client_java/api/io/prometheus/metrics/instrumentation/jvm/JvmBufferPoolMetrics.html)
class. The data is coming from
-the [BufferPoolMXBean](https://docs.oracle.com/en/java/javase/21/docs/api/java.management/java/lang/management/BufferPoolMXBean.html).
+the [BufferPoolMXBean](https://docs.oracle.com/en/java/javase/21/docs/api/java.management/java/lang/management/BufferPoolMXBean.html).
Example metrics:
```text
@@ -67,13 +77,11 @@ jvm_buffer_pool_used_bytes{pool="mapped"} 0.0
## JVM Class Loading Metrics
JVM class loading metrics are provided by
-the [JvmClassLoadingMetrics](/client_java/api/io/prometheus/metrics/instrumentation/jvm/JvmClassLoadingMetrics.html)
+the [JvmClassLoadingMetrics](/client_java/api/io/prometheus/metrics/instrumentation/jvm/JvmClassLoadingMetrics.html)
class. The data is coming from
-the [ClassLoadingMXBean](https://docs.oracle.com/en/java/javase/21/docs/api/java.management/java/lang/management/ClassLoadingMXBean.html).
+the [ClassLoadingMXBean](https://docs.oracle.com/en/java/javase/21/docs/api/java.management/java/lang/management/ClassLoadingMXBean.html).
Example metrics:
-
-
```text
# HELP jvm_classes_currently_loaded The number of classes that are currently loaded in the JVM
# TYPE jvm_classes_currently_loaded gauge
@@ -86,32 +94,26 @@ jvm_classes_loaded_total 1109.0
jvm_classes_unloaded_total 0.0
```
-
-
## JVM Compilation Metrics
JVM compilation metrics are provided by
-the [JvmCompilationMetrics](/client_java/api/io/prometheus/metrics/instrumentation/jvm/JvmCompilationMetrics.html)
+the [JvmCompilationMetrics](/client_java/api/io/prometheus/metrics/instrumentation/jvm/JvmCompilationMetrics.html)
class. The data is coming from
-the [CompilationMXBean](https://docs.oracle.com/en/java/javase/21/docs/api/java.management/java/lang/management/CompilationMXBean.html).
+the [CompilationMXBean](https://docs.oracle.com/en/java/javase/21/docs/api/java.management/java/lang/management/CompilationMXBean.html).
Example metrics:
-
-
```text
# HELP jvm_compilation_time_seconds_total The total time in seconds taken for HotSpot class compilation
# TYPE jvm_compilation_time_seconds_total counter
jvm_compilation_time_seconds_total 0.152
```
-
-
## JVM Garbage Collector Metrics
JVM garbage collector metrics are provided by
-the [JvmGarbageCollectorMetric](/client_java/api/io/prometheus/metrics/instrumentation/jvm/JvmGarbageCollectorMetrics.html)
+the [JvmGarbageCollectorMetric](/client_java/api/io/prometheus/metrics/instrumentation/jvm/JvmGarbageCollectorMetrics.html)
class. The data is coming from
-the [GarbageCollectorMXBean](https://docs.oracle.com/en/java/javase/21/docs/api/java.management/java/lang/management/GarbageCollectorMXBean.html).
+the [GarbageCollectorMXBean](https://docs.oracle.com/en/java/javase/21/docs/api/java.management/java/lang/management/GarbageCollectorMXBean.html).
Example metrics:
```text
@@ -126,14 +128,12 @@ jvm_gc_collection_seconds_sum{gc="PS Scavenge"} 0.0
## JVM Memory Metrics
JVM memory metrics are provided by
-the [JvmMemoryMetrics](/client_java/api/io/prometheus/metrics/instrumentation/jvm/JvmMemoryMetrics.html)
+the [JvmMemoryMetrics](/client_java/api/io/prometheus/metrics/instrumentation/jvm/JvmMemoryMetrics.html)
class. The data is coming from
-the [MemoryMXBean](https://docs.oracle.com/en/java/javase/21/docs/api/java.management/java/lang/management/MemoryMXBean.html)
-and the [MemoryPoolMXBean](https://docs.oracle.com/en/java/javase/21/docs/api/java.management/java/lang/management/MemoryPoolMXBean.html).
+the [MemoryMXBean](https://docs.oracle.com/en/java/javase/21/docs/api/java.management/java/lang/management/MemoryMXBean.html)
+and the [MemoryPoolMXBean](https://docs.oracle.com/en/java/javase/21/docs/api/java.management/java/lang/management/MemoryPoolMXBean.html).
Example metrics:
-
-
```text
# HELP jvm_memory_committed_bytes Committed (bytes) of a given JVM memory area.
# TYPE jvm_memory_committed_bytes gauge
@@ -208,19 +208,15 @@ jvm_memory_used_bytes{area="heap"} 9051232.0
jvm_memory_used_bytes{area="nonheap"} 1.1490688E7
```
-
-
## JVM Memory Pool Allocation Metrics
JVM memory pool allocation metrics are provided by
-the [JvmMemoryPoolAllocationMetrics](/client_java/api/io/prometheus/metrics/instrumentation/jvm/JvmMemoryPoolAllocationMetrics.html)
+the [JvmMemoryPoolAllocationMetrics](/client_java/api/io/prometheus/metrics/instrumentation/jvm/JvmMemoryPoolAllocationMetrics.html)
class. The data is obtained by adding
-a [NotificationListener](https://docs.oracle.com/en/java/javase/21/docs/api/java.management/javax/management/NotificationListener.html)
-to the [GarbageCollectorMXBean](https://docs.oracle.com/en/java/javase/21/docs/api/java.management/java/lang/management/GarbageCollectorMXBean.html).
+a [NotificationListener](https://docs.oracle.com/en/java/javase/21/docs/api/java.management/javax/management/NotificationListener.html)
+to the [GarbageCollectorMXBean](https://docs.oracle.com/en/java/javase/21/docs/api/java.management/java/lang/management/GarbageCollectorMXBean.html).
Example metrics:
-
-
```text
# HELP jvm_memory_pool_allocated_bytes_total Total bytes allocated in a given JVM memory pool. Only updated after GC, not continuously.
# TYPE jvm_memory_pool_allocated_bytes_total counter
@@ -232,35 +228,27 @@ jvm_memory_pool_allocated_bytes_total{pool="PS Old Gen"} 1428888.0
jvm_memory_pool_allocated_bytes_total{pool="PS Survivor Space"} 4115280.0
```
-
-
## JVM Runtime Info Metric
The JVM runtime info metric is provided by
-the [JvmRuntimeInfoMetric](/client_java/api/io/prometheus/metrics/instrumentation/jvm/JvmRuntimeInfoMetric.html)
+the [JvmRuntimeInfoMetric](/client_java/api/io/prometheus/metrics/instrumentation/jvm/JvmRuntimeInfoMetric.html)
class. The data is obtained via system properties and will not change throughout the lifetime of the
application. Example metric:
-
-
```text
# TYPE jvm_runtime info
# HELP jvm_runtime JVM runtime info
jvm_runtime_info{runtime="OpenJDK Runtime Environment",vendor="Oracle Corporation",version="1.8.0_382-b05"} 1
```
-
-
## JVM Thread Metrics
JVM thread metrics are provided by
-the [JvmThreadsMetrics](/client_java/api/io/prometheus/metrics/instrumentation/jvm/JvmThreadsMetrics.html)
+the [JvmThreadsMetrics](/client_java/api/io/prometheus/metrics/instrumentation/jvm/JvmThreadsMetrics.html)
class. The data is coming from
-the [ThreadMXBean](https://docs.oracle.com/en/java/javase/21/docs/api/java.management/java/lang/management/ThreadMXBean.html).
+the [ThreadMXBean](https://docs.oracle.com/en/java/javase/21/docs/api/java.management/java/lang/management/ThreadMXBean.html).
Example metrics:
-
-
```text
# HELP jvm_threads_current Current thread count of a JVM
# TYPE jvm_threads_current gauge
@@ -291,20 +279,18 @@ jvm_threads_state{state="UNKNOWN"} 0.0
jvm_threads_state{state="WAITING"} 3.0
```
-
-
## Process Metrics
Process metrics are provided by
the [ProcessMetrics](/client_java/api/io/prometheus/metrics/instrumentation/jvm/ProcessMetrics.html)
class. The data is coming from
-the [OperatingSystemMXBean](https://docs.oracle.com/en/java/javase/21/docs/api/java.management/java/lang/management/OperatingSystemMXBean.html),
-the [RuntimeMXBean](https://docs.oracle.com/en/java/javase/21/docs/api/java.management/java/lang/management/RuntimeMXBean.html),
+the [OperatingSystemMXBean](https://docs.oracle.com/en/java/javase/21/docs/api/java.management/java/lang/management/OperatingSystemMXBean.html),
+the [RuntimeMXBean](https://docs.oracle.com/en/java/javase/21/docs/api/java.management/java/lang/management/RuntimeMXBean.html),
and from the `/proc/self/status` file on Linux. The metrics with prefix `process_` are not specific
to Java, but should be provided by every Prometheus client library,
see [Process Metrics](https://prometheus.io/docs/instrumenting/writing_clientlibs/#process-metrics)
in the
-Prometheus [writing client libraries](https://prometheus.io/docs/instrumenting/writing_clientlibs/#process-metrics)
+Prometheus [writing client libraries](https://prometheus.io/docs/instrumenting/writing_clientlibs/#process-metrics)
documentation. Example metrics:
```text
diff --git a/docs/content/internals/model.md b/docs/content/internals/model.md
index c54e79ee3..629e87bf0 100644
--- a/docs/content/internals/model.md
+++ b/docs/content/internals/model.md
@@ -19,13 +19,16 @@ All metric types implement
the [Collector](/client_java/api/io/prometheus/metrics/model/registry/Collector.html) interface,
i.e. they provide
a [collect()]()
-method to produce snapshots.
+method to produce snapshots. Implementers expose their registration-time metadata via
+`getMetricFamilyDescriptor()` (or `getMetricFamilyDescriptors()` on `MultiCollector`). When that
+returns `null`, the collector is not validated at registration and must avoid producing the same
+metric name and label schema as another collector, or exposition may be invalid.
## prometheus-metrics-model
The model is an internal library, implementing read-only immutable snapshots. These snapshots are
returned by
-the [Collector.collect()]()
+the [Collector.collect()]()
method.
There is no need for users to use `prometheus-metrics-model` directly. Users should use the API
diff --git a/docs/content/internals/stability.md b/docs/content/internals/stability.md
new file mode 100644
index 000000000..d960c7ffa
--- /dev/null
+++ b/docs/content/internals/stability.md
@@ -0,0 +1,33 @@
+---
+title: API stability
+weight: 2
+---
+
+The published Java API surface is marked with the
+[`@StableApi`](/client_java/api/io/prometheus/metrics/annotations/StableApi.html) annotation. The
+annotation is opt-in: only annotated types and members are part of the stable, published API and
+follow semantic versioning โ backwards-incompatible changes happen only in a major version bump.
+Unannotated public types are not part of the stability contract and may change in any release.
+
+`@StableApi` can be applied to a type to publish the type and its members, or to individual
+constructors, methods, and fields when only part of a public type is stable.
+
+## API diff check
+
+CI runs [japicmp](https://siom79.github.io/japicmp/) against a pinned baseline release and writes
+the published API diffs under `docs/apidiffs/current_vs_latest/`. Pull requests must keep those
+checked-in diffs up to date. Run it locally with:
+
+```bash
+mise run api-diff
+```
+
+Raw reports are written to `**/target/japicmp/*`.
+
+The baseline version is tracked in `pom.xml` and updated by Renovate; the published baseline diffs
+are stored under `docs/apidiffs/`.
+
+Pull requests that change `docs/apidiffs/current_vs_latest/` are automatically labeled
+`api-change` for additional maintainer review. If the committed API diff contains breaking-change
+markers such as `***!`, `---!`, or `+++!`, the pull request is also labeled
+`breaking-api-change`.
diff --git a/docs/content/otel/jvm-runtime-metrics.md b/docs/content/otel/jvm-runtime-metrics.md
new file mode 100644
index 000000000..f8206be15
--- /dev/null
+++ b/docs/content/otel/jvm-runtime-metrics.md
@@ -0,0 +1,241 @@
+---
+title: JVM Runtime Metrics
+weight: 4
+---
+
+OpenTelemetry's
+[runtime-telemetry](https://github.com/open-telemetry/opentelemetry-java-instrumentation/tree/main/instrumentation/runtime-telemetry)
+module is an alternative to
+[prometheus-metrics-instrumentation-jvm]({{< relref "../instrumentation/jvm.md" >}})
+for users who want JVM metrics following OTel semantic conventions.
+
+Key advantages:
+
+- Metric names follow
+ [OTel semantic conventions](https://opentelemetry.io/docs/specs/semconv/runtime/jvm-metrics/)
+- Java 17+ JFR support (context switches, network I/O,
+ lock contention, memory allocation)
+- Alignment with the broader OTel ecosystem
+
+Since OpenTelemetry's `opentelemetry-exporter-prometheus`
+already depends on this library's `PrometheusRegistry`,
+no additional code is needed in this library โ only the
+OTel SDK wiring shown below.
+
+## Dependencies
+
+Use the [OTel Support]({{< relref "support.md" >}}) module
+to pull in the OTel SDK and Prometheus exporter, then add
+the runtime-telemetry instrumentation:
+
+{{< tabs "jvm-runtime-deps" >}}
+{{< tab "Gradle" >}}
+
+```groovy
+implementation 'io.prometheus:prometheus-metrics-otel-support:$version'
+
+// Use opentelemetry-runtime-telemetry-java8 (Java 8+)
+// or opentelemetry-runtime-telemetry-java17 (Java 17+, JFR-based)
+implementation(
+ 'io.opentelemetry.instrumentation:opentelemetry-runtime-telemetry-java8:$otelVersion-alpha'
+)
+```
+
+{{< /tab >}}
+{{< tab "Maven" >}}
+
+```xml
+
+ io.prometheus
+ prometheus-metrics-otel-support
+ $version
+ pom
+
+
+
+
+
+ io.opentelemetry.instrumentation
+ opentelemetry-runtime-telemetry-java8
+ $otelVersion-alpha
+
+
+
+```
+
+{{< /tab >}}
+{{< /tabs >}}
+
+## Standalone Setup
+
+If you **only** want OTel runtime metrics exposed as
+Prometheus, without any Prometheus Java client metrics:
+
+```java
+import io.opentelemetry.exporter.prometheus.PrometheusHttpServer;
+import io.opentelemetry.instrumentation.runtimemetrics.java8.RuntimeMetrics;
+import io.opentelemetry.sdk.OpenTelemetrySdk;
+import io.opentelemetry.sdk.metrics.SdkMeterProvider;
+
+PrometheusHttpServer prometheusServer =
+ PrometheusHttpServer.builder()
+ .setPort(9464)
+ .build();
+
+OpenTelemetrySdk openTelemetry =
+ OpenTelemetrySdk.builder()
+ .setMeterProvider(
+ SdkMeterProvider.builder()
+ .registerMetricReader(prometheusServer)
+ .build())
+ .build();
+
+RuntimeMetrics runtimeMetrics =
+ RuntimeMetrics.builder(openTelemetry).build();
+
+// Close on shutdown to stop metric collection and server
+Runtime.getRuntime().addShutdownHook(new Thread(() -> {
+ runtimeMetrics.close();
+ prometheusServer.close();
+}));
+
+// Scrape at http://localhost:9464/metrics
+```
+
+## Combined with Prometheus Java Client Metrics
+
+If you already have Prometheus Java client metrics and want to
+add OTel runtime metrics to the **same** `/metrics`
+endpoint, use `PrometheusMetricReader` to bridge OTel
+metrics into a `PrometheusRegistry`:
+
+```java
+import io.prometheus.metrics.core.metrics.Counter;
+import io.prometheus.metrics.exporter.httpserver.HTTPServer;
+import io.prometheus.metrics.model.registry.PrometheusRegistry;
+import io.opentelemetry.exporter.prometheus.PrometheusMetricReader;
+import io.opentelemetry.instrumentation.runtimemetrics.java8.RuntimeMetrics;
+import io.opentelemetry.sdk.OpenTelemetrySdk;
+import io.opentelemetry.sdk.metrics.SdkMeterProvider;
+
+PrometheusRegistry registry =
+ new PrometheusRegistry();
+
+// Register Prometheus metrics as usual
+Counter myCounter = Counter.builder()
+ .name("my_requests_total")
+ .register(registry);
+
+// Bridge OTel metrics into the same registry
+PrometheusMetricReader reader =
+ PrometheusMetricReader.create();
+registry.register(reader);
+
+OpenTelemetrySdk openTelemetry =
+ OpenTelemetrySdk.builder()
+ .setMeterProvider(
+ SdkMeterProvider.builder()
+ .registerMetricReader(reader)
+ .build())
+ .build();
+
+RuntimeMetrics runtimeMetrics =
+ RuntimeMetrics.builder(openTelemetry).build();
+Runtime.getRuntime()
+ .addShutdownHook(new Thread(runtimeMetrics::close));
+
+// Expose everything on one endpoint
+HTTPServer.builder()
+ .port(9400)
+ .registry(registry)
+ .buildAndStart();
+```
+
+The [examples/example-otel-jvm-runtime-metrics](https://github.com/prometheus/client_java/tree/main/examples/example-otel-jvm-runtime-metrics)
+directory has a complete runnable example.
+
+## Configuration
+
+The `RuntimeMetricsBuilder` supports two configuration
+options:
+
+### `captureGcCause()`
+
+Adds a `jvm.gc.cause` attribute to the `jvm.gc.duration`
+metric, indicating why the garbage collection occurred
+(e.g. `G1 Evacuation Pause`, `System.gc()`):
+
+```java
+RuntimeMetrics.builder(openTelemetry)
+ .captureGcCause()
+ .build();
+```
+
+### `emitExperimentalTelemetry()`
+
+Enables additional experimental metrics beyond the stable
+set. These are not yet part of the OTel semantic conventions
+and may change in future releases:
+
+- Buffer pool metrics (direct and mapped byte buffers)
+- Extended CPU metrics
+- Extended memory pool metrics
+- File descriptor metrics
+
+```java
+RuntimeMetrics.builder(openTelemetry)
+ .emitExperimentalTelemetry()
+ .build();
+```
+
+Both options can be combined:
+
+```java
+RuntimeMetrics.builder(openTelemetry)
+ .captureGcCause()
+ .emitExperimentalTelemetry()
+ .build();
+```
+
+Selective per-metric registration is not supported by the
+runtime-telemetry API โ it is all-or-nothing with these
+two toggles.
+
+## Java 17 JFR Support
+
+The `opentelemetry-runtime-telemetry-java17` variant adds
+JFR-based metrics. You can selectively enable features:
+
+```java
+import io.opentelemetry.instrumentation.runtimemetrics.java17.JfrFeature;
+import io.opentelemetry.instrumentation.runtimemetrics.java17.RuntimeMetrics;
+
+RuntimeMetrics.builder(openTelemetry)
+ .enableFeature(JfrFeature.BUFFER_METRICS)
+ .enableFeature(JfrFeature.NETWORK_IO_METRICS)
+ .enableFeature(JfrFeature.LOCK_METRICS)
+ .enableFeature(JfrFeature.CONTEXT_SWITCH_METRICS)
+ .build();
+```
+
+## Metric Names
+
+OTel metric names are converted to Prometheus format by
+the exporter. Examples:
+
+| OTel name | Prometheus name |
+| ---------------------------- | ---------------------------------- |
+| `jvm.memory.used` | `jvm_memory_used_bytes` |
+| `jvm.gc.duration` | `jvm_gc_duration_seconds` |
+| `jvm.thread.count` | `jvm_thread_count` |
+| `jvm.class.loaded` | `jvm_class_loaded` |
+| `jvm.cpu.recent_utilization` | `jvm_cpu_recent_utilization_ratio` |
+
+See [Names]({{< relref "names.md" >}}) for full details on
+how OTel names map to Prometheus names.
diff --git a/docs/content/otel/names.md b/docs/content/otel/names.md
index 2945d70e9..66e40f7e2 100644
--- a/docs/content/otel/names.md
+++ b/docs/content/otel/names.md
@@ -5,38 +5,54 @@ weight: 3
OpenTelemetry naming conventions are different from Prometheus naming conventions. The mapping from
OpenTelemetry metric names to Prometheus metric names is well defined in
-OpenTelemetry's [Prometheus and OpenMetrics Compatibility](https://opentelemetry.io/docs/specs/otel/compatibility/prometheus_and_openmetrics/)
+OpenTelemetry's [Prometheus and OpenMetrics Compatibility](https://opentelemetry.io/docs/specs/otel/compatibility/prometheus_and_openmetrics/)
spec, and
-the [OpenTelemetryExporter](/client_java/api/io/prometheus/metrics/exporter/opentelemetry/OpenTelemetryExporter.html)
+the [OpenTelemetryExporter](/client_java/api/io/prometheus/metrics/exporter/opentelemetry/OpenTelemetryExporter.html)
implements that specification.
-The goal is, if you set up a pipeline as illustrated below, you will see the same metric names in
-the Prometheus server as if you had exposed Prometheus metrics directly.
+The goal is, if you set up a pipeline as illustrated below, you will see the same
+metric names in the Prometheus server as if you had exposed Prometheus metrics
+directly.
-
+![Image of a with the Prometheus client library pushing metrics to an OpenTelemetry collector][otel-pipeline]
The main steps when converting OpenTelemetry metric names to Prometheus metric names are:
-- Replace dots with underscores.
+- Escape illegal characters as described in [Unicode support]
- If the metric has a unit, append the unit to the metric name, like `_seconds`.
- If the metric type has a suffix, append it, like `_total` for counters.
+## `preserve_names`
+
+The Prometheus Java client library can also export its own metrics to OpenTelemetry using the
+[OpenTelemetryExporter](/client_java/api/io/prometheus/metrics/exporter/opentelemetry/OpenTelemetryExporter.html).
+
+For that exporter, `io.prometheus.exporter.opentelemetry.preserve_names=true` preserves metric
+names exactly as they were written in the Prometheus Java client.
+
+Examples:
+
+| Prometheus Java metric | Default OTel export | With `preserve_names=true` |
+| ---------------------------------- | --------------------- | --------------------------- |
+| `Counter("events")` | `events` | `events` |
+| `Counter("events_total")` | `events` | `events_total` |
+| `Counter("req").unit(BYTES)` | name `req`, unit `By` | name `req`, unit `By` |
+| `Counter("req_bytes").unit(BYTES)` | name `req`, unit `By` | name `req_bytes`, unit `By` |
+
+Today the default is `false` for backward compatibility. It is planned to change to `true` in the
+next major release.
+
## Dots in Metric and Label Names
OpenTelemetry defines not only a line protocol, but also _semantic conventions_, i.e. standardized
metric and label names. For example,
-OpenTelemetry's [Semantic Conventions for HTTP Metrics](https://opentelemetry.io/docs/specs/otel/metrics/semantic_conventions/http-metrics/)
+OpenTelemetry's [Semantic Conventions for HTTP Metrics](https://opentelemetry.io/docs/specs/otel/metrics/semantic_conventions/http-metrics/)
say that if you instrument an HTTP server with OpenTelemetry, you must have a histogram named
`http.server.duration`.
-Most names defined in semantic conventions use dots. In the Prometheus server, the dot is an illegal
-character (this might change in future versions of the Prometheus server).
-
-The Prometheus Java client library allows dots, so that you can use metric names and label names as
-defined in OpenTelemetry's semantic conventions.
-The dots will automatically be replaced with underscores if you expose metrics in Prometheus format,
-but you will see the original names with dots if you push your metrics in OpenTelemetry format.
+Most names defined in semantic conventions use dots.
+Dots in metric and label names are now supported in the Prometheus Java client library as
+described in [Unicode support].
-That way, you can use OTel-compliant metric and label names today when instrumenting your
-application with the Prometheus Java client, and you are prepared in case your monitoring backend
-adds features in the future that require OTel-compliant instrumentation.
+[Unicode support]: {{< relref "../exporters/unicode.md" >}}
+[otel-pipeline]: /client_java/images/otel-pipeline.png
diff --git a/docs/content/otel/otlp.md b/docs/content/otel/otlp.md
index 568219dd0..e2ea987dd 100644
--- a/docs/content/otel/otlp.md
+++ b/docs/content/otel/otlp.md
@@ -3,10 +3,10 @@ title: OTLP
weight: 1
---
-The Prometheus Java client library allows you to push metrics to an OpenTelemetry endpoint using the
-OTLP protocol.
+The Prometheus Java client library allows you to push metrics to an
+OpenTelemetry endpoint using the OTLP protocol.
-
+![Image of a with the Prometheus client library pushing metrics to an OpenTelemetry collector][otel-pipeline]
To implement this, you need to include `prometheus-metrics-exporter` as a dependency
@@ -39,23 +39,31 @@ OpenTelemetryExporter.builder()
.buildAndStart();
```
-By default, the `OpenTelemetryExporter` will push metrics every 60 seconds to `localhost:4317` using
-`grpc` protocol. You can configure this in code using
-the [OpenTelemetryExporter.Builder](/client_java/api/io/prometheus/metrics/exporter/opentelemetry/OpenTelemetryExporter.Builder.html),
-or at runtime via [`io.prometheus.exporter.opentelemetry.*`]({{< relref "../config/config.md#exporter-opentelemetry-properties" >}})
-properties.
+By default, the `OpenTelemetryExporter` will push metrics every 60 seconds to
+`localhost:4317` using `grpc` protocol. You can configure this in code using
+the [OpenTelemetryExporter.Builder][builder-javadoc], or at runtime via
+[`io.prometheus.exporter.opentelemetry.*`][otel-properties] properties.
+
+The OpenTelemetry exporter also honors the shared [`io.prometheus.exporter.filter.*`][exporter-filter-properties] metric-name
+filter properties.
In addition to the Prometheus Java client configuration, the exporter also recognizes standard
OpenTelemetry configuration. For example, you can set
-the [OTEL_EXPORTER_OTLP_METRICS_ENDPOINT](https://opentelemetry.io/docs/concepts/sdk-configuration/otlp-exporter-configuration/#otel_exporter_otlp_metrics_endpoint)
+the [OTEL_EXPORTER_OTLP_METRICS_ENDPOINT](https://opentelemetry.io/docs/concepts/sdk-configuration/otlp-exporter-configuration/#otel_exporter_otlp_metrics_endpoint)
environment variable to configure the endpoint. The Javadoc
-for [OpenTelemetryExporter.Builder](/client_java/api/io/prometheus/metrics/exporter/opentelemetry/OpenTelemetryExporter.Builder.html)
+for [OpenTelemetryExporter.Builder](/client_java/api/io/prometheus/metrics/exporter/opentelemetry/OpenTelemetryExporter.Builder.html)
shows which settings have corresponding OTel configuration. The intended use case is that if you
attach the
[OpenTelemetry Java agent](https://github.com/open-telemetry/opentelemetry-java-instrumentation/)
for tracing, and use the Prometheus Java client for metrics, it is sufficient to configure the OTel
agent because the Prometheus library will pick up the same configuration.
-The [examples/example-exporter-opentelemetry](https://github.com/prometheus/client_java/tree/main/examples/example-exporter-opentelemetry)
-folder has a Docker compose with a complete end-to-end example, including a Java app, the OTel
-collector, and a Prometheus server.
+The [examples/example-exporter-opentelemetry][opentelemetry-example] folder has
+a Docker compose with a complete end-to-end example, including a Java app, the
+OTel collector, and a Prometheus server.
+
+[builder-javadoc]: /client_java/api/io/prometheus/metrics/exporter/opentelemetry/OpenTelemetryExporter.Builder.html
+[opentelemetry-example]: https://github.com/prometheus/client_java/tree/main/examples/example-exporter-opentelemetry
+[otel-pipeline]: /client_java/images/otel-pipeline.png
+[exporter-filter-properties]: {{< relref "../config/config.md#exporter-filter-properties" >}}
+[otel-properties]: {{< relref "../config/config.md#exporter-opentelemetry-properties" >}}
diff --git a/docs/content/otel/support.md b/docs/content/otel/support.md
new file mode 100644
index 000000000..e3b8cbe3a
--- /dev/null
+++ b/docs/content/otel/support.md
@@ -0,0 +1,47 @@
+---
+title: OTel Support
+weight: 2
+---
+
+The `prometheus-metrics-otel-support` module bundles the
+OpenTelemetry SDK and the Prometheus exporter into a single
+POM dependency.
+
+Use this module when you want to combine OpenTelemetry
+instrumentations (e.g. JVM runtime metrics) with the
+Prometheus Java client on one `/metrics` endpoint.
+
+## Dependencies
+
+{{< tabs "otel-support-deps" >}}
+{{< tab "Gradle" >}}
+
+```groovy
+implementation 'io.prometheus:prometheus-metrics-otel-support:$version'
+```
+
+{{< /tab >}}
+{{< tab "Maven" >}}
+
+```xml
+
+ io.prometheus
+ prometheus-metrics-otel-support
+ $version
+ pom
+
+```
+
+{{< /tab >}}
+{{< /tabs >}}
+
+This single dependency replaces:
+
+- `io.opentelemetry:opentelemetry-sdk`
+- `io.opentelemetry:opentelemetry-exporter-prometheus`
+
+## Use Cases
+
+See [JVM Runtime Metrics]({{< relref "jvm-runtime-metrics.md" >}})
+for a concrete example of combining OTel JVM metrics with
+the Prometheus Java client.
diff --git a/docs/content/otel/tracing.md b/docs/content/otel/tracing.md
index 0e3fb72fa..9d598c6f4 100644
--- a/docs/content/otel/tracing.md
+++ b/docs/content/otel/tracing.md
@@ -6,7 +6,7 @@ weight: 2
OpenTelemetryโs
[vision statement](https://github.com/open-telemetry/community/blob/main/mission-vision-values.md)
says that
-[telemetry should be loosely coupled](https://github.com/open-telemetry/community/blob/main/mission-vision-values.md#telemetry-should-be-loosely-coupled),
+[telemetry should be loosely coupled](https://github.com/open-telemetry/community/blob/main/mission-vision-values.md#telemetry-should-be-loosely-coupled),
allowing end users to pick and choose from the pieces they want without having to bring in the rest
of the project, too. In that spirit, you might choose to instrument your Java application with the
Prometheus Java client library for metrics, and attach the
@@ -56,7 +56,7 @@ Exemplars are only selected every
[`minRetentionPeriodSeconds`]({{< relref "../config/config.md#exemplar-properties" >}}) seconds.
Here's an example of how to configure OpenTelemetry's
-[tail sampling processor](https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/main/processor/tailsamplingprocessor/)
+[tail sampling processor](https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/processor/tailsamplingprocessor/)
to sample all Spans marked with `exemplar="true"`, and then discard 90% of the traces:
```yaml
@@ -71,7 +71,47 @@ policies:
]
```
-The [examples/example-exemplar-tail-sampling/](https://github.com/prometheus/client_java/tree/main/examples/example-exemplars-tail-sampling)
+The [examples/example-exemplar-tail-sampling/](https://github.com/prometheus/client_java/tree/main/examples/example-exemplars-tail-sampling)
directory has a complete end-to-end example, with a distributed Java application with two services,
an OpenTelemetry collector, Prometheus, Tempo as a trace database, and Grafana dashboards. Use
docker-compose as described in the example's readme to run the example and explore the results.
+
+## Adding custom labels to exemplars
+
+Automatically-sampled exemplars carry the `trace_id` and `span_id` labels. You can attach
+additional, custom labels (for example an internal identifier) to every automatically-sampled
+exemplar. There are two options.
+
+### Global (all metrics)
+
+Register a global supplier to add custom labels to the exemplars of _all_ metrics, including
+metrics registered by third-party libraries that you do not control. This is the right option when
+you cannot modify the code that creates the metric:
+
+```java
+ExemplarLabelsSupplier.setExemplarLabelsSupplier(
+ () -> Labels.of("management_id", currentManagementId()));
+```
+
+### Per metric
+
+If you only want the extra labels on a specific metric you define yourself, use the builder:
+
+```java
+Counter counter =
+ Counter.builder()
+ .name("requests_total")
+ .exemplarLabelsSupplier(() -> Labels.of("management_id", currentManagementId()))
+ .build();
+```
+
+### Notes
+
+- The supplier is invoked on the (rate-limited) hot path each time an exemplar is sampled, so it
+ should be cheap. It may return dynamic, request-scoped values (e.g. read from a thread-local).
+- Custom labels are only added when a valid, sampled span context is present; the supplier never
+ causes an exemplar to be created on its own.
+- Precedence on a label-name collision: the reserved `trace_id`/`span_id` labels always win, then
+ the per-metric supplier, then the global supplier. Colliding labels are silently dropped.
+- If the supplier throws, the exception is swallowed and the exemplar is created without the
+ additional labels, so a misbehaving supplier never breaks metric collection.
diff --git a/docs/hugo.toml b/docs/hugo.toml
index b558774ec..9223f49ef 100644
--- a/docs/hugo.toml
+++ b/docs/hugo.toml
@@ -17,18 +17,18 @@ enableRobotsTXT = true
# Needed for mermaid shortcodes
[markup]
- [markup.goldmark.renderer]
- # Needed for mermaid shortcode
- unsafe = true
- [markup.tableOfContents]
- startLevel = 1
- endLevel = 9
- [markup.highlight]
- style = 'solarized-dark'
+[markup.goldmark.renderer]
+# Needed for mermaid shortcode
+unsafe = true
+[markup.tableOfContents]
+startLevel = 1
+endLevel = 9
+[markup.highlight]
+style = 'solarized-dark'
[taxonomies]
- tag = "tags"
+tag = "tags"
[caches]
- [caches.images]
- dir = ':cacheDir/images'
+[caches.images]
+dir = ':cacheDir/images'
diff --git a/docs/themes/hugo-geekdoc/layouts/partials/microformats/schema.html b/docs/themes/hugo-geekdoc/layouts/partials/microformats/schema.html
index e4a71eb4e..7e49ef8c7 100644
--- a/docs/themes/hugo-geekdoc/layouts/partials/microformats/schema.html
+++ b/docs/themes/hugo-geekdoc/layouts/partials/microformats/schema.html
@@ -1,70 +1,58 @@
{{ $isPage := or (and (ne .Type "posts") (in "section page" .Kind )) (and (eq .Type "posts") (eq .Kind "page")) }}
{{- if eq .Kind "home" }}
+ {{- $schema := dict "@context" "http://schema.org" "@type" "WebSite" "name" .Site.Title "url" .Site.BaseURL "inLanguage" .Lang }}
+ {{- with partial "utils/description" . }}
+ {{- $schema = merge $schema (dict "description" (. | plainify | htmlUnescape | chomp)) }}
+ {{- end }}
+ {{- with partial "utils/featured" . }}
+ {{- $schema = merge $schema (dict "thumbnailUrl" .) }}
+ {{- end }}
+ {{- with .Site.Params.geekdocContentLicense }}
+ {{- $schema = merge $schema (dict "license" .name) }}
+ {{- end }}
{{- else if $isPage }}
+ {{- $title := partial "utils/title" . }}
+ {{- $schema := dict
+ "@context" "http://schema.org"
+ "@type" "TechArticle"
+ "articleSection" (.Section | humanize | title)
+ "name" $title
+ "url" .Permalink
+ "headline" $title
+ "wordCount" (string .WordCount)
+ "inLanguage" .Lang
+ "isFamilyFriendly" "true"
+ "copyrightHolder" .Site.Title
+ "copyrightYear" (.Date.Format "2006")
+ "dateCreated" (.Date.Format "2006-01-02T15:04:05.00Z")
+ "datePublished" (.PublishDate.Format "2006-01-02T15:04:05.00Z")
+ "dateModified" (.Lastmod.Format "2006-01-02T15:04:05.00Z")
+ }}
+ {{- with .Params.lead }}
+ {{- $schema = merge $schema (dict "alternativeHeadline" .) }}
+ {{- end }}
+ {{- with partial "utils/description" . }}
+ {{- $schema = merge $schema (dict "description" (. | plainify | htmlUnescape | chomp)) }}
+ {{- end }}
+ {{- with partial "utils/featured" . }}
+ {{- $schema = merge $schema (dict "thumbnailUrl" .) }}
+ {{- end }}
+ {{- with .Site.Params.geekdocContentLicense }}
+ {{- $schema = merge $schema (dict "license" .name) }}
+ {{- end }}
+ {{- $mainEntity := dict "@type" "WebPage" "@id" .Permalink }}
+ {{- $schema = merge $schema (dict "mainEntityOfPage" $mainEntity) }}
+ {{- with $tags := .Params.tags }}
+ {{- $schema = merge $schema (dict "keywords" $tags) }}
+ {{- end }}
+ {{- $logoUrl := default "brand.svg" .Site.Params.logo | absURL }}
+ {{- $logo := dict "@type" "ImageObject" "url" $logoUrl "width" "32" "height" "32" }}
+ {{- $publisher := dict "@type" "Organization" "name" .Site.Title "url" .Site.BaseURL "logo" $logo }}
+ {{- $schema = merge $schema (dict "publisher" $publisher) }}
{{- end }}
diff --git a/examples/example-custom-buckets/README.md b/examples/example-custom-buckets/README.md
new file mode 100644
index 000000000..a1d2c10bb
--- /dev/null
+++ b/examples/example-custom-buckets/README.md
@@ -0,0 +1,167 @@
+# Native Histograms with Custom Buckets (NHCB) Example
+
+This example demonstrates how to use native histograms with custom bucket boundaries (NHCB) in
+Prometheus Java client. It shows three different types of custom bucket configurations and how
+Prometheus converts them to native histograms with schema -53.
+
+## What are Native Histograms with Custom Buckets?
+
+Native Histograms with Custom Buckets (NHCB) is a Prometheus feature that combines the benefits of:
+
+- **Custom bucket boundaries**: Precisely defined buckets optimized for your specific use case
+- **Native histograms**: Efficient storage and querying capabilities of native histograms
+
+When you configure Prometheus with `convert_classic_histograms_to_nhcb: true`, it converts classic
+histograms with custom buckets into native histograms using schema -53, preserving the custom
+bucket boundaries.
+
+## Example Metrics
+
+This example application generates three different histogram metrics demonstrating different
+bucket configuration strategies:
+
+### 1. API Latency - Arbitrary Custom Boundaries
+
+```java
+Histogram apiLatency = Histogram.builder()
+ .name("api_request_duration_seconds")
+ .classicUpperBounds(0.01, 0.05, 0.1, 0.5, 1.0, 5.0, 10.0)
+ .register();
+```
+
+**Use case**: Optimized for typical API response times in seconds.
+
+### 2. Queue Size - Linear Boundaries
+
+```java
+Histogram queueSize = Histogram.builder()
+ .name("message_queue_size")
+ .classicLinearUpperBounds(10, 10, 10) // 10, 20, 30, ..., 100
+ .register();
+```
+
+**Use case**: Equal-width buckets for monitoring queue depth or other discrete values.
+
+### 3. Response Size - Exponential Boundaries
+
+```java
+Histogram responseSize = Histogram.builder()
+ .name("http_response_size_bytes")
+ .classicExponentialUpperBounds(100, 10, 6) // 100, 1k, 10k, 100k, 1M, 10M
+ .register();
+```
+
+**Use case**: Data spanning multiple orders of magnitude (bytes, milliseconds, etc).
+
+## Build
+
+This example is built as part of the `client_java` project:
+
+```shell
+./mvnw package
+```
+
+This creates `./examples/example-custom-buckets/target/example-custom-buckets.jar`.
+
+## Run
+
+With the JAR file present, run:
+
+```shell
+cd ./examples/example-custom-buckets/
+docker-compose up
+```
+
+This starts three Docker containers:
+
+- **[http://localhost:9400/metrics](http://localhost:9400/metrics)** - Example application
+- **[http://localhost:9090](http://localhost:9090)** - Prometheus server (with NHCB enabled)
+- **[http://localhost:3000](http://localhost:3000)** - Grafana (user: _admin_, password: _admin_)
+
+You might need to replace `localhost` with `host.docker.internal` on macOS or Windows.
+
+## Verify NHCB Conversion
+
+### 1. Check Prometheus Configuration
+
+The Prometheus configuration enables NHCB conversion:
+
+```yaml
+scrape_configs:
+ - job_name: "custom-buckets-demo"
+ scrape_protocols: ["PrometheusProto"]
+ convert_classic_histograms_to_nhcb: true
+ scrape_classic_histograms: true
+```
+
+### 2. Verify in Prometheus
+
+Visit [http://localhost:9090](http://localhost:9090) and run queries:
+
+```promql
+# View histogram metadata (should show schema -53 for NHCB)
+prometheus_tsdb_head_series
+
+# Calculate quantiles from custom buckets
+histogram_quantile(0.95, rate(api_request_duration_seconds[1m]))
+
+# View raw histogram structure
+api_request_duration_seconds
+```
+
+### 3. View in Grafana
+
+The Grafana dashboard at [http://localhost:3000](http://localhost:3000) shows:
+
+- p95 and p50 latencies for API endpoints (arbitrary custom buckets)
+- Queue size distribution (linear buckets)
+- Response size distribution (exponential buckets)
+
+## Key Observations
+
+1. **Custom Buckets Preserved**: The custom bucket boundaries you define are preserved when
+ converted to NHCB (schema -53).
+
+2. **Dual Representation**: By default, histograms maintain both classic and native
+ representations, allowing gradual migration.
+
+3. **Efficient Storage**: Native histograms provide more efficient storage than classic histograms
+ while preserving your custom bucket boundaries.
+
+4. **Flexible Bucket Strategies**: You can choose arbitrary, linear, or exponential buckets based
+ on your specific monitoring needs.
+
+## When to Use Custom Buckets
+
+Consider using custom buckets (and NHCB) when:
+
+- **Precise boundaries needed**: You know the expected distribution and want specific bucket edges
+- **Migrating from classic histograms**: You want to preserve existing bucket boundaries
+- **Specific use cases**: Default exponential bucketing doesn't fit your distribution well
+ - Temperature ranges (might include negative values)
+ - Queue depths (discrete values with linear growth)
+ - File sizes (exponential growth but with specific thresholds)
+ - API latencies (specific SLA boundaries)
+
+## Differences from Standard Native Histograms
+
+| Feature | Standard Native Histograms | NHCB (Schema -53) |
+| ----------------- | ------------------------------- | --------------------------------- |
+| Bucket boundaries | Exponential (base 2^(2^-scale)) | Custom boundaries |
+| Use case | General-purpose | Specific distributions |
+| Mergeability | Can merge with same schema | Cannot merge different boundaries |
+| Configuration | Schema level (0-8) | Explicit boundary list |
+
+## Cleanup
+
+Stop the containers:
+
+```shell
+docker-compose down
+```
+
+## Further Reading
+
+- [Prometheus Native Histograms Specification](https://prometheus.io/docs/specs/native_histograms/)
+- [Prometheus Java Client Documentation](https://prometheus.github.io/client_java/)
+- [OpenTelemetry Exponential Histograms](https://opentelemetry.io/docs/specs/otel/metrics/data-model/#exponentialhistogram)
diff --git a/examples/example-custom-buckets/docker-compose.yaml b/examples/example-custom-buckets/docker-compose.yaml
new file mode 100644
index 000000000..6712c8533
--- /dev/null
+++ b/examples/example-custom-buckets/docker-compose.yaml
@@ -0,0 +1,26 @@
+version: "3"
+services:
+ example-application:
+ image: eclipse-temurin:25.0.3_9-jre@sha256:7c1c6297dc3a3ff947922f3ab14ecd326e29083b9edaa8dbff3b94fef1688311
+ network_mode: host
+ volumes:
+ - ./target/example-custom-buckets.jar:/example-custom-buckets.jar
+ command:
+ - /opt/java/openjdk/bin/java
+ - -jar
+ - /example-custom-buckets.jar
+ prometheus:
+ image: prom/prometheus:v3.14.0@sha256:5ce7540c3c00ef4ab0c9d2c995c6a5b9c421f44b4a115d97a2c7af3b1c21cbb0
+ network_mode: host
+ volumes:
+ - ./docker-compose/prometheus.yml:/prometheus.yml
+ command:
+ - --enable-feature=native-histograms
+ - --config.file=/prometheus.yml
+ grafana:
+ image: grafana/grafana:13.2.0@sha256:3fd54ae1214669f8355f065ec9f6445d5279a3d77095ab048ca045685272429b
+ network_mode: host
+ volumes:
+ - ./docker-compose/grafana-datasources.yaml:/etc/grafana/provisioning/datasources/grafana-datasources.yaml
+ - ./docker-compose/grafana-dashboards.yaml:/etc/grafana/provisioning/dashboards/grafana-dashboards.yaml
+ - ./docker-compose/grafana-dashboard-custom-buckets.json:/etc/grafana/grafana-dashboard-custom-buckets.json
diff --git a/examples/example-custom-buckets/docker-compose/grafana-dashboard-custom-buckets.json b/examples/example-custom-buckets/docker-compose/grafana-dashboard-custom-buckets.json
new file mode 100644
index 000000000..11ae25775
--- /dev/null
+++ b/examples/example-custom-buckets/docker-compose/grafana-dashboard-custom-buckets.json
@@ -0,0 +1,349 @@
+{
+ "annotations": {
+ "list": []
+ },
+ "editable": true,
+ "fiscalYearStartMonth": 0,
+ "graphTooltip": 0,
+ "id": null,
+ "links": [],
+ "panels": [
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "prometheus"
+ },
+ "description": "API request duration with custom bucket boundaries (0.01, 0.05, 0.1, 0.5, 1.0, 5.0, 10.0 seconds). Shows how custom buckets are preserved in NHCB (schema -53).",
+ "fieldConfig": {
+ "defaults": {
+ "color": {
+ "mode": "palette-classic"
+ },
+ "custom": {
+ "axisBorderShow": false,
+ "axisCenteredZero": false,
+ "axisColorMode": "text",
+ "axisLabel": "",
+ "axisPlacement": "auto",
+ "barAlignment": 0,
+ "barWidthFactor": 0.6,
+ "drawStyle": "line",
+ "fillOpacity": 0,
+ "gradientMode": "none",
+ "hideFrom": {
+ "tooltip": false,
+ "viz": false,
+ "legend": false
+ },
+ "insertNulls": false,
+ "lineInterpolation": "linear",
+ "lineWidth": 1,
+ "pointSize": 5,
+ "scaleDistribution": {
+ "type": "linear"
+ },
+ "showPoints": "auto",
+ "spanNulls": false,
+ "stacking": {
+ "group": "A",
+ "mode": "none"
+ },
+ "thresholdsStyle": {
+ "mode": "off"
+ }
+ },
+ "mappings": [],
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "green",
+ "value": null
+ }
+ ]
+ },
+ "unit": "s"
+ },
+ "overrides": []
+ },
+ "gridPos": {
+ "h": 8,
+ "w": 24,
+ "x": 0,
+ "y": 0
+ },
+ "id": 1,
+ "options": {
+ "legend": {
+ "calcs": ["mean", "max"],
+ "displayMode": "table",
+ "placement": "right",
+ "showLegend": true
+ },
+ "tooltip": {
+ "mode": "single",
+ "sort": "none"
+ }
+ },
+ "targets": [
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "prometheus"
+ },
+ "editorMode": "code",
+ "expr": "histogram_quantile(0.95, rate(api_request_duration_seconds[1m]))",
+ "instant": false,
+ "legendFormat": "{{endpoint}} {{status}} (p95)",
+ "range": true,
+ "refId": "A"
+ },
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "prometheus"
+ },
+ "editorMode": "code",
+ "expr": "histogram_quantile(0.5, rate(api_request_duration_seconds[1m]))",
+ "hide": false,
+ "instant": false,
+ "legendFormat": "{{endpoint}} {{status}} (p50)",
+ "range": true,
+ "refId": "B"
+ }
+ ],
+ "title": "API Latency - Custom Buckets (Arbitrary Boundaries)",
+ "type": "timeseries"
+ },
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "prometheus"
+ },
+ "description": "Queue size with linear bucket boundaries (10, 20, 30, ..., 100). Demonstrates equal-width buckets for discrete values.",
+ "fieldConfig": {
+ "defaults": {
+ "color": {
+ "mode": "palette-classic"
+ },
+ "custom": {
+ "axisBorderShow": false,
+ "axisCenteredZero": false,
+ "axisColorMode": "text",
+ "axisLabel": "",
+ "axisPlacement": "auto",
+ "barAlignment": 0,
+ "barWidthFactor": 0.6,
+ "drawStyle": "line",
+ "fillOpacity": 0,
+ "gradientMode": "none",
+ "hideFrom": {
+ "tooltip": false,
+ "viz": false,
+ "legend": false
+ },
+ "insertNulls": false,
+ "lineInterpolation": "linear",
+ "lineWidth": 1,
+ "pointSize": 5,
+ "scaleDistribution": {
+ "type": "linear"
+ },
+ "showPoints": "auto",
+ "spanNulls": false,
+ "stacking": {
+ "group": "A",
+ "mode": "none"
+ },
+ "thresholdsStyle": {
+ "mode": "off"
+ }
+ },
+ "mappings": [],
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "green",
+ "value": null
+ }
+ ]
+ },
+ "unit": "short"
+ },
+ "overrides": []
+ },
+ "gridPos": {
+ "h": 8,
+ "w": 12,
+ "x": 0,
+ "y": 8
+ },
+ "id": 2,
+ "options": {
+ "legend": {
+ "calcs": ["mean", "max"],
+ "displayMode": "table",
+ "placement": "bottom",
+ "showLegend": true
+ },
+ "tooltip": {
+ "mode": "single",
+ "sort": "none"
+ }
+ },
+ "targets": [
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "prometheus"
+ },
+ "editorMode": "code",
+ "expr": "histogram_quantile(0.95, rate(message_queue_size[1m]))",
+ "instant": false,
+ "legendFormat": "{{queue_name}} (p95)",
+ "range": true,
+ "refId": "A"
+ },
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "prometheus"
+ },
+ "editorMode": "code",
+ "expr": "histogram_quantile(0.5, rate(message_queue_size[1m]))",
+ "hide": false,
+ "instant": false,
+ "legendFormat": "{{queue_name}} (p50)",
+ "range": true,
+ "refId": "B"
+ }
+ ],
+ "title": "Queue Size - Linear Buckets",
+ "type": "timeseries"
+ },
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "prometheus"
+ },
+ "description": "HTTP response size with exponential bucket boundaries (100, 1k, 10k, 100k, 1M, 10M bytes). Shows exponential growth for data spanning multiple orders of magnitude.",
+ "fieldConfig": {
+ "defaults": {
+ "color": {
+ "mode": "palette-classic"
+ },
+ "custom": {
+ "axisBorderShow": false,
+ "axisCenteredZero": false,
+ "axisColorMode": "text",
+ "axisLabel": "",
+ "axisPlacement": "auto",
+ "barAlignment": 0,
+ "barWidthFactor": 0.6,
+ "drawStyle": "line",
+ "fillOpacity": 0,
+ "gradientMode": "none",
+ "hideFrom": {
+ "tooltip": false,
+ "viz": false,
+ "legend": false
+ },
+ "insertNulls": false,
+ "lineInterpolation": "linear",
+ "lineWidth": 1,
+ "pointSize": 5,
+ "scaleDistribution": {
+ "type": "linear"
+ },
+ "showPoints": "auto",
+ "spanNulls": false,
+ "stacking": {
+ "group": "A",
+ "mode": "none"
+ },
+ "thresholdsStyle": {
+ "mode": "off"
+ }
+ },
+ "mappings": [],
+ "thresholds": {
+ "mode": "absolute",
+ "steps": [
+ {
+ "color": "green",
+ "value": null
+ }
+ ]
+ },
+ "unit": "bytes"
+ },
+ "overrides": []
+ },
+ "gridPos": {
+ "h": 8,
+ "w": 12,
+ "x": 12,
+ "y": 8
+ },
+ "id": 3,
+ "options": {
+ "legend": {
+ "calcs": ["mean", "max"],
+ "displayMode": "table",
+ "placement": "bottom",
+ "showLegend": true
+ },
+ "tooltip": {
+ "mode": "single",
+ "sort": "none"
+ }
+ },
+ "targets": [
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "prometheus"
+ },
+ "editorMode": "code",
+ "expr": "histogram_quantile(0.95, rate(http_response_size_bytes[1m]))",
+ "instant": false,
+ "legendFormat": "{{endpoint}} (p95)",
+ "range": true,
+ "refId": "A"
+ },
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "prometheus"
+ },
+ "editorMode": "code",
+ "expr": "histogram_quantile(0.5, rate(http_response_size_bytes[1m]))",
+ "hide": false,
+ "instant": false,
+ "legendFormat": "{{endpoint}} (p50)",
+ "range": true,
+ "refId": "B"
+ }
+ ],
+ "title": "Response Size - Exponential Buckets",
+ "type": "timeseries"
+ }
+ ],
+ "refresh": "5s",
+ "schemaVersion": 39,
+ "tags": ["custom-buckets", "nhcb", "native-histogram"],
+ "templating": {
+ "list": []
+ },
+ "time": {
+ "from": "now-5m",
+ "to": "now"
+ },
+ "timepicker": {},
+ "timezone": "browser",
+ "title": "Native Histograms with Custom Buckets (NHCB)",
+ "uid": "custom-buckets-nhcb",
+ "version": 1,
+ "weekStart": ""
+}
diff --git a/examples/example-custom-buckets/docker-compose/grafana-dashboards.yaml b/examples/example-custom-buckets/docker-compose/grafana-dashboards.yaml
new file mode 100644
index 000000000..3225b88ae
--- /dev/null
+++ b/examples/example-custom-buckets/docker-compose/grafana-dashboards.yaml
@@ -0,0 +1,8 @@
+apiVersion: 1
+
+providers:
+ - name: "Custom Buckets (NHCB) Example"
+ type: file
+ options:
+ path: /etc/grafana/grafana-dashboard-custom-buckets.json
+ foldersFromFilesStructure: false
diff --git a/examples/example-custom-buckets/docker-compose/grafana-datasources.yaml b/examples/example-custom-buckets/docker-compose/grafana-datasources.yaml
new file mode 100644
index 000000000..d442d28d2
--- /dev/null
+++ b/examples/example-custom-buckets/docker-compose/grafana-datasources.yaml
@@ -0,0 +1,7 @@
+apiVersion: 1
+
+datasources:
+ - name: Prometheus
+ type: prometheus
+ uid: prometheus
+ url: http://localhost:9090
diff --git a/examples/example-custom-buckets/docker-compose/prometheus.yml b/examples/example-custom-buckets/docker-compose/prometheus.yml
new file mode 100644
index 000000000..5c5782023
--- /dev/null
+++ b/examples/example-custom-buckets/docker-compose/prometheus.yml
@@ -0,0 +1,14 @@
+---
+global:
+ scrape_interval: 5s # very short interval for demo purposes
+
+scrape_configs:
+ - job_name: "custom-buckets-demo"
+ # Use protobuf format to receive native histogram data
+ scrape_protocols: ["PrometheusProto"]
+ # Convert classic histograms with custom buckets to NHCB (schema -53)
+ convert_classic_histograms_to_nhcb: true
+ # Also scrape classic histograms for comparison
+ scrape_classic_histograms: true
+ static_configs:
+ - targets: ["localhost:9400"]
diff --git a/examples/example-custom-buckets/pom.xml b/examples/example-custom-buckets/pom.xml
new file mode 100644
index 000000000..2436e179f
--- /dev/null
+++ b/examples/example-custom-buckets/pom.xml
@@ -0,0 +1,70 @@
+
+
+ 4.0.0
+
+ io.prometheus
+ example-custom-buckets
+ 1.8.1-SNAPSHOT
+
+
+ 8
+ UTF-8
+
+
+ Example - Custom Buckets
+
+ End-to-End example of Native Histograms with Custom Buckets (NHCB): Java app -> Prometheus -> Grafana
+
+
+
+
+
+ io.prometheus
+ prometheus-metrics-bom
+ 1.5.1
+ pom
+ import
+
+
+
+
+
+
+ io.prometheus
+ prometheus-metrics-core
+
+
+ io.prometheus
+ prometheus-metrics-instrumentation-jvm
+
+
+ io.prometheus
+ prometheus-metrics-exporter-httpserver
+
+
+
+
+ ${project.artifactId}
+
+
+ org.apache.maven.plugins
+ maven-shade-plugin
+
+
+ package
+
+ shade
+
+
+
+
+ io.prometheus.metrics.examples.custombuckets.Main
+
+
+
+
+
+
+
+
+
diff --git a/examples/example-custom-buckets/src/main/java/io/prometheus/metrics/examples/custombuckets/Main.java b/examples/example-custom-buckets/src/main/java/io/prometheus/metrics/examples/custombuckets/Main.java
new file mode 100644
index 000000000..3d286fdf0
--- /dev/null
+++ b/examples/example-custom-buckets/src/main/java/io/prometheus/metrics/examples/custombuckets/Main.java
@@ -0,0 +1,108 @@
+package io.prometheus.metrics.examples.custombuckets;
+
+import io.prometheus.metrics.core.metrics.Histogram;
+import io.prometheus.metrics.exporter.httpserver.HTTPServer;
+import io.prometheus.metrics.instrumentation.jvm.JvmMetrics;
+import io.prometheus.metrics.model.snapshots.Unit;
+import java.io.IOException;
+import java.util.Random;
+
+/**
+ * Example demonstrating native histograms with custom buckets (NHCB).
+ *
+ * This example shows three different types of custom bucket configurations:
+ *
+ *
+ * API latency with arbitrary custom boundaries optimized for typical response times
+ * Queue size with linear boundaries for equal-width buckets
+ * Response size with exponential boundaries for data spanning multiple orders of magnitude
+ *
+ *
+ * These histograms maintain both classic (with custom buckets) and native representations. When
+ * Prometheus is configured with {@code convert_classic_histograms_to_nhcb: true}, the custom bucket
+ * boundaries are preserved in the native histogram format (schema -53).
+ */
+public class Main {
+
+ public static void main(String[] args) throws IOException, InterruptedException {
+
+ JvmMetrics.builder().register();
+
+ // Example 1: API latency with arbitrary custom boundaries
+ // Optimized for typical API response times in seconds
+ Histogram apiLatency =
+ Histogram.builder()
+ .name("api_request_duration_seconds")
+ .help("API request duration with custom buckets")
+ .unit(Unit.SECONDS)
+ .classicUpperBounds(0.01, 0.05, 0.1, 0.5, 1.0, 5.0, 10.0)
+ .labelNames("endpoint", "status")
+ .register();
+
+ // Example 2: Queue size with linear boundaries
+ // Equal-width buckets for monitoring queue depth
+ Histogram queueSize =
+ Histogram.builder()
+ .name("message_queue_size")
+ .help("Number of messages in queue with linear buckets")
+ .classicLinearUpperBounds(10, 10, 10) // 10, 20, 30, ..., 100
+ .labelNames("queue_name")
+ .register();
+
+ // Example 3: Response size with exponential boundaries
+ // Exponential growth for data spanning multiple orders of magnitude
+ Histogram responseSize =
+ Histogram.builder()
+ .name("http_response_size_bytes")
+ .help("HTTP response size in bytes with exponential buckets")
+ .classicExponentialUpperBounds(100, 10, 6) // 100, 1k, 10k, 100k, 1M, 10M
+ .labelNames("endpoint")
+ .register();
+
+ HTTPServer server = HTTPServer.builder().port(9400).buildAndStart();
+
+ System.out.println(
+ "HTTPServer listening on port http://localhost:" + server.getPort() + "/metrics");
+ System.out.println("\nGenerating metrics with custom bucket configurations:");
+ System.out.println("1. API latency: custom boundaries optimized for response times");
+ System.out.println("2. Queue size: linear boundaries (10, 20, 30, ..., 100)");
+ System.out.println("3. Response size: exponential boundaries (100, 1k, 10k, ..., 10M)");
+ System.out.println("\nPrometheus will convert these to NHCB (schema -53) when configured.\n");
+
+ Random random = new Random(0);
+
+ while (true) {
+ // Simulate API latency observations
+ // Fast endpoint: mostly < 100ms, occasionally slow
+ double fastLatency = Math.abs(random.nextGaussian() * 0.03 + 0.05);
+ String status = random.nextInt(100) < 95 ? "200" : "500";
+ apiLatency.labelValues("/api/fast", status).observe(fastLatency);
+
+ // Slow endpoint: typically 1-3 seconds
+ double slowLatency = Math.abs(random.nextGaussian() * 0.5 + 2.0);
+ apiLatency.labelValues("/api/slow", status).observe(slowLatency);
+
+ // Simulate queue size observations
+ // Queue oscillates between 20-80 items
+ int queueDepth = 50 + (int) (random.nextGaussian() * 15);
+ queueDepth = Math.max(0, Math.min(100, queueDepth));
+ queueSize.labelValues("default").observe(queueDepth);
+
+ // Priority queue: usually smaller
+ int priorityQueueDepth = 10 + (int) (random.nextGaussian() * 5);
+ priorityQueueDepth = Math.max(0, Math.min(50, priorityQueueDepth));
+ queueSize.labelValues("priority").observe(priorityQueueDepth);
+
+ // Simulate response size observations
+ // Small responses: mostly < 10KB
+ double smallResponse = Math.abs(random.nextGaussian() * 2000 + 5000);
+ responseSize.labelValues("/api/summary").observe(smallResponse);
+
+ // Large responses: can be up to several MB
+ double largeResponse = Math.abs(random.nextGaussian() * 200000 + 500000);
+ responseSize.labelValues("/api/download").observe(largeResponse);
+
+ Thread.sleep(1000);
+ }
+ }
+}
diff --git a/examples/example-exemplars-tail-sampling/README.md b/examples/example-exemplars-tail-sampling/README.md
index f12c43c95..4b005f745 100644
--- a/examples/example-exemplars-tail-sampling/README.md
+++ b/examples/example-exemplars-tail-sampling/README.md
@@ -9,8 +9,6 @@ Exemplars are often used to reference trace IDs when distributed tracing is used
The following shows an example of a histogram in OpenMetrics text format where each non-empty bucket
has an Exemplar:
-
-
```text
# TYPE request_duration_seconds histogram
# UNIT request_duration_seconds seconds
@@ -31,13 +29,11 @@ request_duration_seconds_count{http_status="200"} 11243
request_duration_seconds_sum{http_status="200"} 2843.3178731140015
```
-
-
In Grafana Exemplars can be visualized as little green dots. The following shows an example of the
95th [quantile](https://prometheus.io/docs/prometheus/latest/querying/functions/#histogram_quantile)
for the histogram above.
-
+
If you move the mouse over an Exemplar, an overlay pops up with a link to a tracing tool
like [Tempo](https://github.com/grafana/tempo).
@@ -125,11 +121,11 @@ password _admin_.
The example dashboard shows 50 requests / second for the Java services:
-
+
The Tempo metrics show that only ~5 traces / second are received:
-
+
The reason is that the OpenTelemetry collector is configured to sample only 10% of the traces. Yet,
all Exemplars in the
diff --git a/examples/example-exemplars-tail-sampling/docker-compose.yaml b/examples/example-exemplars-tail-sampling/docker-compose.yaml
index 0af304ec2..2d5dc0e24 100644
--- a/examples/example-exemplars-tail-sampling/docker-compose.yaml
+++ b/examples/example-exemplars-tail-sampling/docker-compose.yaml
@@ -36,14 +36,14 @@ services:
- -jar
- /example-greeting-service.jar
collector:
- image: otel/opentelemetry-collector-contrib:0.130.1@sha256:9c247564e65ca19f97d891cca19a1a8d291ce631b890885b44e3503c5fdb3895
+ image: otel/opentelemetry-collector-contrib:0.159.0@sha256:1f2c54a30e713fac6b3ae77a1ec84010c2007e29ced8ec666214fc2f6739c1cc
network_mode: host
volumes:
- ./config/otelcol-config.yaml:/config.yaml
command:
- --config=file:/config.yaml
prometheus:
- image: prom/prometheus:v3.5.0@sha256:63805ebb8d2b3920190daf1cb14a60871b16fd38bed42b857a3182bc621f4996
+ image: prom/prometheus:v3.14.0@sha256:5ce7540c3c00ef4ab0c9d2c995c6a5b9c421f44b4a115d97a2c7af3b1c21cbb0
network_mode: host
volumes:
- ./config/prometheus.yaml:/prometheus.yaml
@@ -52,14 +52,14 @@ services:
- --enable-feature=native-histograms
- --config.file=/prometheus.yaml
tempo:
- image: grafana/tempo:2.8.1@sha256:bc9245fe3da4e63dc4c6862d9c2dad9bcd8be13d0ba4f7705fa6acda4c904d0e
+ image: grafana/tempo:3.0.3@sha256:0296560ac66f8a3600d7fb3014a52c189d4d9c3549ad6ff441bf2409855d68d5
network_mode: host
volumes:
- ./config/tempo-config.yaml:/config.yaml
command:
- --config.file=/config.yaml
grafana:
- image: grafana/grafana:12.1.0@sha256:6ac590e7cabc2fbe8d7b8fc1ce9c9f0582177b334e0df9c927ebd9670469440f
+ image: grafana/grafana:13.2.0@sha256:3fd54ae1214669f8355f065ec9f6445d5279a3d77095ab048ca045685272429b
network_mode: host
ports:
- "3000:3000"
@@ -68,7 +68,7 @@ services:
- ./config/grafana-dashboards.yaml:/etc/grafana/provisioning/dashboards/grafana-dashboards.yaml
- ./config/grafana-example-dashboard.json:/etc/grafana/example-dashboard.json
k6:
- image: grafana/k6@sha256:b1625f686ef1c733340b00de57bce840e0b4b1f7e545c58305a5db53e7ad3797
+ image: grafana/k6@sha256:5221b620a4f874faff6e32ba597aa667c058391fe4898b1c6f6377f062c6cdec
network_mode: host
volumes:
- ./config/k6-script.js:/k6-script.js
diff --git a/examples/example-exemplars-tail-sampling/example-greeting-service/pom.xml b/examples/example-exemplars-tail-sampling/example-greeting-service/pom.xml
index d1938f242..4b7d5484b 100644
--- a/examples/example-exemplars-tail-sampling/example-greeting-service/pom.xml
+++ b/examples/example-exemplars-tail-sampling/example-greeting-service/pom.xml
@@ -1,16 +1,15 @@
-
+
4.0.0
-
- io.prometheus
- example-exemplars-tail-sampling
- 1.4.0-SNAPSHOT
-
-
+ io.prometheus
example-greeting-service
+ 1.8.1-SNAPSHOT
+
+
+ 17
+ UTF-8
+
Example - OpenTelemetry Exemplars - Greeting Service
@@ -18,30 +17,35 @@
tracing
-
- 17
-
+
+
+
+ io.prometheus
+ prometheus-metrics-bom
+ 1.5.1
+ pom
+ import
+
+
+
io.prometheus
prometheus-metrics-core
- ${project.version}
io.prometheus
prometheus-metrics-instrumentation-jvm
- ${project.version}
io.prometheus
prometheus-metrics-exporter-servlet-jakarta
- ${project.version}
org.apache.tomcat.embed
tomcat-embed-core
- 11.0.9
+ 11.0.25
@@ -59,8 +63,7 @@
-
+
io.prometheus.metrics.examples.otel.exemplars.greeting.Main
diff --git a/examples/example-exemplars-tail-sampling/example-greeting-service/src/main/java/io/prometheus/metrics/examples/otel/exemplars/greeting/Main.java b/examples/example-exemplars-tail-sampling/example-greeting-service/src/main/java/io/prometheus/metrics/examples/otel/exemplars/greeting/Main.java
index 99ba1084c..ed47d3787 100644
--- a/examples/example-exemplars-tail-sampling/example-greeting-service/src/main/java/io/prometheus/metrics/examples/otel/exemplars/greeting/Main.java
+++ b/examples/example-exemplars-tail-sampling/example-greeting-service/src/main/java/io/prometheus/metrics/examples/otel/exemplars/greeting/Main.java
@@ -20,10 +20,10 @@ public static void main(String[] args) throws LifecycleException {
Context ctx = tomcat.addContext("", new File(".").getAbsolutePath());
Tomcat.addServlet(ctx, "hello", new GreetingServlet());
- ctx.addServletMappingDecoded("/*", "hello");
+ ctx.addServletMapping("/*", "hello");
Tomcat.addServlet(ctx, "metrics", new PrometheusMetricsServlet());
- ctx.addServletMappingDecoded("/metrics", "metrics");
+ ctx.addServletMapping("/metrics", "metrics");
tomcat.getConnector();
tomcat.start();
diff --git a/examples/example-exemplars-tail-sampling/example-hello-world-app/pom.xml b/examples/example-exemplars-tail-sampling/example-hello-world-app/pom.xml
index f20493cff..d1f01b17f 100644
--- a/examples/example-exemplars-tail-sampling/example-hello-world-app/pom.xml
+++ b/examples/example-exemplars-tail-sampling/example-hello-world-app/pom.xml
@@ -1,16 +1,15 @@
-
+
4.0.0
-
- io.prometheus
- example-exemplars-tail-sampling
- 1.4.0-SNAPSHOT
-
-
+ io.prometheus
example-hello-world-app
+ 1.8.1-SNAPSHOT
+
+
+ 17
+ UTF-8
+
Example - OpenTelemetry Exemplars - Hello World App
@@ -18,30 +17,35 @@
tracing
-
- 17
-
+
+
+
+ io.prometheus
+ prometheus-metrics-bom
+ 1.5.1
+ pom
+ import
+
+
+
io.prometheus
prometheus-metrics-core
- ${project.version}
io.prometheus
prometheus-metrics-instrumentation-jvm
- ${project.version}
io.prometheus
prometheus-metrics-exporter-servlet-jakarta
- ${project.version}
org.apache.tomcat.embed
tomcat-embed-core
- 11.0.9
+ 11.0.25
@@ -59,8 +63,7 @@
-
+
io.prometheus.metrics.examples.otel.exemplars.app.Main
diff --git a/examples/example-exemplars-tail-sampling/example-hello-world-app/src/main/java/io/prometheus/metrics/examples/otel/exemplars/app/Main.java b/examples/example-exemplars-tail-sampling/example-hello-world-app/src/main/java/io/prometheus/metrics/examples/otel/exemplars/app/Main.java
index 25ea8a1c6..b3e83cb7d 100644
--- a/examples/example-exemplars-tail-sampling/example-hello-world-app/src/main/java/io/prometheus/metrics/examples/otel/exemplars/app/Main.java
+++ b/examples/example-exemplars-tail-sampling/example-hello-world-app/src/main/java/io/prometheus/metrics/examples/otel/exemplars/app/Main.java
@@ -20,10 +20,10 @@ public static void main(String[] args) throws LifecycleException {
Context ctx = tomcat.addContext("", new File(".").getAbsolutePath());
Tomcat.addServlet(ctx, "hello", new HelloWorldServlet());
- ctx.addServletMappingDecoded("/*", "hello");
+ ctx.addServletMapping("/*", "hello");
Tomcat.addServlet(ctx, "metrics", new PrometheusMetricsServlet());
- ctx.addServletMappingDecoded("/metrics", "metrics");
+ ctx.addServletMapping("/metrics", "metrics");
tomcat.getConnector();
tomcat.start();
diff --git a/examples/example-exemplars-tail-sampling/pom.xml b/examples/example-exemplars-tail-sampling/pom.xml
index 8c0c7441c..24df1f9e5 100644
--- a/examples/example-exemplars-tail-sampling/pom.xml
+++ b/examples/example-exemplars-tail-sampling/pom.xml
@@ -1,27 +1,17 @@
-
+
4.0.0
-
- io.prometheus
- examples
- 1.4.0-SNAPSHOT
-
-
+ io.prometheus
example-exemplars-tail-sampling
+ 1.8.1-SNAPSHOT
pom
Example - Exemplars with OpenTelemetry's Tail Sampling
- Example project showing Examplars with OpenTelemetry's Tail Sampling.
+ Example project showing Exemplars with OpenTelemetry's Tail Sampling.
-
- 11
-
-
example-greeting-service
example-hello-world-app
diff --git a/examples/example-exporter-httpserver/README.md b/examples/example-exporter-httpserver/README.md
index 341f8e2c3..ad7164aa7 100644
--- a/examples/example-exporter-httpserver/README.md
+++ b/examples/example-exporter-httpserver/README.md
@@ -32,7 +32,7 @@ The exporter supports a `debug` URL parameter to quickly view other formats in y
- [http://localhost:9400/metrics?debug=text](http://localhost:9400/metrics?debug=text): Prometheus
text format, same as without the `debug` option.
-- [http://localhost:9400/metrics?debug=openmetrics](http://localhost:9400/metrics?debug=openmetrics):
+- [http://localhost:9400/metrics?debug=openmetrics](http://localhost:9400/metrics?debug=openmetrics):
OpenMetrics text format.
-- [http://localhost:9400/metrics?debug=prometheus-protobuf](http://localhost:9400/metrics?debug=prometheus-protobuf):
+- [http://localhost:9400/metrics?debug=prometheus-protobuf](http://localhost:9400/metrics?debug=prometheus-protobuf):
Text representation of the Prometheus protobuf format.
diff --git a/examples/example-exporter-httpserver/pom.xml b/examples/example-exporter-httpserver/pom.xml
index 6d0b75560..cda7dad6e 100644
--- a/examples/example-exporter-httpserver/pom.xml
+++ b/examples/example-exporter-httpserver/pom.xml
@@ -1,37 +1,45 @@
-
+
4.0.0
-
- io.prometheus
- examples
- 1.4.0-SNAPSHOT
-
-
+ io.prometheus
example-exporter-httpserver
+ 1.8.1-SNAPSHOT
+
+
+ 8
+ UTF-8
+
Example - HTTPServer Exporter
Prometheus Metrics Example using the HTTPServer for exposing the metrics endpoint
+
+
+
+ io.prometheus
+ prometheus-metrics-bom
+ 1.5.1
+ pom
+ import
+
+
+
+
io.prometheus
prometheus-metrics-core
- ${project.version}
io.prometheus
prometheus-metrics-instrumentation-jvm
- ${project.version}
io.prometheus
prometheus-metrics-exporter-httpserver
- ${project.version}
@@ -49,8 +57,7 @@
-
+
io.prometheus.metrics.examples.httpserver.Main
diff --git a/examples/example-exporter-multi-target/README.md b/examples/example-exporter-multi-target/README.md
index 7b20217ac..b655e3244 100644
--- a/examples/example-exporter-multi-target/README.md
+++ b/examples/example-exporter-multi-target/README.md
@@ -34,8 +34,8 @@ The exporter supports a `debug` URL parameter to quickly view other formats in y
- [http://localhost:9400/metrics?debug=text](http://localhost:9400/metrics?debug=text): Prometheus
text format, same as
without the `debug` option.
-- [http://localhost:9400/metrics?debug=openmetrics](http://localhost:9400/metrics?debug=openmetrics):
+- [http://localhost:9400/metrics?debug=openmetrics](http://localhost:9400/metrics?debug=openmetrics):
OpenMetrics text
format.
-- [http://localhost:9400/metrics?debug=prometheus-protobuf](http://localhost:9400/metrics?debug=prometheus-protobuf):
+- [http://localhost:9400/metrics?debug=prometheus-protobuf](http://localhost:9400/metrics?debug=prometheus-protobuf):
Text representation of the Prometheus protobuf format.
diff --git a/examples/example-exporter-multi-target/pom.xml b/examples/example-exporter-multi-target/pom.xml
index 75855a9c1..0457a7591 100644
--- a/examples/example-exporter-multi-target/pom.xml
+++ b/examples/example-exporter-multi-target/pom.xml
@@ -1,37 +1,45 @@
-
+
4.0.0
-
- io.prometheus
- examples
- 1.4.0-SNAPSHOT
-
-
+ io.prometheus
example-exporter-multi-target
+ 1.8.1-SNAPSHOT
+
+
+ 8
+ UTF-8
+
Example - HTTPServer Exporter Multi Target
Prometheus Metrics Example for multi-target pattern implementation
+
+
+
+ io.prometheus
+ prometheus-metrics-bom
+ 1.5.1
+ pom
+ import
+
+
+
+
io.prometheus
prometheus-metrics-core
- ${project.version}
io.prometheus
prometheus-metrics-instrumentation-jvm
- ${project.version}
io.prometheus
prometheus-metrics-exporter-httpserver
- ${project.version}
@@ -49,8 +57,7 @@
-
+
io.prometheus.metrics.examples.multitarget.Main
diff --git a/examples/example-exporter-multi-target/src/main/java/io/prometheus/metrics/examples/multitarget/SampleMultiCollector.java b/examples/example-exporter-multi-target/src/main/java/io/prometheus/metrics/examples/multitarget/SampleMultiCollector.java
index 207c024a5..72e2b28ad 100644
--- a/examples/example-exporter-multi-target/src/main/java/io/prometheus/metrics/examples/multitarget/SampleMultiCollector.java
+++ b/examples/example-exporter-multi-target/src/main/java/io/prometheus/metrics/examples/multitarget/SampleMultiCollector.java
@@ -77,7 +77,12 @@ protected MetricSnapshots collectMetricSnapshots(PrometheusScrapeRequest scrapeR
return new MetricSnapshots(snaps);
}
+ /**
+ * @deprecated Use {@code getMetricFamilyDescriptors()} instead.
+ */
@Override
+ @Deprecated
+ @SuppressWarnings("InlineMeSuggester")
public List getPrometheusNames() {
List names = new ArrayList();
names.add("x_calls_total");
diff --git a/examples/example-exporter-opentelemetry/README.md b/examples/example-exporter-opentelemetry/README.md
index 03dc39e45..5e0551ae5 100644
--- a/examples/example-exporter-opentelemetry/README.md
+++ b/examples/example-exporter-opentelemetry/README.md
@@ -20,14 +20,10 @@ docker-compose up
This will set up the following scenario:
-
-
```mermaid
flowchart LR
A[example app] -->|OTLP|B[OpenTelemetry collector] -->|Prometheus remote write|C[Prometheus server]
```
-
-
The OpenTelemetry collector is configured to log incoming metrics to the console.
The Prometheus server is running on [http://localhost:9090](http://localhost:9090).
diff --git a/examples/example-exporter-opentelemetry/docker-compose.yaml b/examples/example-exporter-opentelemetry/docker-compose.yaml
index ab2473e66..116b57461 100644
--- a/examples/example-exporter-opentelemetry/docker-compose.yaml
+++ b/examples/example-exporter-opentelemetry/docker-compose.yaml
@@ -13,14 +13,14 @@ services:
#- -agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=*:5005
- /example-exporter-opentelemetry.jar
collector:
- image: otel/opentelemetry-collector-contrib:0.130.1@sha256:9c247564e65ca19f97d891cca19a1a8d291ce631b890885b44e3503c5fdb3895
+ image: otel/opentelemetry-collector-contrib:0.159.0@sha256:1f2c54a30e713fac6b3ae77a1ec84010c2007e29ced8ec666214fc2f6739c1cc
network_mode: host
volumes:
- ./config/otelcol-config.yaml:/config.yaml
command:
- --config=file:/config.yaml
prometheus:
- image: prom/prometheus:v3.5.0@sha256:63805ebb8d2b3920190daf1cb14a60871b16fd38bed42b857a3182bc621f4996
+ image: prom/prometheus:v3.14.0@sha256:5ce7540c3c00ef4ab0c9d2c995c6a5b9c421f44b4a115d97a2c7af3b1c21cbb0
network_mode: host
volumes:
- ./config/prometheus.yaml:/prometheus.yaml
diff --git a/examples/example-exporter-opentelemetry/oats-tests/agent/Dockerfile b/examples/example-exporter-opentelemetry/oats-tests/agent/Dockerfile
index 6ea7b6ca9..3f49b8774 100644
--- a/examples/example-exporter-opentelemetry/oats-tests/agent/Dockerfile
+++ b/examples/example-exporter-opentelemetry/oats-tests/agent/Dockerfile
@@ -1,8 +1,8 @@
-FROM eclipse-temurin:21.0.7_6-jre@sha256:bca347dc76e38a60a1a01b29a7d1312e514603a97ba594268e5a2e4a1a0c9a8f
+FROM eclipse-temurin:25.0.3_9-jre@sha256:7c1c6297dc3a3ff947922f3ab14ecd326e29083b9edaa8dbff3b94fef1688311
COPY target/example-exporter-opentelemetry.jar ./app.jar
# check that the resource attributes from the agent are used, epsecially the service.instance.id should be the same
-ADD --chmod=644 https://github.com/open-telemetry/opentelemetry-java-instrumentation/releases/download/v2.8.0/opentelemetry-javaagent.jar /usr/src/app/opentelemetry-javaagent.jar
+ADD --chmod=644 https://github.com/open-telemetry/opentelemetry-java-instrumentation/releases/download/v2.21.0/opentelemetry-javaagent.jar /usr/src/app/opentelemetry-javaagent.jar
ENV JAVA_TOOL_OPTIONS=-javaagent:/usr/src/app/opentelemetry-javaagent.jar
#ENTRYPOINT [ "java", "-Dotel.javaagent.debug=true","-jar", "./app.jar" ] # for debugging
diff --git a/examples/example-exporter-opentelemetry/oats-tests/agent/oats-case.yaml b/examples/example-exporter-opentelemetry/oats-tests/agent/oats-case.yaml
new file mode 100644
index 000000000..a8a05d0f2
--- /dev/null
+++ b/examples/example-exporter-opentelemetry/oats-tests/agent/oats-case.yaml
@@ -0,0 +1,20 @@
+name: java agent exporter preserves target_info identity
+fixture:
+ compose:
+ template: lgtm
+ file: docker-compose.yml
+seed:
+ type: app
+expected:
+ metrics:
+ - promql: "uptime_seconds_total{}"
+ value: ">= 0"
+ - promql: 'count(target_info{service_name!="otelcol-contrib"})'
+ value: "== 2"
+ - promql: 'count(count by (instance) (target_info{service_name!="otelcol-contrib"}))'
+ value: "== 1"
+ custom-checks:
+ - script: |
+ #!/usr/bin/env bash
+ set -euo pipefail
+ curl -fsS "${OATS_GRAFANA_URL:?}/api/health" >/dev/null
diff --git a/examples/example-exporter-opentelemetry/oats-tests/agent/oats.yaml b/examples/example-exporter-opentelemetry/oats-tests/agent/oats.yaml
deleted file mode 100644
index 9c380dea9..000000000
--- a/examples/example-exporter-opentelemetry/oats-tests/agent/oats.yaml
+++ /dev/null
@@ -1,11 +0,0 @@
-# OATS is an acceptance testing framework for OpenTelemetry -
-# https://github.com/grafana/oats/tree/main/yaml
-docker-compose:
- files:
- - ./docker-compose.yml
-expected:
- custom-checks:
- - script: ./service_instance_id_check.py
- metrics:
- - promql: "uptime_seconds_total{}"
- value: ">= 0"
diff --git a/examples/example-exporter-opentelemetry/oats-tests/agent/service_instance_id_check.py b/examples/example-exporter-opentelemetry/oats-tests/agent/service_instance_id_check.py
deleted file mode 100755
index 1cfe2513f..000000000
--- a/examples/example-exporter-opentelemetry/oats-tests/agent/service_instance_id_check.py
+++ /dev/null
@@ -1,48 +0,0 @@
-#!/usr/bin/env python3
-
-"""This script is used to check if the service instance id is present in the exported data
-The script will return 0 if the service instance id is present in the exported data"""
-
-import json
-import urllib.parse
-from urllib.request import urlopen
-
-
-def get(url):
- global response, res
- with urlopen(url) as response:
- # read the response
- res = response.read()
- # decode the response
- res = json.loads(res.decode("utf-8"))
- return res
-
-
-res = get(" http://localhost:9090/api/v1/query?query=target_info")
-
-# uncomment the following line to use the local file instead of the url - for debugging
-# with open('example_target_info.json') as f:
-# res = json.load(f)
-
-values = list(
- {
- r["metric"]["instance"]
- for r in res["data"]["result"]
- if not r["metric"]["service_name"] == "otelcol-contrib"
- }
-)
-print(values)
-
-# both the agent and the exporter should report the same instance id
-assert len(values) == 1
-
-path = f'target_info{{instance="{values[0]}"}}'
-path = urllib.parse.quote_plus(path)
-res = get(f"http://localhost:9090/api/v1/query?query={path}")
-
-infos = res["data"]["result"]
-print(infos)
-
-# they should not have the same target info
-# e.g. only the agent has telemetry_distro_name
-assert len(infos) == 2
diff --git a/examples/example-exporter-opentelemetry/oats-tests/http/Dockerfile b/examples/example-exporter-opentelemetry/oats-tests/http/Dockerfile
index 88947a9d9..d41513edc 100644
--- a/examples/example-exporter-opentelemetry/oats-tests/http/Dockerfile
+++ b/examples/example-exporter-opentelemetry/oats-tests/http/Dockerfile
@@ -1,4 +1,4 @@
-FROM eclipse-temurin:21.0.7_6-jre@sha256:bca347dc76e38a60a1a01b29a7d1312e514603a97ba594268e5a2e4a1a0c9a8f
+FROM eclipse-temurin:25.0.3_9-jre@sha256:7c1c6297dc3a3ff947922f3ab14ecd326e29083b9edaa8dbff3b94fef1688311
COPY target/example-exporter-opentelemetry.jar ./app.jar
diff --git a/examples/example-exporter-opentelemetry/oats-tests/http/oats-case.yaml b/examples/example-exporter-opentelemetry/oats-tests/http/oats-case.yaml
new file mode 100644
index 000000000..fab99dfe3
--- /dev/null
+++ b/examples/example-exporter-opentelemetry/oats-tests/http/oats-case.yaml
@@ -0,0 +1,11 @@
+name: http protobuf exporter emits uptime metric
+fixture:
+ compose:
+ template: lgtm
+ file: docker-compose.yml
+seed:
+ type: app
+expected:
+ metrics:
+ - promql: "uptime_seconds_total{}"
+ value: ">= 0"
diff --git a/examples/example-exporter-opentelemetry/oats-tests/http/oats.yaml b/examples/example-exporter-opentelemetry/oats-tests/http/oats.yaml
deleted file mode 100644
index 66430ca3b..000000000
--- a/examples/example-exporter-opentelemetry/oats-tests/http/oats.yaml
+++ /dev/null
@@ -1,9 +0,0 @@
-# OATS is an acceptance testing framework for OpenTelemetry -
-# https://github.com/grafana/oats/tree/main/yaml
-docker-compose:
- files:
- - ./docker-compose.yml
-expected:
- metrics:
- - promql: "uptime_seconds_total{}"
- value: ">= 0"
diff --git a/examples/example-exporter-opentelemetry/pom.xml b/examples/example-exporter-opentelemetry/pom.xml
index 4c9d1d143..3bfec6143 100644
--- a/examples/example-exporter-opentelemetry/pom.xml
+++ b/examples/example-exporter-opentelemetry/pom.xml
@@ -1,37 +1,45 @@
-
+
4.0.0
-
- io.prometheus
- examples
- 1.4.0-SNAPSHOT
-
-
+ io.prometheus
example-exporter-opentelemetry
+ 1.8.1-SNAPSHOT
+
+
+ 8
+ UTF-8
+
Example - OpenTelemetry Metrics Exporter
Example of exposing metrics in OpenTelemetry format and pushing them to an OpenTelemetry collector
+
+
+
+ io.prometheus
+ prometheus-metrics-bom
+ 1.5.1
+ pom
+ import
+
+
+
+
io.prometheus
prometheus-metrics-core
- ${project.version}
io.prometheus
prometheus-metrics-instrumentation-jvm
- ${project.version}
io.prometheus
prometheus-metrics-exporter-opentelemetry
- ${project.version}
@@ -49,8 +57,7 @@
-
+
io.prometheus.metrics.examples.opentelemetry.Main
diff --git a/examples/example-exporter-servlet-tomcat/README.md b/examples/example-exporter-servlet-tomcat/README.md
index 05ac894e3..5ca273896 100644
--- a/examples/example-exporter-servlet-tomcat/README.md
+++ b/examples/example-exporter-servlet-tomcat/README.md
@@ -54,24 +54,27 @@ browser:
- [http://localhost:8080/metrics?debug=text](http://localhost:8080/metrics?debug=text): Prometheus
text format, same as
without the `debug` option.
-- [http://localhost:8080/metrics?debug=openmetrics](http://localhost:8080/metrics?debug=openmetrics):
+- [http://localhost:8080/metrics?debug=openmetrics](http://localhost:8080/metrics?debug=openmetrics):
OpenMetrics text
format.
-- [http://localhost:8080/metrics?debug=prometheus-protobuf](http://localhost:8080/metrics?debug=prometheus-protobuf):
+- [http://localhost:8080/metrics?debug=prometheus-protobuf](http://localhost:8080/metrics?debug=prometheus-protobuf):
Text representation of the Prometheus protobuf format.
## Testing with the Prometheus Server
1. Download the latest Prometheus server release
- from [https://github.com/prometheus/prometheus/releases](https://github.com/prometheus/prometheus/releases).
+ from [https://github.com/prometheus/prometheus/releases](https://github.com/prometheus/prometheus/releases).
2. Extract the archive
3. Edit `prometheus.yml` and append the following snippet at the end:
+
```yaml
job_name: "tomcat-servlet-example"
static_configs:
- targets: ["localhost:8080"]
```
-4. Run with native histograms and examplars enabled:
+
+4. Run with native histograms and exemplars enabled:
+
```shell
./prometheus --enable-feature=native-histograms --enable-feature=exemplar-storage
```
@@ -83,7 +86,7 @@ Prometheus is now scraping metrics in Protobuf format. If you type the name
`request_duration_seconds` you will see a
non-human-readable representation of the histogram including the native buckets:
-
+
Note: You have to run at least one GET request on the Hello World
endpoint [http://localhost:8080](http://localhost:8080) before you see the metric.
@@ -94,4 +97,6 @@ Use the `histogram_quantile()` function to calculate quantiles from the native h
histogram_quantile(0.95, rate(request_duration_seconds[10m]))
```
-
+![Screenshot showing the 95th Percentile Calculated from a Prometheus Native Histogram][native-histogram-quantile]
+
+[native-histogram-quantile]: https://github.com/prometheus/client_java/assets/330535/889fb769-9445-4f6f-8540-2b1ddffca55e
diff --git a/examples/example-exporter-servlet-tomcat/pom.xml b/examples/example-exporter-servlet-tomcat/pom.xml
index efb8cdd63..c7a2b1e4d 100644
--- a/examples/example-exporter-servlet-tomcat/pom.xml
+++ b/examples/example-exporter-servlet-tomcat/pom.xml
@@ -1,45 +1,50 @@
-
+
4.0.0
-
- io.prometheus
- examples
- 1.4.0-SNAPSHOT
-
-
+ io.prometheus
example-exporter-servlet-tomcat
+ 1.8.1-SNAPSHOT
+
+
+ 17
+ UTF-8
+
Example - Servlet Exporter with Tomcat
Prometheus Metrics Example using Embedded Tomcat and the Exporter Servlet
-
- 17
-
+
+
+
+ io.prometheus
+ prometheus-metrics-bom
+ 1.5.1
+ pom
+ import
+
+
+
io.prometheus
prometheus-metrics-core
- ${project.version}
io.prometheus
prometheus-metrics-instrumentation-jvm
- ${project.version}
io.prometheus
prometheus-metrics-exporter-servlet-jakarta
- ${project.version}
org.apache.tomcat.embed
tomcat-embed-core
- 11.0.9
+ 11.0.25
@@ -57,8 +62,7 @@
-
+
io.prometheus.metrics.examples.tomcat_servlet.Main
diff --git a/examples/example-exporter-servlet-tomcat/src/main/java/io/prometheus/metrics/examples/tomcat_servlet/Main.java b/examples/example-exporter-servlet-tomcat/src/main/java/io/prometheus/metrics/examples/tomcat_servlet/Main.java
index 81bc2ac19..2348cea9d 100644
--- a/examples/example-exporter-servlet-tomcat/src/main/java/io/prometheus/metrics/examples/tomcat_servlet/Main.java
+++ b/examples/example-exporter-servlet-tomcat/src/main/java/io/prometheus/metrics/examples/tomcat_servlet/Main.java
@@ -24,10 +24,10 @@ public static void main(String[] args) throws LifecycleException, IOException {
Context ctx = tomcat.addContext("", new File(".").getAbsolutePath());
Tomcat.addServlet(ctx, "hello", new HelloWorldServlet());
- ctx.addServletMappingDecoded("/*", "hello");
+ ctx.addServletMapping("/*", "hello");
Tomcat.addServlet(ctx, "metrics", new PrometheusMetricsServlet());
- ctx.addServletMappingDecoded("/metrics", "metrics");
+ ctx.addServletMapping("/metrics", "metrics");
tomcat.getConnector();
tomcat.start();
diff --git a/examples/example-native-histogram/docker-compose.yaml b/examples/example-native-histogram/docker-compose.yaml
index a951f4075..25ff9b5a8 100644
--- a/examples/example-native-histogram/docker-compose.yaml
+++ b/examples/example-native-histogram/docker-compose.yaml
@@ -1,7 +1,7 @@
version: "3"
services:
example-application:
- image: eclipse-temurin:21.0.7_6-jre@sha256:bca347dc76e38a60a1a01b29a7d1312e514603a97ba594268e5a2e4a1a0c9a8f
+ image: eclipse-temurin:25.0.3_9-jre@sha256:7c1c6297dc3a3ff947922f3ab14ecd326e29083b9edaa8dbff3b94fef1688311
network_mode: host
volumes:
- ./target/example-native-histogram.jar:/example-native-histogram.jar
@@ -10,7 +10,7 @@ services:
- -jar
- /example-native-histogram.jar
prometheus:
- image: prom/prometheus:v3.5.0@sha256:63805ebb8d2b3920190daf1cb14a60871b16fd38bed42b857a3182bc621f4996
+ image: prom/prometheus:v3.14.0@sha256:5ce7540c3c00ef4ab0c9d2c995c6a5b9c421f44b4a115d97a2c7af3b1c21cbb0
network_mode: host
volumes:
- ./docker-compose/prometheus.yml:/prometheus.yml
@@ -18,7 +18,7 @@ services:
- --enable-feature=native-histograms
- --config.file=/prometheus.yml
grafana:
- image: grafana/grafana:12.1.0@sha256:6ac590e7cabc2fbe8d7b8fc1ce9c9f0582177b334e0df9c927ebd9670469440f
+ image: grafana/grafana:13.2.0@sha256:3fd54ae1214669f8355f065ec9f6445d5279a3d77095ab048ca045685272429b
network_mode: host
volumes:
- ./docker-compose/grafana-datasources.yaml:/etc/grafana/provisioning/datasources/grafana-datasources.yaml
diff --git a/examples/example-native-histogram/pom.xml b/examples/example-native-histogram/pom.xml
index 9ad73b092..ab0045e88 100644
--- a/examples/example-native-histogram/pom.xml
+++ b/examples/example-native-histogram/pom.xml
@@ -1,36 +1,45 @@
-
+
4.0.0
-
- io.prometheus
- examples
- 1.4.0-SNAPSHOT
-
-
+ io.prometheus
example-native-histogram
+ 1.8.1-SNAPSHOT
+
+
+ 8
+ UTF-8
+
Example - Native Histogram
End-to-End example of a Native histogram: Java app -> Prometheus -> Grafana
+
+
+
+ io.prometheus
+ prometheus-metrics-bom
+ 1.5.1
+ pom
+ import
+
+
+
+
io.prometheus
prometheus-metrics-core
- ${project.version}
io.prometheus
prometheus-metrics-instrumentation-jvm
- ${project.version}
io.prometheus
prometheus-metrics-exporter-httpserver
- ${project.version}
@@ -48,8 +57,7 @@
-
+
io.prometheus.metrics.examples.nativehistogram.Main
diff --git a/examples/example-otel-jvm-runtime-metrics/README.md b/examples/example-otel-jvm-runtime-metrics/README.md
new file mode 100644
index 000000000..a58584694
--- /dev/null
+++ b/examples/example-otel-jvm-runtime-metrics/README.md
@@ -0,0 +1,41 @@
+# OTel JVM Runtime Metrics with Prometheus HTTPServer
+
+## Build
+
+This example is built as part of the `client_java` project.
+
+```shell
+./mvnw package
+```
+
+## Run
+
+The build creates a JAR file with the example application in
+`./examples/example-otel-jvm-runtime-metrics/target/`.
+
+```shell
+java -jar ./examples/example-otel-jvm-runtime-metrics/target/example-otel-jvm-runtime-metrics.jar
+```
+
+## Manually Testing the Metrics Endpoint
+
+Accessing
+[http://localhost:9400/metrics](http://localhost:9400/metrics)
+with a Web browser should yield both a Prometheus counter metric
+and OTel JVM runtime metrics on the same endpoint.
+
+Prometheus counter:
+
+```text
+# HELP uptime_seconds_total total number of seconds since this application was started
+# TYPE uptime_seconds_total counter
+uptime_seconds_total 42.0
+```
+
+OTel JVM runtime metrics (excerpt):
+
+```text
+# HELP jvm_memory_used_bytes Measure of memory used.
+# TYPE jvm_memory_used_bytes gauge
+jvm_memory_used_bytes{jvm_memory_pool_name="G1 Eden Space",jvm_memory_type="heap"} 4194304.0
+```
diff --git a/examples/example-otel-jvm-runtime-metrics/pom.xml b/examples/example-otel-jvm-runtime-metrics/pom.xml
new file mode 100644
index 000000000..75232e35c
--- /dev/null
+++ b/examples/example-otel-jvm-runtime-metrics/pom.xml
@@ -0,0 +1,82 @@
+
+
+ 4.0.0
+
+ io.prometheus
+ example-otel-jvm-runtime-metrics
+ 1.8.1-SNAPSHOT
+
+
+ 8
+ UTF-8
+
+
+ Example - OTel JVM Runtime Metrics
+
+ Example of combining Prometheus metrics with OpenTelemetry JVM runtime metrics on one endpoint
+
+
+
+
+
+ io.prometheus
+ prometheus-metrics-bom
+ 1.5.1
+ pom
+ import
+
+
+ io.opentelemetry.instrumentation
+ opentelemetry-instrumentation-bom-alpha
+ 2.31.0-alpha
+ pom
+ import
+
+
+
+
+
+
+ io.prometheus
+ prometheus-metrics-core
+
+
+ io.prometheus
+ prometheus-metrics-exporter-httpserver
+
+
+ io.prometheus
+ prometheus-metrics-otel-support
+ pom
+
+
+ io.opentelemetry.instrumentation
+ opentelemetry-runtime-telemetry
+
+
+
+
+ ${project.artifactId}
+
+
+ org.apache.maven.plugins
+ maven-shade-plugin
+
+
+ package
+
+ shade
+
+
+
+
+ io.prometheus.metrics.examples.otelruntimemetrics.Main
+
+
+
+
+
+
+
+
+
diff --git a/examples/example-otel-jvm-runtime-metrics/src/main/java/io/prometheus/metrics/examples/otelruntimemetrics/Main.java b/examples/example-otel-jvm-runtime-metrics/src/main/java/io/prometheus/metrics/examples/otelruntimemetrics/Main.java
new file mode 100644
index 000000000..07971096e
--- /dev/null
+++ b/examples/example-otel-jvm-runtime-metrics/src/main/java/io/prometheus/metrics/examples/otelruntimemetrics/Main.java
@@ -0,0 +1,72 @@
+package io.prometheus.metrics.examples.otelruntimemetrics;
+
+import io.opentelemetry.exporter.prometheus.PrometheusMetricReader;
+import io.opentelemetry.instrumentation.runtimetelemetry.RuntimeTelemetry;
+import io.opentelemetry.sdk.OpenTelemetrySdk;
+import io.opentelemetry.sdk.metrics.SdkMeterProvider;
+import io.prometheus.metrics.core.metrics.Counter;
+import io.prometheus.metrics.exporter.httpserver.HTTPServer;
+import io.prometheus.metrics.model.registry.PrometheusRegistry;
+import io.prometheus.metrics.model.snapshots.Unit;
+import java.io.IOException;
+
+/**
+ * Example combining Prometheus metrics with OpenTelemetry JVM runtime metrics on a single endpoint.
+ *
+ * This demonstrates:
+ *
+ *
+ * Registering a Prometheus counter metric
+ * Bridging OTel runtime metrics into the same PrometheusRegistry
+ * Exposing everything via the built-in HTTPServer on /metrics
+ *
+ */
+public class Main {
+
+ public static void main(String[] args) throws IOException, InterruptedException {
+
+ PrometheusRegistry registry = new PrometheusRegistry();
+
+ // 1. Register a Prometheus counter metric
+ Counter counter =
+ Counter.builder()
+ .name("uptime_seconds_total")
+ .help("total number of seconds since this application was started")
+ .unit(Unit.SECONDS)
+ .register(registry);
+
+ // 2. Create a PrometheusMetricReader and register it with the same registry.
+ // This bridges OTel metrics into the Prometheus registry.
+ PrometheusMetricReader reader = PrometheusMetricReader.create();
+ registry.register(reader);
+
+ // 3. Build the OTel SDK with the reader.
+ OpenTelemetrySdk openTelemetry =
+ OpenTelemetrySdk.builder()
+ .setMeterProvider(SdkMeterProvider.builder().registerMetricReader(reader).build())
+ .build();
+
+ // 4. Start OTel JVM runtime metrics collection.
+ RuntimeTelemetry runtimeMetrics = RuntimeTelemetry.create(openTelemetry);
+
+ // 5. Expose both Prometheus and OTel metrics on a single endpoint.
+ HTTPServer server = HTTPServer.builder().port(9400).registry(registry).buildAndStart();
+
+ // 6. Close RuntimeMetrics and server on shutdown to stop JMX metric collection.
+ Runtime.getRuntime()
+ .addShutdownHook(
+ new Thread(
+ () -> {
+ runtimeMetrics.close();
+ server.close();
+ }));
+
+ System.out.println(
+ "HTTPServer listening on port http://localhost:" + server.getPort() + "/metrics");
+
+ while (true) {
+ Thread.sleep(1000);
+ counter.inc();
+ }
+ }
+}
diff --git a/examples/example-prometheus-properties/README.md b/examples/example-prometheus-properties/README.md
index 2a4e61e80..9faed9201 100644
--- a/examples/example-prometheus-properties/README.md
+++ b/examples/example-prometheus-properties/README.md
@@ -17,8 +17,8 @@ This should create the file
java -jar ./examples/example-prometheus-properties/target/example-prometheus-properties.jar
```
-View the metrics
-on [http://localhost:9401/metrics?name[]=request_duration_seconds&name[]=request_size_bytes](http://localhost:9401/metrics?name[]=request_duration_seconds&name[]=request_size_bytes).
+View the metrics on
+[http://localhost:9401/metrics?name[]=request_duration_seconds&name[]=request_size_bytes][metrics-url].
The example has a `prometheus.properties` file in the classpath with a few examples of how to change
settings at runtime.
@@ -28,3 +28,5 @@ There are multiple alternative ways to specify the location of the `prometheus.p
- Put it in the classpath, like in this example.
- Set the environment variable `PROMETHEUS_CONFIG` to the file location.
- Set the `prometheus.config` System property to the file location.
+
+[metrics-url]: http://localhost:9401/metrics?name[]=request_duration_seconds&name[]=request_size_bytes
diff --git a/examples/example-prometheus-properties/pom.xml b/examples/example-prometheus-properties/pom.xml
index 4556e7f73..3bf352d16 100644
--- a/examples/example-prometheus-properties/pom.xml
+++ b/examples/example-prometheus-properties/pom.xml
@@ -1,36 +1,45 @@
-
+
4.0.0
-
- io.prometheus
- examples
- 1.4.0-SNAPSHOT
-
-
+ io.prometheus
example-prometheus-properties
+ 1.8.1-SNAPSHOT
+
+
+ 8
+ UTF-8
+
Example - prometheus.properties
Example of runtime configuration with prometheus.properties
+
+
+
+ io.prometheus
+ prometheus-metrics-bom
+ 1.5.1
+ pom
+ import
+
+
+
+
io.prometheus
prometheus-metrics-core
- ${project.version}
io.prometheus
prometheus-metrics-instrumentation-jvm
- ${project.version}
io.prometheus
prometheus-metrics-exporter-httpserver
- ${project.version}
@@ -48,8 +57,7 @@
-
+
io.prometheus.metrics.examples.prometheus_properties.Main
diff --git a/examples/example-prometheus-properties/src/main/resources/prometheus.properties b/examples/example-prometheus-properties/src/main/resources/prometheus.properties
index a786fd370..be895f2fe 100644
--- a/examples/example-prometheus-properties/src/main/resources/prometheus.properties
+++ b/examples/example-prometheus-properties/src/main/resources/prometheus.properties
@@ -1,8 +1,8 @@
-io.prometheus.exporter.httpServer.port = 9401
-io.prometheus.exporter.includeCreatedTimestamps = true
+io.prometheus.exporter.http_server.port = 9401
+io.prometheus.exporter.include_created_timestamps = true
# Set a new default for all histograms
-io.prometheus.metrics.histogramClassicUpperBounds = .2, .4, .8, .1
+io.prometheus.metrics.histogram_classic_upper_bounds = .2, .4, .8, .1
# Override the default for one specific histogram
-io.prometheus.metrics.request_size_bytes.histogramClassicUpperBounds = 256, 512, 768, 1024
+io.prometheus.metrics.request_size_bytes.histogram_classic_upper_bounds = 256, 512, 768, 1024
diff --git a/examples/example-simpleclient-bridge/pom.xml b/examples/example-simpleclient-bridge/pom.xml
index f5e465d06..0f8bf496a 100644
--- a/examples/example-simpleclient-bridge/pom.xml
+++ b/examples/example-simpleclient-bridge/pom.xml
@@ -1,21 +1,33 @@
-
+
4.0.0
-
- io.prometheus
- examples
- 1.4.0-SNAPSHOT
-
-
+ io.prometheus
example-simpleclient-bridge
+ 1.8.1-SNAPSHOT
+
+
+ 8
+ UTF-8
+
Example - Simpleclient Bridge
Prometheus Metrics Example of the Simpleclient Backwards Compatibility module
+
+
+
+ io.prometheus
+ prometheus-metrics-bom
+ 1.5.1
+ pom
+ import
+
+
+
+
io.prometheus
@@ -25,12 +37,10 @@
io.prometheus
prometheus-metrics-simpleclient-bridge
- ${project.version}
io.prometheus
prometheus-metrics-exporter-httpserver
- ${project.version}
@@ -48,8 +58,7 @@
-
+
io.prometheus.metrics.examples.simpleclient.Main
diff --git a/examples/pom.xml b/examples/pom.xml
index b5f8886ce..80594e550 100644
--- a/examples/pom.xml
+++ b/examples/pom.xml
@@ -1,12 +1,11 @@
-
+
4.0.0
io.prometheus
client_java
- 1.4.0-SNAPSHOT
+ 1.8.1-SNAPSHOT
examples
@@ -19,6 +18,7 @@
true
+ true
@@ -29,7 +29,9 @@
example-exporter-opentelemetry
example-simpleclient-bridge
example-native-histogram
+ example-custom-buckets
example-prometheus-properties
+ example-otel-jvm-runtime-metrics
diff --git a/integration-tests/it-common/pom.xml b/integration-tests/it-common/pom.xml
index 5586a7f9a..9a4895f1c 100644
--- a/integration-tests/it-common/pom.xml
+++ b/integration-tests/it-common/pom.xml
@@ -1,12 +1,11 @@
-
+
4.0.0
io.prometheus
integration-tests
- 1.4.0-SNAPSHOT
+ 1.8.1-SNAPSHOT
it-common
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
index 00a3d544f..449a19186 100644
--- 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
@@ -4,13 +4,13 @@
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_31_1.Metrics;
+import io.prometheus.metrics.expositionformats.generated.Metrics;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
+import java.net.URI;
import java.net.URISyntaxException;
-import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
@@ -24,7 +24,7 @@
import org.testcontainers.containers.GenericContainer;
public abstract class ExporterTest {
- private final GenericContainer> sampleAppContainer;
+ protected final GenericContainer> sampleAppContainer;
private final Volume sampleAppVolume;
protected final String sampleApp;
@@ -33,8 +33,12 @@ public ExporterTest(String sampleApp) throws IOException, URISyntaxException {
this.sampleAppVolume =
Volume.create("it-exporter")
.copy("../../it-" + sampleApp + "/target/" + sampleApp + ".jar");
+ String javaVersion = System.getenv("TEST_JAVA_VERSION");
+ if (javaVersion == null || javaVersion.isEmpty()) {
+ javaVersion = "25";
+ }
this.sampleAppContainer =
- new GenericContainer<>("openjdk:17")
+ new GenericContainer<>("eclipse-temurin:" + javaVersion)
.withFileSystemBind(sampleAppVolume.getHostPath(), "/app", BindMode.READ_ONLY)
.withWorkingDirectory("/app")
.withLogConsumer(LogConsumer.withPrefix(sampleApp))
@@ -53,7 +57,7 @@ protected void start(String outcome) {
}
@AfterEach
- public void tearDown() throws IOException {
+ void tearDown() throws IOException {
sampleAppContainer.stop();
sampleAppVolume.remove();
}
@@ -68,7 +72,7 @@ protected Response scrape(String method, String queryString, String... requestHe
throws IOException {
return scrape(
method,
- new URL(
+ URI.create(
"http://localhost:"
+ sampleAppContainer.getMappedPort(9400)
+ "/metrics?"
@@ -76,10 +80,10 @@ protected Response scrape(String method, String queryString, String... requestHe
requestHeaders);
}
- public static Response scrape(String method, URL url, String... requestHeaders)
+ public static Response scrape(String method, URI uri, String... requestHeaders)
throws IOException {
long timeoutMillis = TimeUnit.SECONDS.toMillis(5);
- HttpURLConnection con = (HttpURLConnection) url.openConnection();
+ HttpURLConnection con = (HttpURLConnection) uri.toURL().openConnection();
con.setRequestMethod(method);
for (int i = 0; i < requestHeaders.length; i += 2) {
con.setRequestProperty(requestHeaders[i], requestHeaders[i + 1]);
@@ -111,7 +115,7 @@ public static Response scrape(String method, URL url, String... requestHeaders)
if (exception != null) {
exception.printStackTrace();
}
- fail("timeout while getting metrics from " + url);
+ fail("timeout while getting metrics from " + uri);
return null; // will not happen
}
diff --git a/integration-tests/it-exporter/it-exporter-duplicate-metrics-sample/pom.xml b/integration-tests/it-exporter/it-exporter-duplicate-metrics-sample/pom.xml
new file mode 100644
index 000000000..e0c05f7f8
--- /dev/null
+++ b/integration-tests/it-exporter/it-exporter-duplicate-metrics-sample/pom.xml
@@ -0,0 +1,57 @@
+
+
+ 4.0.0
+
+
+ io.prometheus
+ it-exporter
+ 1.8.1-SNAPSHOT
+
+
+ it-exporter-duplicate-metrics-sample
+
+ Integration Tests - Duplicate Metrics Sample
+
+ HTTPServer Sample demonstrating duplicate metric names with different label sets
+
+
+
+
+ io.prometheus
+ prometheus-metrics-exporter-httpserver
+ ${project.version}
+
+
+ io.prometheus
+ prometheus-metrics-core
+ ${project.version}
+
+
+
+
+ exporter-duplicate-metrics-sample
+
+
+ org.apache.maven.plugins
+ maven-shade-plugin
+
+
+ package
+
+ shade
+
+
+
+
+
+ io.prometheus.metrics.it.exporter.duplicatemetrics.DuplicateMetricsSample
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/integration-tests/it-exporter/it-exporter-duplicate-metrics-sample/src/main/java/io/prometheus/metrics/it/exporter/duplicatemetrics/DuplicateMetricsSample.java b/integration-tests/it-exporter/it-exporter-duplicate-metrics-sample/src/main/java/io/prometheus/metrics/it/exporter/duplicatemetrics/DuplicateMetricsSample.java
new file mode 100644
index 000000000..c6005674a
--- /dev/null
+++ b/integration-tests/it-exporter/it-exporter-duplicate-metrics-sample/src/main/java/io/prometheus/metrics/it/exporter/duplicatemetrics/DuplicateMetricsSample.java
@@ -0,0 +1,91 @@
+package io.prometheus.metrics.it.exporter.duplicatemetrics;
+
+import io.prometheus.metrics.core.metrics.Counter;
+import io.prometheus.metrics.core.metrics.Gauge;
+import io.prometheus.metrics.exporter.httpserver.HTTPServer;
+import io.prometheus.metrics.model.snapshots.Unit;
+import java.io.IOException;
+
+/** Integration test sample demonstrating metrics with duplicate names but different label sets. */
+public class DuplicateMetricsSample {
+
+ public static void main(String[] args) throws IOException, InterruptedException {
+ if (args.length != 2) {
+ System.err.println("Usage: java -jar duplicate-metrics-sample.jar ");
+ System.err.println("Where outcome is \"success\" or \"error\".");
+ System.exit(1);
+ }
+
+ int port = parsePortOrExit(args[0]);
+ String outcome = args[1];
+ run(port, outcome);
+ }
+
+ private static void run(int port, String outcome) throws IOException, InterruptedException {
+ // Register multiple counters with the same Prometheus name "http_requests_total"
+ // but different label sets
+ Counter requestsSuccess =
+ Counter.builder()
+ .name("http_requests_total")
+ .help("Total HTTP requests by status")
+ .labelNames("status", "method")
+ .register();
+ requestsSuccess.labelValues("success", "GET").inc(150);
+ requestsSuccess.labelValues("success", "POST").inc(45);
+
+ Counter requestsError =
+ Counter.builder()
+ .name("http_requests_total")
+ .help("Total HTTP requests by status")
+ .labelNames("status", "endpoint")
+ .register();
+ requestsError.labelValues("error", "/api").inc(5);
+ requestsError.labelValues("error", "/health").inc(2);
+
+ // Register multiple gauges with the same Prometheus name "active_connections"
+ // but different label sets
+ Gauge connectionsByRegion =
+ Gauge.builder()
+ .name("active_connections")
+ .help("Active connections")
+ .labelNames("region", "protocol")
+ .register();
+ connectionsByRegion.labelValues("us-east", "http").set(42);
+ connectionsByRegion.labelValues("us-west", "http").set(38);
+ connectionsByRegion.labelValues("eu-west", "https").set(55);
+
+ Gauge connectionsByPool =
+ Gauge.builder()
+ .name("active_connections")
+ .help("Active connections")
+ .labelNames("pool", "type")
+ .register();
+ connectionsByPool.labelValues("primary", "read").set(30);
+ connectionsByPool.labelValues("replica", "write").set(10);
+
+ // Also add a regular metric without duplicates for reference
+ Counter uniqueMetric =
+ Counter.builder()
+ .name("unique_metric_total")
+ .help("A unique metric for reference")
+ .unit(Unit.BYTES)
+ .register();
+ uniqueMetric.inc(1024);
+
+ HTTPServer server = HTTPServer.builder().port(port).buildAndStart();
+
+ System.out.println(
+ "DuplicateMetricsSample listening on 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
+ }
+}
diff --git a/integration-tests/it-exporter/it-exporter-httpserver-sample/pom.xml b/integration-tests/it-exporter/it-exporter-httpserver-sample/pom.xml
index 08f3e392f..5255122dd 100644
--- a/integration-tests/it-exporter/it-exporter-httpserver-sample/pom.xml
+++ b/integration-tests/it-exporter/it-exporter-httpserver-sample/pom.xml
@@ -1,12 +1,11 @@
-
+
4.0.0
io.prometheus
it-exporter
- 1.4.0-SNAPSHOT
+ 1.8.1-SNAPSHOT
it-exporter-httpserver-sample
@@ -43,8 +42,7 @@
-
+
io.prometheus.metrics.it.exporter.httpserver.HTTPServerSample
diff --git a/integration-tests/it-exporter/it-exporter-no-protobuf/pom.xml b/integration-tests/it-exporter/it-exporter-no-protobuf/pom.xml
index 5e2c64ce3..0193d414f 100644
--- a/integration-tests/it-exporter/it-exporter-no-protobuf/pom.xml
+++ b/integration-tests/it-exporter/it-exporter-no-protobuf/pom.xml
@@ -1,12 +1,11 @@
-
+
4.0.0
io.prometheus
it-exporter
- 1.4.0-SNAPSHOT
+ 1.8.1-SNAPSHOT
it-exporter-no-protobuf
@@ -55,8 +54,7 @@
-
+
io.prometheus.metrics.it.exporter.httpserver.HTTPServerSample
diff --git a/integration-tests/it-exporter/it-exporter-servlet-jetty-sample/pom.xml b/integration-tests/it-exporter/it-exporter-servlet-jetty-sample/pom.xml
index fef7042df..a8a93ab20 100644
--- a/integration-tests/it-exporter/it-exporter-servlet-jetty-sample/pom.xml
+++ b/integration-tests/it-exporter/it-exporter-servlet-jetty-sample/pom.xml
@@ -1,12 +1,11 @@
-
+
4.0.0
io.prometheus
it-exporter
- 1.4.0-SNAPSHOT
+ 1.8.1-SNAPSHOT
it-exporter-servlet-jetty-sample
@@ -16,8 +15,8 @@
Jetty Sample for the Exporter Integration Test
- 12.0.23
- 17
+ 12.1.12
+ 25
@@ -57,8 +56,7 @@
-
+
io.prometheus.metrics.it.exporter.servlet.jetty.ExporterServletJettySample
diff --git a/integration-tests/it-exporter/it-exporter-servlet-tomcat-sample/pom.xml b/integration-tests/it-exporter/it-exporter-servlet-tomcat-sample/pom.xml
index 16124705e..7e9d9ab7d 100644
--- a/integration-tests/it-exporter/it-exporter-servlet-tomcat-sample/pom.xml
+++ b/integration-tests/it-exporter/it-exporter-servlet-tomcat-sample/pom.xml
@@ -1,12 +1,11 @@
-
+
4.0.0
io.prometheus
it-exporter
- 1.4.0-SNAPSHOT
+ 1.8.1-SNAPSHOT
it-exporter-servlet-tomcat-sample
@@ -17,7 +16,7 @@
- 17
+ 25
@@ -34,7 +33,7 @@
org.apache.tomcat.embed
tomcat-embed-core
- 11.0.9
+ 11.0.25
@@ -52,8 +51,7 @@
-
+
io.prometheus.metrics.it.exporter.servlet.tomcat.ExporterServletTomcatSample
diff --git a/integration-tests/it-exporter/it-exporter-servlet-tomcat-sample/src/main/java/io/prometheus/metrics/it/exporter/servlet/tomcat/ExporterServletTomcatSample.java b/integration-tests/it-exporter/it-exporter-servlet-tomcat-sample/src/main/java/io/prometheus/metrics/it/exporter/servlet/tomcat/ExporterServletTomcatSample.java
index fa470b306..3cfd603c0 100644
--- a/integration-tests/it-exporter/it-exporter-servlet-tomcat-sample/src/main/java/io/prometheus/metrics/it/exporter/servlet/tomcat/ExporterServletTomcatSample.java
+++ b/integration-tests/it-exporter/it-exporter-servlet-tomcat-sample/src/main/java/io/prometheus/metrics/it/exporter/servlet/tomcat/ExporterServletTomcatSample.java
@@ -78,7 +78,7 @@ private static void run(Mode mode, int port) throws IOException, LifecycleExcept
tomcat.setBaseDir(tmpDir.toFile().getAbsolutePath());
Context ctx = tomcat.addContext("", new File(".").getAbsolutePath());
Tomcat.addServlet(ctx, "metrics", new PrometheusMetricsServlet());
- ctx.addServletMappingDecoded("/metrics", "metrics");
+ ctx.addServletMapping("/metrics", "metrics");
tomcat.getConnector();
tomcat.start();
diff --git a/integration-tests/it-exporter/it-exporter-test/pom.xml b/integration-tests/it-exporter/it-exporter-test/pom.xml
index b50529468..40ee58412 100644
--- a/integration-tests/it-exporter/it-exporter-test/pom.xml
+++ b/integration-tests/it-exporter/it-exporter-test/pom.xml
@@ -1,12 +1,11 @@
-
+
4.0.0
io.prometheus
it-exporter
- 1.4.0-SNAPSHOT
+ 1.8.1-SNAPSHOT
it-exporter-test
@@ -24,5 +23,11 @@
${project.version}
test
+
+ com.google.guava
+ guava
+ ${guava.version}
+ test
+
diff --git a/integration-tests/it-exporter/it-exporter-test/src/test/java/io/prometheus/metrics/it/exporter/test/DuplicateMetricsIT.java b/integration-tests/it-exporter/it-exporter-test/src/test/java/io/prometheus/metrics/it/exporter/test/DuplicateMetricsIT.java
new file mode 100644
index 000000000..d69a65a29
--- /dev/null
+++ b/integration-tests/it-exporter/it-exporter-test/src/test/java/io/prometheus/metrics/it/exporter/test/DuplicateMetricsIT.java
@@ -0,0 +1,181 @@
+package io.prometheus.metrics.it.exporter.test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import io.prometheus.client.it.common.ExporterTest;
+import io.prometheus.metrics.expositionformats.generated.Metrics;
+import java.io.IOException;
+import java.net.URISyntaxException;
+import java.util.List;
+import org.junit.jupiter.api.Test;
+
+class DuplicateMetricsIT extends ExporterTest {
+
+ public DuplicateMetricsIT() throws IOException, URISyntaxException {
+ super("exporter-duplicate-metrics-sample");
+ }
+
+ @Test
+ void testDuplicateMetricsInPrometheusTextFormat() throws IOException {
+ start();
+ Response response = scrape("GET", "");
+ assertThat(response.status).isEqualTo(200);
+ assertContentType(
+ "text/plain; version=0.0.4; charset=utf-8", response.getHeader("Content-Type"));
+
+ String expected =
+ """
+ # HELP active_connections Active connections
+ # TYPE active_connections gauge
+ active_connections{pool="primary",type="read"} 30.0
+ active_connections{pool="replica",type="write"} 10.0
+ active_connections{protocol="http",region="us-east"} 42.0
+ active_connections{protocol="http",region="us-west"} 38.0
+ active_connections{protocol="https",region="eu-west"} 55.0
+ # HELP http_requests_total Total HTTP requests by status
+ # TYPE http_requests_total counter
+ http_requests_total{endpoint="/api",status="error"} 5.0
+ http_requests_total{endpoint="/health",status="error"} 2.0
+ http_requests_total{method="GET",status="success"} 150.0
+ http_requests_total{method="POST",status="success"} 45.0
+ # HELP unique_metric_bytes_total A unique metric for reference
+ # TYPE unique_metric_bytes_total counter
+ unique_metric_bytes_total 1024.0
+ """;
+
+ assertThat(response.stringBody()).isEqualTo(expected);
+ }
+
+ @Test
+ void testDuplicateMetricsInOpenMetricsTextFormat() throws IOException {
+ start();
+ Response response =
+ scrape("GET", "", "Accept", "application/openmetrics-text; version=1.0.0; charset=utf-8");
+ assertThat(response.status).isEqualTo(200);
+ assertContentType(
+ "application/openmetrics-text; version=1.0.0; charset=utf-8",
+ response.getHeader("Content-Type"));
+
+ // OpenMetrics format should have UNIT for unique_metric_bytes (base name without _total)
+ String expected =
+ """
+ # TYPE active_connections gauge
+ # HELP active_connections Active connections
+ active_connections{pool="primary",type="read"} 30.0
+ active_connections{pool="replica",type="write"} 10.0
+ active_connections{protocol="http",region="us-east"} 42.0
+ active_connections{protocol="http",region="us-west"} 38.0
+ active_connections{protocol="https",region="eu-west"} 55.0
+ # TYPE http_requests counter
+ # HELP http_requests Total HTTP requests by status
+ http_requests_total{endpoint="/api",status="error"} 5.0
+ http_requests_total{endpoint="/health",status="error"} 2.0
+ http_requests_total{method="GET",status="success"} 150.0
+ http_requests_total{method="POST",status="success"} 45.0
+ # TYPE unique_metric_bytes counter
+ # UNIT unique_metric_bytes bytes
+ # HELP unique_metric_bytes A unique metric for reference
+ unique_metric_bytes_total 1024.0
+ # EOF
+ """;
+
+ assertThat(response.stringBody()).isEqualTo(expected);
+ }
+
+ @Test
+ void testDuplicateMetricsInPrometheusProtobufFormat() throws IOException {
+ start();
+ Response response =
+ scrape(
+ "GET",
+ "",
+ "Accept",
+ "application/vnd.google.protobuf; proto=io.prometheus.client.MetricFamily;"
+ + " encoding=delimited");
+ assertThat(response.status).isEqualTo(200);
+ assertContentType(
+ "application/vnd.google.protobuf; proto=io.prometheus.client.MetricFamily;"
+ + " encoding=delimited",
+ response.getHeader("Content-Type"));
+
+ List metrics = response.protoBody();
+
+ assertThat(metrics).hasSize(3);
+
+ // Metrics are sorted by name
+ assertThat(metrics.get(0).getName()).isEqualTo("active_connections");
+ assertThat(metrics.get(1).getName()).isEqualTo("http_requests_total");
+ assertThat(metrics.get(2).getName()).isEqualTo("unique_metric_bytes_total");
+
+ // Verify active_connections has all 5 data points merged
+ Metrics.MetricFamily activeConnections = metrics.get(0);
+ assertThat(activeConnections.getType()).isEqualTo(Metrics.MetricType.GAUGE);
+ assertThat(activeConnections.getHelp()).isEqualTo("Active connections");
+ assertThat(activeConnections.getMetricList()).hasSize(5);
+
+ // Verify http_requests_total has all 4 data points merged
+ Metrics.MetricFamily httpRequests = metrics.get(1);
+ assertThat(httpRequests.getType()).isEqualTo(Metrics.MetricType.COUNTER);
+ assertThat(httpRequests.getHelp()).isEqualTo("Total HTTP requests by status");
+ assertThat(httpRequests.getMetricList()).hasSize(4);
+
+ // Verify each data point has the expected labels
+ boolean foundSuccessGet = false;
+ boolean foundSuccessPost = false;
+ boolean foundErrorApi = false;
+ boolean foundErrorHealth = false;
+
+ for (Metrics.Metric metric : httpRequests.getMetricList()) {
+ List labels = metric.getLabelList();
+ if (hasLabel(labels, "status", "success") && hasLabel(labels, "method", "GET")) {
+ assertThat(metric.getCounter().getValue()).isEqualTo(150.0);
+ foundSuccessGet = true;
+ } else if (hasLabel(labels, "status", "success") && hasLabel(labels, "method", "POST")) {
+ assertThat(metric.getCounter().getValue()).isEqualTo(45.0);
+ foundSuccessPost = true;
+ } else if (hasLabel(labels, "status", "error") && hasLabel(labels, "endpoint", "/api")) {
+ assertThat(metric.getCounter().getValue()).isEqualTo(5.0);
+ foundErrorApi = true;
+ } else if (hasLabel(labels, "status", "error") && hasLabel(labels, "endpoint", "/health")) {
+ assertThat(metric.getCounter().getValue()).isEqualTo(2.0);
+ foundErrorHealth = true;
+ }
+ }
+
+ assertThat(foundSuccessGet).isTrue();
+ assertThat(foundSuccessPost).isTrue();
+ assertThat(foundErrorApi).isTrue();
+ assertThat(foundErrorHealth).isTrue();
+
+ Metrics.MetricFamily uniqueMetric = metrics.get(2);
+ assertThat(uniqueMetric.getType()).isEqualTo(Metrics.MetricType.COUNTER);
+ assertThat(uniqueMetric.getMetricList()).hasSize(1);
+ assertThat(uniqueMetric.getMetric(0).getCounter().getValue()).isEqualTo(1024.0);
+ }
+
+ @Test
+ void testDuplicateMetricsWithNameFilter() throws IOException {
+ start();
+ // Only scrape http_requests_total
+ Response response = scrape("GET", nameParam());
+ assertThat(response.status).isEqualTo(200);
+
+ String body = response.stringBody();
+
+ assertThat(body)
+ .contains("http_requests_total{method=\"GET\",status=\"success\"} 150.0")
+ .contains("http_requests_total{endpoint=\"/api\",status=\"error\"} 5.0");
+
+ // Should NOT contain active_connections or unique_metric_total
+ assertThat(body).doesNotContain("active_connections").doesNotContain("unique_metric_total");
+ }
+
+ private boolean hasLabel(List labels, String name, String value) {
+ return labels.stream()
+ .anyMatch(label -> label.getName().equals(name) && label.getValue().equals(value));
+ }
+
+ private String nameParam() {
+ return "name[]=" + "http_requests_total";
+ }
+}
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 1ab3f3237..5a80d8bdf 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
@@ -5,11 +5,12 @@
import com.google.common.io.Resources;
import io.prometheus.client.it.common.ExporterTest;
-import io.prometheus.metrics.expositionformats.generated.com_google_protobuf_4_31_1.Metrics;
+import io.prometheus.metrics.expositionformats.generated.Metrics;
import java.io.IOException;
import java.net.URISyntaxException;
import java.net.URLEncoder;
import java.util.List;
+import java.util.regex.Pattern;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
@@ -21,7 +22,7 @@ public ExporterIT(String sampleApp) throws IOException, URISyntaxException {
}
@Test
- public void testOpenMetricsTextFormat() throws IOException {
+ void testOpenMetricsTextFormat() throws IOException {
start();
Response response =
scrape("GET", "", "Accept", "application/openmetrics-text; version=1.0.0; charset=utf-8");
@@ -43,7 +44,7 @@ public void testOpenMetricsTextFormat() throws IOException {
}
@Test
- public void testPrometheusTextFormat() throws IOException {
+ void testPrometheusTextFormat() throws IOException {
start();
Response response = scrape("GET", "");
assertThat(response.status).isEqualTo(200);
@@ -63,7 +64,7 @@ public void testPrometheusTextFormat() throws IOException {
}
@Test
- public void testPrometheusProtobufFormat() throws IOException {
+ void testPrometheusProtobufFormat() throws IOException {
start();
Response response =
scrape(
@@ -101,15 +102,30 @@ public void testPrometheusProtobufDebugFormat(String format, String expected) th
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));
+
+ String actualResponse = response.stringBody().trim();
+ String expectedResponse =
+ Resources.toString(Resources.getResource(expected), UTF_8)
+ .trim()
+ .replace("", sampleApp);
+
+ if ("prometheus-protobuf".equals(format)) {
+ // Protobuf text format omits fields with value 0, so nanos may be absent.
+ // Replace the nanos placeholder with a regex-friendly marker before quoting.
+ String withOptionalNanos =
+ expectedResponse.replace("\n nanos: ", "");
+ String pattern =
+ Pattern.quote(withOptionalNanos)
+ .replace("", "\\E\\d+\\Q")
+ .replace("", "\\E(\n nanos: \\d+)?\\Q");
+ assertThat(actualResponse).matches(pattern);
+ } else {
+ assertThat(actualResponse).isEqualTo(expectedResponse);
+ }
}
@Test
- public void testCompression() throws IOException {
+ void testCompression() throws IOException {
start();
Response response =
scrape(
@@ -137,7 +153,7 @@ public void testCompression() throws IOException {
}
@Test
- public void testErrorHandling() throws IOException {
+ void testErrorHandling() throws IOException {
start("error");
Response response = scrape("GET", "");
assertThat(response.status).isEqualTo(500);
@@ -145,7 +161,7 @@ public void testErrorHandling() throws IOException {
}
@Test
- public void testHeadRequest() throws IOException {
+ void testHeadRequest() throws IOException {
start();
Response fullResponse = scrape("GET", "");
int size = fullResponse.body.length;
@@ -157,7 +173,7 @@ public void testHeadRequest() throws IOException {
}
@Test
- public void testDebug() throws IOException {
+ void testDebug() throws IOException {
start();
Response response = scrape("GET", "debug=openmetrics");
assertThat(response.status).isEqualTo(200);
@@ -168,7 +184,7 @@ public void testDebug() throws IOException {
}
@Test
- public void testNameFilter() throws IOException {
+ void testNameFilter() throws IOException {
start();
Response response =
scrape(
@@ -187,7 +203,7 @@ public void testNameFilter() throws IOException {
}
@Test
- public void testEmptyResponseOpenMetrics() throws IOException {
+ void testEmptyResponseOpenMetrics() throws IOException {
start();
Response response =
scrape(
@@ -205,7 +221,7 @@ public void testEmptyResponseOpenMetrics() throws IOException {
}
@Test
- public void testEmptyResponseText() throws IOException {
+ void testEmptyResponseText() throws IOException {
start();
Response response = scrape("GET", nameParam("none_existing"));
assertThat(response.status).isEqualTo(200);
@@ -219,7 +235,7 @@ public void testEmptyResponseText() throws IOException {
}
@Test
- public void testEmptyResponseProtobuf() throws IOException {
+ void testEmptyResponseProtobuf() throws IOException {
start();
Response response =
scrape(
@@ -237,7 +253,7 @@ public void testEmptyResponseProtobuf() throws IOException {
}
@Test
- public void testEmptyResponseGzipOpenMetrics() throws IOException {
+ void testEmptyResponseGzipOpenMetrics() throws IOException {
start();
Response response =
scrape(
@@ -253,7 +269,7 @@ public void testEmptyResponseGzipOpenMetrics() throws IOException {
}
@Test
- public void testEmptyResponseGzipText() throws IOException {
+ void testEmptyResponseGzipText() throws IOException {
start();
Response response = scrape("GET", nameParam("none_existing"), "Accept-Encoding", "gzip");
assertThat(response.status).isEqualTo(200);
@@ -266,7 +282,7 @@ private String nameParam(String name) {
}
@Test
- public void testDebugUnknown() throws IOException {
+ void testDebugUnknown() throws IOException {
start();
Response response = scrape("GET", "debug=unknown");
assertThat(response.status).isEqualTo(500);
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
index 1d7603c1b..06f19b85c 100644
--- 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
@@ -37,6 +37,10 @@ type: COUNTER
metric {
counter {
value: 17.0
+ created_timestamp {
+ seconds:
+ nanos:
+ }
}
}
diff --git a/integration-tests/it-exporter/it-no-protobuf-test/pom.xml b/integration-tests/it-exporter/it-no-protobuf-test/pom.xml
index 98a58f84e..389867de5 100644
--- a/integration-tests/it-exporter/it-no-protobuf-test/pom.xml
+++ b/integration-tests/it-exporter/it-no-protobuf-test/pom.xml
@@ -1,12 +1,11 @@
-
+
4.0.0
io.prometheus
it-exporter
- 1.4.0-SNAPSHOT
+ 1.8.1-SNAPSHOT
it-no-protobuf-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
index cd534dcb9..9b041795e 100644
--- 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
@@ -14,7 +14,7 @@ public NoProtobufIT() throws IOException, URISyntaxException {
}
@Test
- public void testPrometheusProtobufDebugFormat() throws IOException {
+ void testPrometheusProtobufDebugFormat() throws IOException {
start();
assertThat(scrape("GET", "debug=text").status).isEqualTo(200);
// protobuf is not supported
diff --git a/integration-tests/it-exporter/pom.xml b/integration-tests/it-exporter/pom.xml
index 3dcb27f28..b759c4330 100644
--- a/integration-tests/it-exporter/pom.xml
+++ b/integration-tests/it-exporter/pom.xml
@@ -1,12 +1,11 @@
-
+
4.0.0
io.prometheus
integration-tests
- 1.4.0-SNAPSHOT
+ 1.8.1-SNAPSHOT
it-exporter
@@ -21,6 +20,7 @@
it-exporter-servlet-tomcat-sample
it-exporter-servlet-jetty-sample
it-exporter-httpserver-sample
+ it-exporter-duplicate-metrics-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 b0b0bdc33..bfc73ab10 100644
--- a/integration-tests/it-pushgateway/pom.xml
+++ b/integration-tests/it-pushgateway/pom.xml
@@ -1,12 +1,11 @@
-
+
4.0.0
io.prometheus
integration-tests
- 1.4.0-SNAPSHOT
+ 1.8.1-SNAPSHOT
it-pushgateway
@@ -48,7 +47,7 @@
com.jayway.jsonpath
json-path
- 2.9.0
+ 3.0.0
test
@@ -67,8 +66,7 @@
-
+
io.prometheus.metrics.it.pushgateway.PushGatewayTestApp
diff --git a/integration-tests/it-pushgateway/src/test/java/io/prometheus/metrics/it/pushgateway/PushGatewayIT.java b/integration-tests/it-pushgateway/src/test/java/io/prometheus/metrics/it/pushgateway/PushGatewayIT.java
index beb83d7d9..3d31129f1 100644
--- a/integration-tests/it-pushgateway/src/test/java/io/prometheus/metrics/it/pushgateway/PushGatewayIT.java
+++ b/integration-tests/it-pushgateway/src/test/java/io/prometheus/metrics/it/pushgateway/PushGatewayIT.java
@@ -22,7 +22,7 @@
import org.testcontainers.containers.wait.strategy.Wait;
import org.testcontainers.utility.MountableFile;
-public class PushGatewayIT {
+class PushGatewayIT {
private GenericContainer> sampleAppContainer;
private GenericContainer> pushGatewayContainer;
@@ -30,9 +30,13 @@ public class PushGatewayIT {
private Volume sampleAppVolume;
@BeforeEach
- public void setUp() throws IOException, URISyntaxException {
+ void setUp() throws IOException, URISyntaxException {
Network network = Network.newNetwork();
sampleAppVolume = Volume.create("it-pushgateway").copy("pushgateway-test-app.jar");
+ String javaVersion = System.getenv("TEST_JAVA_VERSION");
+ if (javaVersion == null || javaVersion.isEmpty()) {
+ javaVersion = "25";
+ }
pushGatewayContainer =
new GenericContainer<>("prom/pushgateway:v1.8.0")
.withExposedPorts(9091)
@@ -41,7 +45,7 @@ public void setUp() throws IOException, URISyntaxException {
.withLogConsumer(LogConsumer.withPrefix("pushgateway"))
.waitingFor(Wait.forListeningPort());
sampleAppContainer =
- new GenericContainer<>("openjdk:17")
+ new GenericContainer<>("eclipse-temurin:" + javaVersion)
.withFileSystemBind(sampleAppVolume.getHostPath(), "/app", BindMode.READ_ONLY)
.withNetwork(network)
.withWorkingDirectory("/app")
@@ -56,7 +60,7 @@ public void setUp() throws IOException, URISyntaxException {
}
@AfterEach
- public void tearDown() throws IOException {
+ void tearDown() throws IOException {
prometheusContainer.stop();
pushGatewayContainer.stop();
sampleAppContainer.stop();
@@ -66,7 +70,7 @@ public void tearDown() throws IOException {
final OkHttpClient client = new OkHttpClient();
@Test
- public void testSimple() throws IOException, InterruptedException {
+ void testSimple() throws IOException, InterruptedException {
pushGatewayContainer.start();
sampleAppContainer
.withCommand(
@@ -86,7 +90,7 @@ public void testSimple() throws IOException, InterruptedException {
}
@Test
- public void testTextFormat() throws IOException, InterruptedException {
+ void testTextFormat() throws IOException, InterruptedException {
pushGatewayContainer.start();
sampleAppContainer
.withCommand(
@@ -106,7 +110,7 @@ public void testTextFormat() throws IOException, InterruptedException {
}
@Test
- public void testBasicAuth() throws IOException, InterruptedException {
+ void testBasicAuth() throws IOException, InterruptedException {
pushGatewayContainer
.withCopyFileToContainer(
MountableFile.forClasspathResource("/pushgateway-basicauth.yaml"),
@@ -131,7 +135,7 @@ public void testBasicAuth() throws IOException, InterruptedException {
}
@Test
- public void testSsl() throws InterruptedException, IOException {
+ void testSsl() throws InterruptedException, IOException {
pushGatewayContainer
.withCopyFileToContainer(
MountableFile.forClasspathResource("/pushgateway-ssl.yaml"),
@@ -156,7 +160,7 @@ public void testSsl() throws InterruptedException, IOException {
}
@Test
- public void testProtobuf() throws IOException, InterruptedException {
+ void testProtobuf() throws IOException, InterruptedException {
pushGatewayContainer.start();
sampleAppContainer
.withCommand(
diff --git a/integration-tests/it-spring-boot-smoke-test/pom.xml b/integration-tests/it-spring-boot-smoke-test/pom.xml
index 4189a8416..0431364e2 100644
--- a/integration-tests/it-spring-boot-smoke-test/pom.xml
+++ b/integration-tests/it-spring-boot-smoke-test/pom.xml
@@ -1,28 +1,26 @@
-
+
4.0.0
org.springframework.boot
spring-boot-starter-parent
- 3.5.4
+ 4.1.1
io.prometheus
it-spring-boot-smoke-test
- 1.4.0-SNAPSHOT
+ 1.8.1-SNAPSHOT
Integration Test - Spring Smoke Tests
Spring Smoke Tests
- 17
- 5.13.4
+ 25
+ 6.1.3
@@ -89,75 +87,19 @@
-
- org.graalvm.buildtools
- native-maven-plugin
-
-
-
-
- --initialize-at-build-time=org.junit.jupiter.api.DisplayNameGenerator$IndicativeSentences
-
-
- --initialize-at-build-time=org.junit.jupiter.engine.descriptor.ClassBasedTestDescriptor$ClassInfo
-
-
- --initialize-at-build-time=org.junit.jupiter.engine.descriptor.ClassBasedTestDescriptor$LifecycleMethods
-
-
- --initialize-at-build-time=org.junit.jupiter.engine.descriptor.ClassTemplateInvocationTestDescriptor
-
-
- --initialize-at-build-time=org.junit.jupiter.engine.descriptor.ClassTemplateTestDescriptor
-
-
- --initialize-at-build-time=org.junit.jupiter.engine.descriptor.DynamicDescendantFilter$Mode
-
-
- --initialize-at-build-time=org.junit.jupiter.engine.descriptor.ExclusiveResourceCollector$1
-
-
- --initialize-at-build-time=org.junit.jupiter.engine.descriptor.MethodBasedTestDescriptor$MethodInfo
-
-
- --initialize-at-build-time=org.junit.jupiter.engine.discovery.ClassSelectorResolver$DummyClassTemplateInvocationContext
-
-
- --initialize-at-build-time=org.junit.platform.engine.support.store.NamespacedHierarchicalStore$EvaluatedValue
-
- --initialize-at-build-time=org.junit.platform.launcher.core.DiscoveryIssueNotifier
-
-
- --initialize-at-build-time=org.junit.platform.launcher.core.HierarchicalOutputDirectoryProvider
-
-
- --initialize-at-build-time=org.junit.platform.launcher.core.LauncherDiscoveryResult$EngineResultInfo
-
-
- --initialize-at-build-time=org.junit.platform.suite.engine.SuiteTestDescriptor$LifecycleMethods
-
-
-
-
-
org.springframework.boot
spring-boot-maven-plugin
+
- com.diffplug.spotless
- spotless-maven-plugin
- 2.46.1
-
-
-
-
-
+ org.apache.maven.plugins
+ maven-failsafe-plugin
- verify
- check
+ integration-test
+ verify
@@ -165,4 +107,83 @@
+
+
+ java17-plus
+
+ [17,)
+
+
+
+
+ org.graalvm.buildtools
+ native-maven-plugin
+
+
+
+
+ --initialize-at-build-time=org.junit.jupiter.api.DisplayNameGenerator$IndicativeSentences
+
+
+ --initialize-at-build-time=org.junit.jupiter.engine.descriptor.ClassBasedTestDescriptor$ClassInfo
+
+
+ --initialize-at-build-time=org.junit.jupiter.engine.descriptor.ClassBasedTestDescriptor$LifecycleMethods
+
+
+ --initialize-at-build-time=org.junit.jupiter.engine.descriptor.ClassTemplateInvocationTestDescriptor
+
+
+ --initialize-at-build-time=org.junit.jupiter.engine.descriptor.ClassTemplateTestDescriptor
+
+
+ --initialize-at-build-time=org.junit.jupiter.engine.descriptor.DynamicDescendantFilter$Mode
+
+
+ --initialize-at-build-time=org.junit.jupiter.engine.descriptor.ExclusiveResourceCollector$1
+
+
+ --initialize-at-build-time=org.junit.jupiter.engine.descriptor.MethodBasedTestDescriptor$MethodInfo
+
+
+ --initialize-at-build-time=org.junit.jupiter.engine.discovery.ClassSelectorResolver$DummyClassTemplateInvocationContext
+
+
+ --initialize-at-build-time=org.junit.platform.engine.support.store.NamespacedHierarchicalStore$EvaluatedValue
+
+ --initialize-at-build-time=org.junit.platform.launcher.core.DiscoveryIssueNotifier
+
+
+ --initialize-at-build-time=org.junit.platform.launcher.core.HierarchicalOutputDirectoryProvider
+
+
+ --initialize-at-build-time=org.junit.platform.launcher.core.LauncherDiscoveryResult$EngineResultInfo
+
+
+ --initialize-at-build-time=org.junit.platform.suite.engine.SuiteTestDescriptor$LifecycleMethods
+
+
+ --initialize-at-build-time=org.junit.platform.commons.logging.LoggerFactory$DelegatingLogger
+
+
+ --initialize-at-build-time=org.junit.jupiter.engine.execution.ConditionEvaluator
+
+
+ --initialize-at-build-time=org.junit.jupiter.engine.execution.InterceptingExecutableInvoker
+
+
+ --initialize-at-build-time=org.junit.jupiter.api.extension.ConditionEvaluationResult
+
+
+ --initialize-at-build-time=org.junit.jupiter.engine.execution.InvocationInterceptorChain
+
+
+
+
+
+
+
+
+
+
diff --git a/integration-tests/it-spring-boot-smoke-test/src/test/java/io/prometheus/metrics/it/springboot/ApplicationIT.java b/integration-tests/it-spring-boot-smoke-test/src/test/java/io/prometheus/metrics/it/springboot/ApplicationIT.java
new file mode 100644
index 000000000..43f389482
--- /dev/null
+++ b/integration-tests/it-spring-boot-smoke-test/src/test/java/io/prometheus/metrics/it/springboot/ApplicationIT.java
@@ -0,0 +1,74 @@
+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.config.EscapingScheme;
+import io.prometheus.metrics.expositionformats.generated.Metrics;
+import io.prometheus.metrics.expositionformats.internal.PrometheusProtobufWriterImpl;
+import io.prometheus.metrics.model.snapshots.HistogramSnapshot;
+import io.prometheus.metrics.model.snapshots.MetricSnapshots;
+import io.prometheus.metrics.model.snapshots.NativeHistogramBuckets;
+import java.io.IOException;
+import java.net.URI;
+import java.util.List;
+import java.util.Optional;
+import org.junit.jupiter.api.Test;
+import org.springframework.boot.test.context.SpringBootTest;
+
+@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)
+class ApplicationIT {
+ @Test
+ void testUsesShadedProtobufRuntime() {
+ assertThat(Metrics.MetricFamily.class.getSuperclass().getName())
+ .startsWith("io.prometheus.metrics.shaded.com_google_protobuf_");
+ }
+
+ @Test
+ void testPrometheusProtobufFormat() throws IOException {
+ ExporterTest.Response response =
+ ExporterTest.scrape(
+ "GET",
+ URI.create("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();
+ }
+
+ @Test
+ void testPrometheusProtobufDebugFormat() throws IOException {
+ HistogramSnapshot histogram =
+ HistogramSnapshot.builder()
+ .name("native_debug_repro_seconds")
+ .help("native debug repro")
+ .dataPoint(
+ HistogramSnapshot.HistogramDataPointSnapshot.builder()
+ .sum(0.123)
+ .nativeSchema(5)
+ .nativeZeroThreshold(2.938735877055719E-39)
+ .nativeZeroCount(0)
+ .nativeBucketsForPositiveValues(
+ NativeHistogramBuckets.builder().bucket(-96, 1).build())
+ .build())
+ .build();
+
+ String debugString =
+ new PrometheusProtobufWriterImpl()
+ .toDebugString(MetricSnapshots.of(histogram), EscapingScheme.UNDERSCORE_ESCAPING);
+
+ assertThat(debugString)
+ .contains(
+ "name: \"native_debug_repro_seconds\"",
+ "type: HISTOGRAM",
+ "schema: 5",
+ "positive_span");
+ }
+}
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
deleted file mode 100644
index 26f555847..000000000
--- a/integration-tests/it-spring-boot-smoke-test/src/test/java/io/prometheus/metrics/it/springboot/ApplicationTest.java
+++ /dev/null
@@ -1,36 +0,0 @@
-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_31_1.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/it-spring-boot-smoke-test/src/test/resources/META-INF/native-image/io.prometheus/it-spring-boot-smoke-test/serialization-config.json b/integration-tests/it-spring-boot-smoke-test/src/test/resources/META-INF/native-image/io.prometheus/it-spring-boot-smoke-test/serialization-config.json
new file mode 100644
index 000000000..eccc08a42
--- /dev/null
+++ b/integration-tests/it-spring-boot-smoke-test/src/test/resources/META-INF/native-image/io.prometheus/it-spring-boot-smoke-test/serialization-config.json
@@ -0,0 +1,5 @@
+[
+ {
+ "name": "org.junit.platform.engine.UniqueId$SerializedForm"
+ }
+]
diff --git a/integration-tests/pom.xml b/integration-tests/pom.xml
index bca697c1e..c3f299d69 100644
--- a/integration-tests/pom.xml
+++ b/integration-tests/pom.xml
@@ -1,12 +1,11 @@
-
+
4.0.0
io.prometheus
client_java
- 1.4.0-SNAPSHOT
+ 1.8.1-SNAPSHOT
integration-tests
@@ -19,6 +18,7 @@
true
+ true
@@ -51,12 +51,12 @@
commons-io
commons-io
- 2.20.0
+ 2.22.0
org.testcontainers
junit-jupiter
- 1.21.3
+ 1.21.4
test
diff --git a/lychee.toml b/lychee.toml
deleted file mode 100644
index 599f2de7a..000000000
--- a/lychee.toml
+++ /dev/null
@@ -1,15 +0,0 @@
-max_retries = 6
-exclude_loopback = true
-cache = true
-
-base = "https://prometheus.github.io"
-exclude_path = ["docs/themes"]
-exclude = [
- '^https://github\.com/prometheus/client_java/settings',
- '#',
- 'CONTRIBUTING.md',
- 'LICENSE',
- 'MAINTAINERS.md'
-]
-
-
diff --git a/mise.lock b/mise.lock
new file mode 100644
index 000000000..fb1773f67
--- /dev/null
+++ b/mise.lock
@@ -0,0 +1,884 @@
+# @generated - this file is auto-generated by `mise lock` https://mise.en.dev/dev-tools/mise-lock.html
+
+[[tools.actionlint]]
+version = "1.7.12"
+backend = "aqua:rhysd/actionlint"
+
+[tools.actionlint."platforms.linux-arm64"]
+checksum = "sha256:325e971b6ba9bfa504672e29be93c24981eeb1c07576d730e9f7c8805afff0c6"
+url = "https://github.com/rhysd/actionlint/releases/download/v1.7.12/actionlint_1.7.12_linux_arm64.tar.gz"
+url_api = "https://api.github.com/repos/rhysd/actionlint/releases/assets/384924897"
+provenance = "github-attestations"
+
+[tools.actionlint."platforms.linux-arm64-musl"]
+checksum = "sha256:325e971b6ba9bfa504672e29be93c24981eeb1c07576d730e9f7c8805afff0c6"
+url = "https://github.com/rhysd/actionlint/releases/download/v1.7.12/actionlint_1.7.12_linux_arm64.tar.gz"
+url_api = "https://api.github.com/repos/rhysd/actionlint/releases/assets/384924897"
+provenance = "github-attestations"
+
+[tools.actionlint."platforms.linux-x64"]
+checksum = "sha256:8aca8db96f1b94770f1b0d72b6dddcb1ebb8123cb3712530b08cc387b349a3d8"
+url = "https://github.com/rhysd/actionlint/releases/download/v1.7.12/actionlint_1.7.12_linux_amd64.tar.gz"
+url_api = "https://api.github.com/repos/rhysd/actionlint/releases/assets/384924896"
+provenance = "github-attestations"
+
+[tools.actionlint."platforms.linux-x64-musl"]
+checksum = "sha256:8aca8db96f1b94770f1b0d72b6dddcb1ebb8123cb3712530b08cc387b349a3d8"
+url = "https://github.com/rhysd/actionlint/releases/download/v1.7.12/actionlint_1.7.12_linux_amd64.tar.gz"
+url_api = "https://api.github.com/repos/rhysd/actionlint/releases/assets/384924896"
+provenance = "github-attestations"
+
+[tools.actionlint."platforms.macos-arm64"]
+checksum = "sha256:aba9ced2dee8d27fecca3dc7feb1a7f9a52caefa1eb46f3271ea66b6e0e6953f"
+url = "https://github.com/rhysd/actionlint/releases/download/v1.7.12/actionlint_1.7.12_darwin_arm64.tar.gz"
+url_api = "https://api.github.com/repos/rhysd/actionlint/releases/assets/384924893"
+provenance = "github-attestations"
+
+[tools.actionlint."platforms.macos-x64"]
+checksum = "sha256:5b44c3bc2255115c9b69e30efc0fecdf498fdb63c5d58e17084fd5f16324c644"
+url = "https://github.com/rhysd/actionlint/releases/download/v1.7.12/actionlint_1.7.12_darwin_amd64.tar.gz"
+url_api = "https://api.github.com/repos/rhysd/actionlint/releases/assets/384924880"
+provenance = "github-attestations"
+
+[tools.actionlint."platforms.windows-x64"]
+checksum = "sha256:6e7241b51e6817ea6a047693d8e6fed13b31819c9a0dd6c5a726e1592d22f6e9"
+url = "https://github.com/rhysd/actionlint/releases/download/v1.7.12/actionlint_1.7.12_windows_amd64.zip"
+url_api = "https://api.github.com/repos/rhysd/actionlint/releases/assets/384924919"
+provenance = "github-attestations"
+
+[[tools."aqua:grafana/flint"]]
+version = "0.22.10"
+backend = "aqua:grafana/flint"
+
+[tools."aqua:grafana/flint"."platforms.linux-arm64"]
+checksum = "sha256:6dec82cb6486b7e645e0b1674493497191d92a8e28c5c79194e5553882d213de"
+url = "https://github.com/grafana/flint/releases/download/v0.22.10/flint-aarch64-unknown-linux-gnu.tar.gz"
+url_api = "https://api.github.com/repos/grafana/flint/releases/assets/491750275"
+provenance = "github-attestations"
+
+[tools."aqua:grafana/flint"."platforms.linux-arm64-musl"]
+checksum = "sha256:373060c08a4cdd905d6e8e83f62ef43da8778d9133cb66b7801f392587812e8f"
+url = "https://github.com/grafana/flint/releases/download/v0.22.10/flint-aarch64-unknown-linux-musl.tar.gz"
+url_api = "https://api.github.com/repos/grafana/flint/releases/assets/491750193"
+provenance = "github-attestations"
+
+[tools."aqua:grafana/flint"."platforms.linux-x64"]
+checksum = "sha256:a9f7f3768ec02cdd082c12cea0e815fc142d3ee721f4d8485f4ba9fc2c7d0521"
+url = "https://github.com/grafana/flint/releases/download/v0.22.10/flint-x86_64-unknown-linux-gnu.tar.gz"
+url_api = "https://api.github.com/repos/grafana/flint/releases/assets/491749844"
+provenance = "github-attestations"
+
+[tools."aqua:grafana/flint"."platforms.linux-x64-musl"]
+checksum = "sha256:3b174b31f10b1ded2cf0a54e8b62e5526713a49816c1fd74b00073519044f7bb"
+url = "https://github.com/grafana/flint/releases/download/v0.22.10/flint-x86_64-unknown-linux-musl.tar.gz"
+url_api = "https://api.github.com/repos/grafana/flint/releases/assets/491750257"
+provenance = "github-attestations"
+
+[tools."aqua:grafana/flint"."platforms.macos-arm64"]
+checksum = "sha256:f9aa1a95aa5953f7f5a64c0d72f86e634c4930e9cb73422a95e342c6cb9be5d5"
+url = "https://github.com/grafana/flint/releases/download/v0.22.10/flint-aarch64-apple-darwin.tar.gz"
+url_api = "https://api.github.com/repos/grafana/flint/releases/assets/491750245"
+provenance = "github-attestations"
+
+[tools."aqua:grafana/flint"."platforms.macos-x64"]
+checksum = "sha256:b55731f1b74196afea675bdbd6ebbbd94171b4e9d31f79ecee36372bdf8818cf"
+url = "https://github.com/grafana/flint/releases/download/v0.22.10/flint-x86_64-apple-darwin.tar.gz"
+url_api = "https://api.github.com/repos/grafana/flint/releases/assets/491752660"
+provenance = "github-attestations"
+
+[tools."aqua:grafana/flint"."platforms.windows-x64"]
+checksum = "sha256:494f16ad4d2d8eae137438832e544675d196a2887086ab8070a5a346d70650be"
+url = "https://github.com/grafana/flint/releases/download/v0.22.10/flint-x86_64-pc-windows-msvc.zip"
+url_api = "https://api.github.com/repos/grafana/flint/releases/assets/491751016"
+provenance = "github-attestations"
+
+[[tools."aqua:grafana/gcx"]]
+version = "v1.1.0"
+backend = "aqua:grafana/gcx"
+
+[tools."aqua:grafana/gcx"."platforms.linux-arm64"]
+checksum = "sha256:4d39b70a5691045b7e381fd37976b75988926931f6cf184698444099099692e1"
+url = "https://github.com/grafana/gcx/releases/download/v1.1.0/gcx_1.1.0_linux_arm64.tar.gz"
+url_api = "https://api.github.com/repos/grafana/gcx/releases/assets/514303325"
+
+[tools."aqua:grafana/gcx"."platforms.linux-arm64-musl"]
+checksum = "sha256:4d39b70a5691045b7e381fd37976b75988926931f6cf184698444099099692e1"
+url = "https://github.com/grafana/gcx/releases/download/v1.1.0/gcx_1.1.0_linux_arm64.tar.gz"
+url_api = "https://api.github.com/repos/grafana/gcx/releases/assets/514303325"
+
+[tools."aqua:grafana/gcx"."platforms.linux-x64"]
+checksum = "sha256:0c7867e99f5786b7e1b5ea449cf540c14b67f40c1c92c0f93031eaa2e0c38df9"
+url = "https://github.com/grafana/gcx/releases/download/v1.1.0/gcx_1.1.0_linux_amd64.tar.gz"
+url_api = "https://api.github.com/repos/grafana/gcx/releases/assets/514303327"
+
+[tools."aqua:grafana/gcx"."platforms.linux-x64-musl"]
+checksum = "sha256:0c7867e99f5786b7e1b5ea449cf540c14b67f40c1c92c0f93031eaa2e0c38df9"
+url = "https://github.com/grafana/gcx/releases/download/v1.1.0/gcx_1.1.0_linux_amd64.tar.gz"
+url_api = "https://api.github.com/repos/grafana/gcx/releases/assets/514303327"
+
+[tools."aqua:grafana/gcx"."platforms.macos-arm64"]
+checksum = "sha256:a1e1e62b54700d829ece3cc4a013570e0eff3c872e8c11991866ed58a6701709"
+url = "https://github.com/grafana/gcx/releases/download/v1.1.0/gcx_1.1.0_darwin_arm64.tar.gz"
+url_api = "https://api.github.com/repos/grafana/gcx/releases/assets/514303326"
+
+[tools."aqua:grafana/gcx"."platforms.macos-x64"]
+checksum = "sha256:94324fd3d052eee604df58af41ef125ac5cfc6d878fcb615b34012e21a6bc7f5"
+url = "https://github.com/grafana/gcx/releases/download/v1.1.0/gcx_1.1.0_darwin_amd64.tar.gz"
+url_api = "https://api.github.com/repos/grafana/gcx/releases/assets/514303329"
+
+[tools."aqua:grafana/gcx"."platforms.windows-x64"]
+checksum = "sha256:21039e34896c0bafaf044cee0de7545d3ccda2f05ed0f13404685b1e09fd3cb0"
+url = "https://github.com/grafana/gcx/releases/download/v1.1.0/gcx_1.1.0_windows_amd64.zip"
+url_api = "https://api.github.com/repos/grafana/gcx/releases/assets/514303368"
+
+[[tools."aqua:grafana/oats"]]
+version = "0.10.0"
+backend = "aqua:grafana/oats"
+
+[tools."aqua:grafana/oats"."platforms.linux-arm64"]
+checksum = "sha256:be195aeeafce644c8a59438bbc8c472f5ca51df6f9570c6b646373a703d686b0"
+url = "https://github.com/grafana/oats/releases/download/v0.10.0/oats_0.10.0_linux_arm64.tar.gz"
+url_api = "https://api.github.com/repos/grafana/oats/releases/assets/493772122"
+provenance = "github-attestations"
+
+[tools."aqua:grafana/oats"."platforms.linux-arm64-musl"]
+checksum = "sha256:be195aeeafce644c8a59438bbc8c472f5ca51df6f9570c6b646373a703d686b0"
+url = "https://github.com/grafana/oats/releases/download/v0.10.0/oats_0.10.0_linux_arm64.tar.gz"
+url_api = "https://api.github.com/repos/grafana/oats/releases/assets/493772122"
+provenance = "github-attestations"
+
+[tools."aqua:grafana/oats"."platforms.linux-x64"]
+checksum = "sha256:b72a7a587148d1eaa15ccea3ce1adfd67e040d1cd9f9e267c850154035ef9a18"
+url = "https://github.com/grafana/oats/releases/download/v0.10.0/oats_0.10.0_linux_amd64.tar.gz"
+url_api = "https://api.github.com/repos/grafana/oats/releases/assets/493772131"
+provenance = "github-attestations"
+
+[tools."aqua:grafana/oats"."platforms.linux-x64-musl"]
+checksum = "sha256:b72a7a587148d1eaa15ccea3ce1adfd67e040d1cd9f9e267c850154035ef9a18"
+url = "https://github.com/grafana/oats/releases/download/v0.10.0/oats_0.10.0_linux_amd64.tar.gz"
+url_api = "https://api.github.com/repos/grafana/oats/releases/assets/493772131"
+provenance = "github-attestations"
+
+[tools."aqua:grafana/oats"."platforms.macos-arm64"]
+checksum = "sha256:570d5c3b43c0cbe0d88c527d2fcecb9ee565e2f9d1a4d49fb4b6bbf5c3fa47e5"
+url = "https://github.com/grafana/oats/releases/download/v0.10.0/oats_0.10.0_darwin_arm64.tar.gz"
+url_api = "https://api.github.com/repos/grafana/oats/releases/assets/493772124"
+provenance = "github-attestations"
+
+[tools."aqua:grafana/oats"."platforms.macos-x64"]
+checksum = "sha256:c3579f73928df56d1cf6d6bbb90d20424649ff08c228daa4810914017f8620d8"
+url = "https://github.com/grafana/oats/releases/download/v0.10.0/oats_0.10.0_darwin_amd64.tar.gz"
+url_api = "https://api.github.com/repos/grafana/oats/releases/assets/493772123"
+provenance = "github-attestations"
+
+[tools."aqua:grafana/oats"."platforms.windows-x64"]
+checksum = "sha256:b52112bf0932ea8dea1fb051bc04e29efa5ef138f67d09d775c4eb716d3d92f9"
+url = "https://github.com/grafana/oats/releases/download/v0.10.0/oats_0.10.0_windows_amd64.zip"
+url_api = "https://api.github.com/repos/grafana/oats/releases/assets/493772125"
+provenance = "github-attestations"
+
+[[tools."aqua:jonwiggins/xmloxide"]]
+version = "v0.5.0"
+backend = "aqua:jonwiggins/xmloxide"
+
+[tools."aqua:jonwiggins/xmloxide"."platforms.linux-arm64"]
+checksum = "sha256:554d4ae976782bed0d9fa44b9ee7106a756f6a599ef9867b412db47a2a664399"
+url = "https://github.com/jonwiggins/xmloxide/releases/download/v0.5.0/xmllint_linux-aarch64"
+url_api = "https://api.github.com/repos/jonwiggins/xmloxide/releases/assets/506799877"
+
+[tools."aqua:jonwiggins/xmloxide"."platforms.linux-arm64-musl"]
+checksum = "sha256:554d4ae976782bed0d9fa44b9ee7106a756f6a599ef9867b412db47a2a664399"
+url = "https://github.com/jonwiggins/xmloxide/releases/download/v0.5.0/xmllint_linux-aarch64"
+url_api = "https://api.github.com/repos/jonwiggins/xmloxide/releases/assets/506799877"
+
+[tools."aqua:jonwiggins/xmloxide"."platforms.linux-x64"]
+checksum = "sha256:4dd8c45676bb34bd397db97179231f4e6f77caff35059f18de9d1d4b68773d55"
+url = "https://github.com/jonwiggins/xmloxide/releases/download/v0.5.0/xmllint_linux-x86-64"
+url_api = "https://api.github.com/repos/jonwiggins/xmloxide/releases/assets/506799872"
+
+[tools."aqua:jonwiggins/xmloxide"."platforms.linux-x64-musl"]
+checksum = "sha256:4dd8c45676bb34bd397db97179231f4e6f77caff35059f18de9d1d4b68773d55"
+url = "https://github.com/jonwiggins/xmloxide/releases/download/v0.5.0/xmllint_linux-x86-64"
+url_api = "https://api.github.com/repos/jonwiggins/xmloxide/releases/assets/506799872"
+
+[tools."aqua:jonwiggins/xmloxide"."platforms.macos-arm64"]
+checksum = "sha256:02a44c8cab86cd2d8765fcf0dc7e7610768571a0cf0af3785fb691196d1eebe5"
+url = "https://github.com/jonwiggins/xmloxide/releases/download/v0.5.0/xmllint_darwin-aarch64"
+url_api = "https://api.github.com/repos/jonwiggins/xmloxide/releases/assets/506799873"
+
+[tools."aqua:jonwiggins/xmloxide"."platforms.macos-x64"]
+checksum = "sha256:6876c1f1ec9dd31bc80a6417548fa337898e78e50ac0479ed391f3bd26e0b4f3"
+url = "https://github.com/jonwiggins/xmloxide/releases/download/v0.5.0/xmllint_darwin-x86-64"
+url_api = "https://api.github.com/repos/jonwiggins/xmloxide/releases/assets/506799874"
+
+[tools."aqua:jonwiggins/xmloxide"."platforms.windows-x64"]
+checksum = "sha256:ecf509be8f9de6cb86921c88a516a9fac4a920f0905c18f62d91a4ed41af920f"
+url = "https://github.com/jonwiggins/xmloxide/releases/download/v0.5.0/xmllint_windows-x86-64.exe"
+url_api = "https://api.github.com/repos/jonwiggins/xmloxide/releases/assets/506799871"
+
+[[tools."aqua:owenlamont/ryl"]]
+version = "0.21.0"
+backend = "aqua:owenlamont/ryl"
+
+[tools."aqua:owenlamont/ryl"."platforms.linux-arm64"]
+checksum = "sha256:134dfccd5dd7fd13092dbaf482e14e1e9c2b238ecf4306ac020b036212f85abc"
+url = "https://github.com/owenlamont/ryl/releases/download/v0.21.0/ryl-aarch64-unknown-linux-musl.tar.gz"
+url_api = "https://api.github.com/repos/owenlamont/ryl/releases/assets/454375977"
+provenance = "github-attestations"
+
+[tools."aqua:owenlamont/ryl"."platforms.linux-arm64-musl"]
+checksum = "sha256:134dfccd5dd7fd13092dbaf482e14e1e9c2b238ecf4306ac020b036212f85abc"
+url = "https://github.com/owenlamont/ryl/releases/download/v0.21.0/ryl-aarch64-unknown-linux-musl.tar.gz"
+url_api = "https://api.github.com/repos/owenlamont/ryl/releases/assets/454375977"
+provenance = "github-attestations"
+
+[tools."aqua:owenlamont/ryl"."platforms.linux-x64"]
+checksum = "sha256:36867148c4e7415b5f8dd24a1c4e3212e8ec016a6a16d1d1e0d16d7c2f641214"
+url = "https://github.com/owenlamont/ryl/releases/download/v0.21.0/ryl-x86_64-unknown-linux-musl.tar.gz"
+url_api = "https://api.github.com/repos/owenlamont/ryl/releases/assets/454375995"
+provenance = "github-attestations"
+
+[tools."aqua:owenlamont/ryl"."platforms.linux-x64-musl"]
+checksum = "sha256:36867148c4e7415b5f8dd24a1c4e3212e8ec016a6a16d1d1e0d16d7c2f641214"
+url = "https://github.com/owenlamont/ryl/releases/download/v0.21.0/ryl-x86_64-unknown-linux-musl.tar.gz"
+url_api = "https://api.github.com/repos/owenlamont/ryl/releases/assets/454375995"
+provenance = "github-attestations"
+
+[tools."aqua:owenlamont/ryl"."platforms.macos-arm64"]
+checksum = "sha256:ed5e23a5634f138aac5c2c15e72975f0f485f694582ef6874625e38460b0c3b4"
+url = "https://github.com/owenlamont/ryl/releases/download/v0.21.0/ryl-aarch64-apple-darwin.tar.gz"
+url_api = "https://api.github.com/repos/owenlamont/ryl/releases/assets/454375976"
+provenance = "github-attestations"
+
+[tools."aqua:owenlamont/ryl"."platforms.windows-x64"]
+checksum = "sha256:7522a1fde12775ad43b74e154a6abe3affb676bb52d719ac9e7eb6980bda856d"
+url = "https://github.com/owenlamont/ryl/releases/download/v0.21.0/ryl-x86_64-pc-windows-msvc.zip"
+url_api = "https://api.github.com/repos/owenlamont/ryl/releases/assets/454375990"
+provenance = "github-attestations"
+
+[[tools.biome]]
+version = "2.5.8"
+backend = "aqua:biomejs/biome"
+
+[tools.biome."platforms.linux-arm64"]
+checksum = "sha256:3f2be9f1f68dca8e0b96d2a9212b408f15dc2668203b2c42cfb78a2894cc966b"
+url = "https://github.com/biomejs/biome/releases/download/%40biomejs/biome%402.5.8/biome-linux-arm64"
+url_api = "https://api.github.com/repos/biomejs/biome/releases/assets/509899232"
+provenance = "github-attestations"
+
+[tools.biome."platforms.linux-arm64-musl"]
+checksum = "sha256:09d02b9839aa1bf97b65fcce39b561c5687f6521607db46e4bcffdd99a0175a5"
+url = "https://github.com/biomejs/biome/releases/download/%40biomejs/biome%402.5.8/biome-linux-arm64-musl"
+url_api = "https://api.github.com/repos/biomejs/biome/releases/assets/509899235"
+provenance = "github-attestations"
+
+[tools.biome."platforms.linux-x64"]
+checksum = "sha256:17abac7ef72e7a1aaccd89892f7e2e62c9919d27473defe772be04ad78400ac2"
+url = "https://github.com/biomejs/biome/releases/download/%40biomejs/biome%402.5.8/biome-linux-x64"
+url_api = "https://api.github.com/repos/biomejs/biome/releases/assets/509899231"
+provenance = "github-attestations"
+
+[tools.biome."platforms.linux-x64-musl"]
+checksum = "sha256:365f2aac71051842d65a9d03139dc0f189628a0e8c2dc3aa2ef09a51ef94bcbd"
+url = "https://github.com/biomejs/biome/releases/download/%40biomejs/biome%402.5.8/biome-linux-x64-musl"
+url_api = "https://api.github.com/repos/biomejs/biome/releases/assets/509899233"
+provenance = "github-attestations"
+
+[tools.biome."platforms.macos-arm64"]
+checksum = "sha256:04c2d44e61242c8bf56e2d663e5312e78639334b4a2fef84b5f59ac8bc9579fb"
+url = "https://github.com/biomejs/biome/releases/download/%40biomejs/biome%402.5.8/biome-darwin-arm64"
+url_api = "https://api.github.com/repos/biomejs/biome/releases/assets/509899230"
+provenance = "github-attestations"
+
+[tools.biome."platforms.macos-x64"]
+checksum = "sha256:6bfc751bfff3429888c527d2bb1fe632808769d2f5849579ed8cf71e5fe25c8b"
+url = "https://github.com/biomejs/biome/releases/download/%40biomejs/biome%402.5.8/biome-darwin-x64"
+url_api = "https://api.github.com/repos/biomejs/biome/releases/assets/509899236"
+provenance = "github-attestations"
+
+[tools.biome."platforms.windows-x64"]
+checksum = "sha256:80ac9293d359f55e5f75e1dec13c2ec9c28f01e7e8a8e409b0407a1498da006f"
+url = "https://github.com/biomejs/biome/releases/download/%40biomejs/biome%402.5.8/biome-win32-x64.exe"
+url_api = "https://api.github.com/repos/biomejs/biome/releases/assets/509899229"
+provenance = "github-attestations"
+
+[[tools.checkstyle]]
+version = "13.8.0"
+backend = "aqua:checkstyle/checkstyle"
+
+[tools.checkstyle."platforms.linux-arm64"]
+checksum = "sha256:cb0b1083eb5d3f9f0b7f0edd9a86d8dbe406bf44da73c3df869abace8cb8502c"
+url = "https://github.com/checkstyle/checkstyle/releases/download/checkstyle-13.8.0/checkstyle-13.8.0-all.jar"
+url_api = "https://api.github.com/repos/checkstyle/checkstyle/releases/assets/474019260"
+
+[tools.checkstyle."platforms.linux-arm64-musl"]
+checksum = "sha256:cb0b1083eb5d3f9f0b7f0edd9a86d8dbe406bf44da73c3df869abace8cb8502c"
+url = "https://github.com/checkstyle/checkstyle/releases/download/checkstyle-13.8.0/checkstyle-13.8.0-all.jar"
+url_api = "https://api.github.com/repos/checkstyle/checkstyle/releases/assets/474019260"
+
+[tools.checkstyle."platforms.linux-x64"]
+checksum = "sha256:cb0b1083eb5d3f9f0b7f0edd9a86d8dbe406bf44da73c3df869abace8cb8502c"
+url = "https://github.com/checkstyle/checkstyle/releases/download/checkstyle-13.8.0/checkstyle-13.8.0-all.jar"
+url_api = "https://api.github.com/repos/checkstyle/checkstyle/releases/assets/474019260"
+
+[tools.checkstyle."platforms.linux-x64-musl"]
+checksum = "sha256:cb0b1083eb5d3f9f0b7f0edd9a86d8dbe406bf44da73c3df869abace8cb8502c"
+url = "https://github.com/checkstyle/checkstyle/releases/download/checkstyle-13.8.0/checkstyle-13.8.0-all.jar"
+url_api = "https://api.github.com/repos/checkstyle/checkstyle/releases/assets/474019260"
+
+[tools.checkstyle."platforms.macos-arm64"]
+checksum = "sha256:cb0b1083eb5d3f9f0b7f0edd9a86d8dbe406bf44da73c3df869abace8cb8502c"
+url = "https://github.com/checkstyle/checkstyle/releases/download/checkstyle-13.8.0/checkstyle-13.8.0-all.jar"
+url_api = "https://api.github.com/repos/checkstyle/checkstyle/releases/assets/474019260"
+
+[tools.checkstyle."platforms.macos-x64"]
+checksum = "sha256:cb0b1083eb5d3f9f0b7f0edd9a86d8dbe406bf44da73c3df869abace8cb8502c"
+url = "https://github.com/checkstyle/checkstyle/releases/download/checkstyle-13.8.0/checkstyle-13.8.0-all.jar"
+url_api = "https://api.github.com/repos/checkstyle/checkstyle/releases/assets/474019260"
+
+[tools.checkstyle."platforms.windows-x64"]
+checksum = "sha256:cb0b1083eb5d3f9f0b7f0edd9a86d8dbe406bf44da73c3df869abace8cb8502c"
+url = "https://github.com/checkstyle/checkstyle/releases/download/checkstyle-13.8.0/checkstyle-13.8.0-all.jar"
+url_api = "https://api.github.com/repos/checkstyle/checkstyle/releases/assets/474019260"
+
+[[tools.editorconfig-checker]]
+version = "3.11.1"
+backend = "aqua:editorconfig-checker/editorconfig-checker"
+
+[tools.editorconfig-checker."platforms.linux-arm64"]
+checksum = "sha256:073d5263f0c5953f3e847df44a84403ecc284ab77419de56e280ab92bc082e8d"
+url = "https://github.com/editorconfig-checker/editorconfig-checker/releases/download/v3.11.1/ec-linux-arm64.tar.gz"
+url_api = "https://api.github.com/repos/editorconfig-checker/editorconfig-checker/releases/assets/506306098"
+
+[tools.editorconfig-checker."platforms.linux-arm64-musl"]
+checksum = "sha256:073d5263f0c5953f3e847df44a84403ecc284ab77419de56e280ab92bc082e8d"
+url = "https://github.com/editorconfig-checker/editorconfig-checker/releases/download/v3.11.1/ec-linux-arm64.tar.gz"
+url_api = "https://api.github.com/repos/editorconfig-checker/editorconfig-checker/releases/assets/506306098"
+
+[tools.editorconfig-checker."platforms.linux-x64"]
+checksum = "sha256:5a37922963248451e88149251e49f6ae08f69717a3918202a51fe9945e19691e"
+url = "https://github.com/editorconfig-checker/editorconfig-checker/releases/download/v3.11.1/ec-linux-amd64.tar.gz"
+url_api = "https://api.github.com/repos/editorconfig-checker/editorconfig-checker/releases/assets/506306099"
+
+[tools.editorconfig-checker."platforms.linux-x64-musl"]
+checksum = "sha256:5a37922963248451e88149251e49f6ae08f69717a3918202a51fe9945e19691e"
+url = "https://github.com/editorconfig-checker/editorconfig-checker/releases/download/v3.11.1/ec-linux-amd64.tar.gz"
+url_api = "https://api.github.com/repos/editorconfig-checker/editorconfig-checker/releases/assets/506306099"
+
+[tools.editorconfig-checker."platforms.macos-arm64"]
+checksum = "sha256:69205598fe8b26677d31194c5c9fdf7e8be321909ffb3c99efb5c9e4e648583e"
+url = "https://github.com/editorconfig-checker/editorconfig-checker/releases/download/v3.11.1/ec-darwin-arm64.tar.gz"
+url_api = "https://api.github.com/repos/editorconfig-checker/editorconfig-checker/releases/assets/506306097"
+
+[tools.editorconfig-checker."platforms.macos-x64"]
+checksum = "sha256:a9f68961e33d1a3b3f134c403557c8b2bbafb70b21b4c32b5d9245ffc2875f4f"
+url = "https://github.com/editorconfig-checker/editorconfig-checker/releases/download/v3.11.1/ec-darwin-amd64.tar.gz"
+url_api = "https://api.github.com/repos/editorconfig-checker/editorconfig-checker/releases/assets/506306114"
+
+[tools.editorconfig-checker."platforms.windows-x64"]
+checksum = "sha256:6f81f034bbaf77d7ea73274aa7d33995baf9e0d91bc4d4caa8eccff4645f870e"
+url = "https://github.com/editorconfig-checker/editorconfig-checker/releases/download/v3.11.1/ec-windows-amd64.zip"
+url_api = "https://api.github.com/repos/editorconfig-checker/editorconfig-checker/releases/assets/506306105"
+
+[[tools.google-java-format]]
+version = "1.36.1"
+backend = "aqua:google/google-java-format"
+
+[tools.google-java-format."platforms.linux-arm64"]
+checksum = "sha256:37d27263fce029fc59aa4e1b556ac6997d0eb2266a38ba2b8746b709f6c77b11"
+url = "https://github.com/google/google-java-format/releases/download/v1.36.1/google-java-format_linux-arm64"
+url_api = "https://api.github.com/repos/google/google-java-format/releases/assets/495658509"
+
+[tools.google-java-format."platforms.linux-arm64-musl"]
+checksum = "sha256:37d27263fce029fc59aa4e1b556ac6997d0eb2266a38ba2b8746b709f6c77b11"
+url = "https://github.com/google/google-java-format/releases/download/v1.36.1/google-java-format_linux-arm64"
+url_api = "https://api.github.com/repos/google/google-java-format/releases/assets/495658509"
+
+[tools.google-java-format."platforms.linux-x64"]
+checksum = "sha256:8dc71663a6c9cb17b02ba9709bfab5c4de59de6f4ac133fd7a1d5a4394b1193b"
+url = "https://github.com/google/google-java-format/releases/download/v1.36.1/google-java-format_linux-x86-64"
+url_api = "https://api.github.com/repos/google/google-java-format/releases/assets/495658560"
+
+[tools.google-java-format."platforms.linux-x64-musl"]
+checksum = "sha256:8dc71663a6c9cb17b02ba9709bfab5c4de59de6f4ac133fd7a1d5a4394b1193b"
+url = "https://github.com/google/google-java-format/releases/download/v1.36.1/google-java-format_linux-x86-64"
+url_api = "https://api.github.com/repos/google/google-java-format/releases/assets/495658560"
+
+[tools.google-java-format."platforms.macos-arm64"]
+checksum = "sha256:572c8be5ea4e15c674f7f9c89f9c0a4b9336950d9c42c07220557e97a4002bce"
+url = "https://github.com/google/google-java-format/releases/download/v1.36.1/google-java-format_darwin-arm64"
+url_api = "https://api.github.com/repos/google/google-java-format/releases/assets/495658217"
+
+[tools.google-java-format."platforms.windows-x64"]
+checksum = "sha256:23a6acedcfd9253da8413984dd1e3aecab32f00543477566d1abe5ae5aab1019"
+url = "https://github.com/google/google-java-format/releases/download/v1.36.1/google-java-format_windows-x86-64.exe"
+url_api = "https://api.github.com/repos/google/google-java-format/releases/assets/495658633"
+
+[[tools.hugo]]
+version = "0.165.0"
+backend = "aqua:gohugoio/hugo"
+
+[tools.hugo."platforms.linux-arm64"]
+checksum = "sha256:65c9fdd75e82d5f1eaf565f6e9fede6c0ceecaa267798e10c73068986996b77d"
+url = "https://github.com/gohugoio/hugo/releases/download/v0.165.0/hugo_0.165.0_linux-arm64.tar.gz"
+url_api = "https://api.github.com/repos/gohugoio/hugo/releases/assets/511697401"
+
+[tools.hugo."platforms.linux-arm64-musl"]
+checksum = "sha256:65c9fdd75e82d5f1eaf565f6e9fede6c0ceecaa267798e10c73068986996b77d"
+url = "https://github.com/gohugoio/hugo/releases/download/v0.165.0/hugo_0.165.0_linux-arm64.tar.gz"
+url_api = "https://api.github.com/repos/gohugoio/hugo/releases/assets/511697401"
+
+[tools.hugo."platforms.linux-x64"]
+checksum = "sha256:5c3a37a5450b3e386e5b75a87a790fea2d04a796d75e171216c80ef48a32b432"
+url = "https://github.com/gohugoio/hugo/releases/download/v0.165.0/hugo_0.165.0_linux-amd64.tar.gz"
+url_api = "https://api.github.com/repos/gohugoio/hugo/releases/assets/511697404"
+
+[tools.hugo."platforms.linux-x64-musl"]
+checksum = "sha256:5c3a37a5450b3e386e5b75a87a790fea2d04a796d75e171216c80ef48a32b432"
+url = "https://github.com/gohugoio/hugo/releases/download/v0.165.0/hugo_0.165.0_linux-amd64.tar.gz"
+url_api = "https://api.github.com/repos/gohugoio/hugo/releases/assets/511697404"
+
+[tools.hugo."platforms.macos-arm64"]
+checksum = "sha256:10ea75335975a13d0e73ac298402179335c55fa4e99d1687452d9cfa70b30d16"
+url = "https://github.com/gohugoio/hugo/releases/download/v0.165.0/hugo_0.165.0_darwin-universal.pkg"
+url_api = "https://api.github.com/repos/gohugoio/hugo/releases/assets/511697483"
+
+[tools.hugo."platforms.macos-x64"]
+checksum = "sha256:10ea75335975a13d0e73ac298402179335c55fa4e99d1687452d9cfa70b30d16"
+url = "https://github.com/gohugoio/hugo/releases/download/v0.165.0/hugo_0.165.0_darwin-universal.pkg"
+url_api = "https://api.github.com/repos/gohugoio/hugo/releases/assets/511697483"
+
+[tools.hugo."platforms.windows-x64"]
+checksum = "sha256:bdd9cc7837a42389067b2d0df7858d5878ceddef21307c2dd27fa10885fbbcf9"
+url = "https://github.com/gohugoio/hugo/releases/download/v0.165.0/hugo_0.165.0_windows-amd64.zip"
+url_api = "https://api.github.com/repos/gohugoio/hugo/releases/assets/511697493"
+
+[[tools.java]]
+version = "temurin-25.0.3+9.0.LTS"
+backend = "core:java"
+
+[tools.java."platforms.linux-arm64"]
+checksum = "sha256:3e4287cb98870ba824ed698854bdc27cff984254caf66dd12cc291e7bfdde26b"
+url = "https://github.com/adoptium/temurin25-binaries/releases/download/jdk-25.0.3%2B9/OpenJDK25U-jdk_aarch64_linux_hotspot_25.0.3_9.tar.gz"
+
+[tools.java."platforms.linux-arm64-musl"]
+checksum = "sha256:6ed368e93049d3b188c045fce0b20953bbea92fe0614dbbf4d3fd8daad7be3b2"
+url = "https://github.com/adoptium/temurin25-binaries/releases/download/jdk-25.0.3%2B9/OpenJDK25U-jdk_aarch64_alpine-linux_hotspot_25.0.3_9.tar.gz"
+
+[tools.java."platforms.linux-x64"]
+checksum = "sha256:69264a7a211bf5029830d07bc3370f879769d62ebc5b5488e90c9343a2da0e1f"
+url = "https://github.com/adoptium/temurin25-binaries/releases/download/jdk-25.0.3%2B9/OpenJDK25U-jdk_x64_linux_hotspot_25.0.3_9.tar.gz"
+
+[tools.java."platforms.linux-x64-musl"]
+checksum = "sha256:51c2415b370aac7c3796b0c4663c8fcf91bc22d76f03df95b25fa5667cb5fdd8"
+url = "https://github.com/adoptium/temurin25-binaries/releases/download/jdk-25.0.3%2B9/OpenJDK25U-jdk_x64_alpine-linux_hotspot_25.0.3_9.tar.gz"
+
+[tools.java."platforms.macos-arm64"]
+checksum = "sha256:7baab4d69a15554e119b86ff78d40e3fdc28819b5b322955c913cebfe3f6a37c"
+url = "https://github.com/adoptium/temurin25-binaries/releases/download/jdk-25.0.3%2B9/OpenJDK25U-jdk_aarch64_mac_hotspot_25.0.3_9.tar.gz"
+
+[tools.java."platforms.macos-x64"]
+checksum = "sha256:4c539a18b4d656960ff6766727e9ca546fc17f7a29714dba9e7b47bdcb37c447"
+url = "https://github.com/adoptium/temurin25-binaries/releases/download/jdk-25.0.3%2B9/OpenJDK25U-jdk_x64_mac_hotspot_25.0.3_9.tar.gz"
+
+[tools.java."platforms.windows-x64"]
+checksum = "sha256:709312cd0420296d9b9de917fe6e28a5b979e875ee5ab91783fb79bcd5857235"
+url = "https://github.com/adoptium/temurin25-binaries/releases/download/jdk-25.0.3%2B9/OpenJDK25U-jdk_x64_windows_hotspot_25.0.3_9.zip"
+
+[[tools.lychee]]
+version = "0.24.2"
+backend = "aqua:lycheeverse/lychee"
+
+[tools.lychee."platforms.linux-arm64"]
+checksum = "sha256:5d0b0e3aeab240f41920c633a6eaf97599be6eedda034b36e858ede7dba5e535"
+url = "https://github.com/lycheeverse/lychee/releases/download/lychee-v0.24.2/lychee-aarch64-unknown-linux-musl.tar.gz"
+url_api = "https://api.github.com/repos/lycheeverse/lychee/releases/assets/409958602"
+
+[tools.lychee."platforms.linux-arm64-musl"]
+checksum = "sha256:5d0b0e3aeab240f41920c633a6eaf97599be6eedda034b36e858ede7dba5e535"
+url = "https://github.com/lycheeverse/lychee/releases/download/lychee-v0.24.2/lychee-aarch64-unknown-linux-musl.tar.gz"
+url_api = "https://api.github.com/repos/lycheeverse/lychee/releases/assets/409958602"
+
+[tools.lychee."platforms.linux-x64"]
+checksum = "sha256:73657a111819a30c47c08352896796f23d64e4eb2b3ed39b6d32149241566fc5"
+url = "https://github.com/lycheeverse/lychee/releases/download/lychee-v0.24.2/lychee-x86_64-unknown-linux-musl.tar.gz"
+url_api = "https://api.github.com/repos/lycheeverse/lychee/releases/assets/409959271"
+
+[tools.lychee."platforms.linux-x64-musl"]
+checksum = "sha256:73657a111819a30c47c08352896796f23d64e4eb2b3ed39b6d32149241566fc5"
+url = "https://github.com/lycheeverse/lychee/releases/download/lychee-v0.24.2/lychee-x86_64-unknown-linux-musl.tar.gz"
+url_api = "https://api.github.com/repos/lycheeverse/lychee/releases/assets/409959271"
+
+[tools.lychee."platforms.macos-arm64"]
+checksum = "sha256:c9d3740ea2d891854d37116c9fba840f37b6e7c89d330e7db84ac333631c4977"
+url = "https://github.com/lycheeverse/lychee/releases/download/lychee-v0.24.2/lychee-aarch64-apple-darwin.tar.gz"
+url_api = "https://api.github.com/repos/lycheeverse/lychee/releases/assets/409957951"
+
+[tools.lychee."platforms.macos-x64"]
+checksum = "sha256:887503a9cff667d322b8d0892b40bf49976eb9507af8483220a3706cdad55978"
+url = "https://github.com/lycheeverse/lychee/releases/download/lychee-v0.24.2/lychee-x86_64-apple-darwin.tar.gz"
+url_api = "https://api.github.com/repos/lycheeverse/lychee/releases/assets/409957687"
+
+[tools.lychee."platforms.windows-x64"]
+checksum = "sha256:32975d1493ee1a975d6bb41e4fb56fe419cb442ded628bb772ba2e614acfacad"
+url = "https://github.com/lycheeverse/lychee/releases/download/lychee-v0.24.2/lychee-x86_64-pc-windows-msvc.zip"
+url_api = "https://api.github.com/repos/lycheeverse/lychee/releases/assets/409959491"
+
+[[tools.node]]
+version = "24.19.0"
+backend = "core:node"
+
+[tools.node."platforms.linux-arm64"]
+checksum = "sha256:d28c8a5bf0a808f0ed434a1dce8c54ae98f0371c0bd86ac58abc613f73e6643f"
+url = "https://nodejs.org/dist/v24.19.0/node-v24.19.0-linux-arm64.tar.gz"
+
+[tools.node."platforms.linux-arm64-musl"]
+checksum = "sha256:20824e4d35948fae5b337dccef47813b04d8995312f59df7386f2256d9f9ab7e"
+url = "https://unofficial-builds.nodejs.org/download/release/v24.19.0/node-v24.19.0-linux-arm64-musl.tar.gz"
+
+[tools.node."platforms.linux-x64"]
+checksum = "sha256:f625d97cd707df4ff96254916fbc5ff014f09c09effe5a1e0ca8f6d41a8789d4"
+url = "https://nodejs.org/dist/v24.19.0/node-v24.19.0-linux-x64.tar.gz"
+
+[tools.node."platforms.linux-x64-musl"]
+checksum = "sha256:c60223786df14a5d23e220ebb8e60318f5322640a62f90e6d9e54d3a18da532e"
+url = "https://unofficial-builds.nodejs.org/download/release/v24.19.0/node-v24.19.0-linux-x64-musl.tar.gz"
+
+[tools.node."platforms.macos-arm64"]
+checksum = "sha256:8294b7aa9b03997481c06babf1e8b270c859358f27da57a11509afe537ac381d"
+url = "https://nodejs.org/dist/v24.19.0/node-v24.19.0-darwin-arm64.tar.gz"
+
+[tools.node."platforms.macos-x64"]
+checksum = "sha256:d1b5e999db158c62fe8f7267a4476b035d8bd93b1a605bac24a3f0dd166e3316"
+url = "https://nodejs.org/dist/v24.19.0/node-v24.19.0-darwin-x64.tar.gz"
+
+[tools.node."platforms.windows-x64"]
+checksum = "sha256:57f71ab3652e797d84acddc79c81cc9ff1c6ddb2a1974cdb83f00fee9bff4c73"
+url = "https://nodejs.org/dist/v24.19.0/node-v24.19.0-win-x64.zip"
+
+[[tools."npm:renovate"]]
+version = "44.29.4"
+backend = "npm:renovate"
+
+[tools."npm:renovate".options]
+trust_policy_excludes = '["@yarnpkg/libzip@3.2.2"]'
+
+[[tools.protoc]]
+version = "36.0"
+backend = "aqua:protocolbuffers/protobuf/protoc"
+
+[tools.protoc."platforms.linux-arm64"]
+checksum = "sha256:4a00ec5e256d20a3deadd9e77d56da0ac04c72367c3c959f6d08e110a368400a"
+url = "https://github.com/protocolbuffers/protobuf/releases/download/v36.0/protoc-36.0-linux-aarch_64.zip"
+url_api = "https://api.github.com/repos/protocolbuffers/protobuf/releases/assets/522289978"
+
+[tools.protoc."platforms.linux-arm64-musl"]
+checksum = "sha256:4a00ec5e256d20a3deadd9e77d56da0ac04c72367c3c959f6d08e110a368400a"
+url = "https://github.com/protocolbuffers/protobuf/releases/download/v36.0/protoc-36.0-linux-aarch_64.zip"
+url_api = "https://api.github.com/repos/protocolbuffers/protobuf/releases/assets/522289978"
+
+[tools.protoc."platforms.linux-x64"]
+checksum = "sha256:bc8211ce760bd43ee21ddc145d6d9dbaeeabae205267a79d9054a240e367d4b4"
+url = "https://github.com/protocolbuffers/protobuf/releases/download/v36.0/protoc-36.0-linux-x86_64.zip"
+url_api = "https://api.github.com/repos/protocolbuffers/protobuf/releases/assets/522290000"
+
+[tools.protoc."platforms.linux-x64-musl"]
+checksum = "sha256:bc8211ce760bd43ee21ddc145d6d9dbaeeabae205267a79d9054a240e367d4b4"
+url = "https://github.com/protocolbuffers/protobuf/releases/download/v36.0/protoc-36.0-linux-x86_64.zip"
+url_api = "https://api.github.com/repos/protocolbuffers/protobuf/releases/assets/522290000"
+
+[tools.protoc."platforms.macos-arm64"]
+checksum = "sha256:b6bc4afdcb880124bf342851d05155b6e3d9b6e661236d87b9c614250d26ae00"
+url = "https://github.com/protocolbuffers/protobuf/releases/download/v36.0/protoc-36.0-osx-aarch_64.zip"
+url_api = "https://api.github.com/repos/protocolbuffers/protobuf/releases/assets/522290002"
+
+[tools.protoc."platforms.macos-x64"]
+checksum = "sha256:2847d952ecd1c466769ae3ca319c9cd34c3613542eba335dc9b02c49537f6c70"
+url = "https://github.com/protocolbuffers/protobuf/releases/download/v36.0/protoc-36.0-osx-x86_64.zip"
+url_api = "https://api.github.com/repos/protocolbuffers/protobuf/releases/assets/522290007"
+
+[tools.protoc."platforms.windows-x64"]
+checksum = "sha256:510fb2369a4720adb457783768c5e1481a0e1137d9e7694478dfe9b8fe445dc1"
+url = "https://github.com/protocolbuffers/protobuf/releases/download/v36.0/protoc-36.0-win64.zip"
+url_api = "https://api.github.com/repos/protocolbuffers/protobuf/releases/assets/522290018"
+
+[[tools.ruff]]
+version = "0.16.3"
+backend = "aqua:astral-sh/ruff"
+
+[tools.ruff."platforms.linux-arm64"]
+checksum = "sha256:8319ba22f655e6efc086103486d7165bf0de73f71ff1c5f25ba580153ad05feb"
+url = "https://github.com/astral-sh/ruff/releases/download/0.16.3/ruff-aarch64-unknown-linux-musl.tar.gz"
+url_api = "https://api.github.com/repos/astral-sh/ruff/releases/assets/513140238"
+provenance = "github-attestations"
+
+[tools.ruff."platforms.linux-arm64-musl"]
+checksum = "sha256:8319ba22f655e6efc086103486d7165bf0de73f71ff1c5f25ba580153ad05feb"
+url = "https://github.com/astral-sh/ruff/releases/download/0.16.3/ruff-aarch64-unknown-linux-musl.tar.gz"
+url_api = "https://api.github.com/repos/astral-sh/ruff/releases/assets/513140238"
+provenance = "github-attestations"
+
+[tools.ruff."platforms.linux-x64"]
+checksum = "sha256:d67c9b5949981698c48915abf65e0b3406ba9184ad73521cdf20a926bc889c73"
+url = "https://github.com/astral-sh/ruff/releases/download/0.16.3/ruff-x86_64-unknown-linux-musl.tar.gz"
+url_api = "https://api.github.com/repos/astral-sh/ruff/releases/assets/513140356"
+provenance = "github-attestations"
+
+[tools.ruff."platforms.linux-x64-musl"]
+checksum = "sha256:d67c9b5949981698c48915abf65e0b3406ba9184ad73521cdf20a926bc889c73"
+url = "https://github.com/astral-sh/ruff/releases/download/0.16.3/ruff-x86_64-unknown-linux-musl.tar.gz"
+url_api = "https://api.github.com/repos/astral-sh/ruff/releases/assets/513140356"
+provenance = "github-attestations"
+
+[tools.ruff."platforms.macos-arm64"]
+checksum = "sha256:136a4db6512d9b16dda56ac8604696ed65c3b1a914a142de029e7f8d5006f1d9"
+url = "https://github.com/astral-sh/ruff/releases/download/0.16.3/ruff-aarch64-apple-darwin.tar.gz"
+url_api = "https://api.github.com/repos/astral-sh/ruff/releases/assets/513140218"
+provenance = "github-attestations"
+
+[tools.ruff."platforms.macos-x64"]
+checksum = "sha256:05c2a6705e7c0c056d6d93ff538978583f0c47b4c28d334ab9d58d2e8daf4c24"
+url = "https://github.com/astral-sh/ruff/releases/download/0.16.3/ruff-x86_64-apple-darwin.tar.gz"
+url_api = "https://api.github.com/repos/astral-sh/ruff/releases/assets/513140338"
+provenance = "github-attestations"
+
+[tools.ruff."platforms.windows-x64"]
+checksum = "sha256:f10c709755b393fd9821506b21070bcca969b9966504edd1e490efd08e3662ba"
+url = "https://github.com/astral-sh/ruff/releases/download/0.16.3/ruff-x86_64-pc-windows-msvc.zip"
+url_api = "https://api.github.com/repos/astral-sh/ruff/releases/assets/513140346"
+provenance = "github-attestations"
+
+[[tools.rumdl]]
+version = "v0.2.55"
+backend = "aqua:rvben/rumdl"
+
+[tools.rumdl."platforms.linux-arm64"]
+checksum = "sha256:baed03dac3bf50565f4e1e461fb8162fcff58742034c6d643a1977d9025dfdb6"
+url = "https://github.com/rvben/rumdl/releases/download/v0.2.55/rumdl-v0.2.55-aarch64-unknown-linux-musl.tar.gz"
+url_api = "https://api.github.com/repos/rvben/rumdl/releases/assets/512169605"
+provenance = "github-attestations"
+
+[tools.rumdl."platforms.linux-arm64-musl"]
+checksum = "sha256:baed03dac3bf50565f4e1e461fb8162fcff58742034c6d643a1977d9025dfdb6"
+url = "https://github.com/rvben/rumdl/releases/download/v0.2.55/rumdl-v0.2.55-aarch64-unknown-linux-musl.tar.gz"
+url_api = "https://api.github.com/repos/rvben/rumdl/releases/assets/512169605"
+provenance = "github-attestations"
+
+[tools.rumdl."platforms.linux-x64"]
+checksum = "sha256:09c8f72cb57f1ff646d68b5ec2ef6a7b296e6d9f945093db2698c260e81d9f90"
+url = "https://github.com/rvben/rumdl/releases/download/v0.2.55/rumdl-v0.2.55-x86_64-unknown-linux-musl.tar.gz"
+url_api = "https://api.github.com/repos/rvben/rumdl/releases/assets/512169600"
+provenance = "github-attestations"
+
+[tools.rumdl."platforms.linux-x64-musl"]
+checksum = "sha256:09c8f72cb57f1ff646d68b5ec2ef6a7b296e6d9f945093db2698c260e81d9f90"
+url = "https://github.com/rvben/rumdl/releases/download/v0.2.55/rumdl-v0.2.55-x86_64-unknown-linux-musl.tar.gz"
+url_api = "https://api.github.com/repos/rvben/rumdl/releases/assets/512169600"
+provenance = "github-attestations"
+
+[tools.rumdl."platforms.macos-arm64"]
+checksum = "sha256:98dd5620e7eb8ba8fd77830fe1f313530ae2a6368ae867149f7de13dbebde6e3"
+url = "https://github.com/rvben/rumdl/releases/download/v0.2.55/rumdl-v0.2.55-aarch64-apple-darwin.tar.gz"
+url_api = "https://api.github.com/repos/rvben/rumdl/releases/assets/512169607"
+provenance = "github-attestations"
+
+[tools.rumdl."platforms.macos-x64"]
+checksum = "sha256:8dadf1b9aeda17f8a41ab4fcdd2a7f7dfb6eda8981ddce3ebf61b72e3d1d3f89"
+url = "https://github.com/rvben/rumdl/releases/download/v0.2.55/rumdl-v0.2.55-x86_64-apple-darwin.tar.gz"
+url_api = "https://api.github.com/repos/rvben/rumdl/releases/assets/512169601"
+provenance = "github-attestations"
+
+[tools.rumdl."platforms.windows-x64"]
+checksum = "sha256:47c04176f960e2d196b1e465564869363469ec00714c2970e1f441deff7f679d"
+url = "https://github.com/rvben/rumdl/releases/download/v0.2.55/rumdl-v0.2.55-x86_64-pc-windows-msvc.zip"
+url_api = "https://api.github.com/repos/rvben/rumdl/releases/assets/512169608"
+provenance = "github-attestations"
+
+[[tools.shellcheck]]
+version = "v0.11.0"
+backend = "aqua:koalaman/shellcheck"
+
+[tools.shellcheck."platforms.linux-arm64"]
+checksum = "sha256:12b331c1d2db6b9eb13cfca64306b1b157a86eb69db83023e261eaa7e7c14588"
+url = "https://github.com/koalaman/shellcheck/releases/download/v0.11.0/shellcheck-v0.11.0.linux.aarch64.tar.xz"
+url_api = "https://api.github.com/repos/koalaman/shellcheck/releases/assets/279056934"
+
+[tools.shellcheck."platforms.linux-arm64-musl"]
+checksum = "sha256:12b331c1d2db6b9eb13cfca64306b1b157a86eb69db83023e261eaa7e7c14588"
+url = "https://github.com/koalaman/shellcheck/releases/download/v0.11.0/shellcheck-v0.11.0.linux.aarch64.tar.xz"
+url_api = "https://api.github.com/repos/koalaman/shellcheck/releases/assets/279056934"
+
+[tools.shellcheck."platforms.linux-x64"]
+checksum = "sha256:8c3be12b05d5c177a04c29e3c78ce89ac86f1595681cab149b65b97c4e227198"
+url = "https://github.com/koalaman/shellcheck/releases/download/v0.11.0/shellcheck-v0.11.0.linux.x86_64.tar.xz"
+url_api = "https://api.github.com/repos/koalaman/shellcheck/releases/assets/279056942"
+
+[tools.shellcheck."platforms.linux-x64-musl"]
+checksum = "sha256:8c3be12b05d5c177a04c29e3c78ce89ac86f1595681cab149b65b97c4e227198"
+url = "https://github.com/koalaman/shellcheck/releases/download/v0.11.0/shellcheck-v0.11.0.linux.x86_64.tar.xz"
+url_api = "https://api.github.com/repos/koalaman/shellcheck/releases/assets/279056942"
+
+[tools.shellcheck."platforms.macos-arm64"]
+checksum = "sha256:56affdd8de5527894dca6dc3d7e0a99a873b0f004d7aabc30ae407d3f48b0a79"
+url = "https://github.com/koalaman/shellcheck/releases/download/v0.11.0/shellcheck-v0.11.0.darwin.aarch64.tar.xz"
+url_api = "https://api.github.com/repos/koalaman/shellcheck/releases/assets/279056932"
+
+[tools.shellcheck."platforms.macos-x64"]
+checksum = "sha256:3c89db4edcab7cf1c27bff178882e0f6f27f7afdf54e859fa041fca10febe4c6"
+url = "https://github.com/koalaman/shellcheck/releases/download/v0.11.0/shellcheck-v0.11.0.darwin.x86_64.tar.xz"
+url_api = "https://api.github.com/repos/koalaman/shellcheck/releases/assets/279056930"
+
+[tools.shellcheck."platforms.windows-x64"]
+checksum = "sha256:8a4e35ab0b331c85d73567b12f2a444df187f483e5079ceffa6bda1faa2e740e"
+url = "https://github.com/koalaman/shellcheck/releases/download/v0.11.0/shellcheck-v0.11.0.zip"
+url_api = "https://api.github.com/repos/koalaman/shellcheck/releases/assets/279056944"
+
+[[tools.shfmt]]
+version = "3.13.1"
+backend = "aqua:mvdan/sh"
+
+[tools.shfmt."platforms.linux-arm64"]
+checksum = "sha256:32d92acaa5cd8abb29fc49dac123dc412442d5713967819d8af2c29f1b3857c7"
+url = "https://github.com/mvdan/sh/releases/download/v3.13.1/shfmt_v3.13.1_linux_arm64"
+url_api = "https://api.github.com/repos/mvdan/sh/releases/assets/390322859"
+
+[tools.shfmt."platforms.linux-arm64-musl"]
+checksum = "sha256:32d92acaa5cd8abb29fc49dac123dc412442d5713967819d8af2c29f1b3857c7"
+url = "https://github.com/mvdan/sh/releases/download/v3.13.1/shfmt_v3.13.1_linux_arm64"
+url_api = "https://api.github.com/repos/mvdan/sh/releases/assets/390322859"
+
+[tools.shfmt."platforms.linux-x64"]
+checksum = "sha256:fb096c5d1ac6beabbdbaa2874d025badb03ee07929f0c9ff67563ce8c75398b1"
+url = "https://github.com/mvdan/sh/releases/download/v3.13.1/shfmt_v3.13.1_linux_amd64"
+url_api = "https://api.github.com/repos/mvdan/sh/releases/assets/390322866"
+
+[tools.shfmt."platforms.linux-x64-musl"]
+checksum = "sha256:fb096c5d1ac6beabbdbaa2874d025badb03ee07929f0c9ff67563ce8c75398b1"
+url = "https://github.com/mvdan/sh/releases/download/v3.13.1/shfmt_v3.13.1_linux_amd64"
+url_api = "https://api.github.com/repos/mvdan/sh/releases/assets/390322866"
+
+[tools.shfmt."platforms.macos-arm64"]
+checksum = "sha256:9680526be4a66ea1ffe988ed08af58e1400fe1e4f4aef5bd88b20bb9b3da33f8"
+url = "https://github.com/mvdan/sh/releases/download/v3.13.1/shfmt_v3.13.1_darwin_arm64"
+url_api = "https://api.github.com/repos/mvdan/sh/releases/assets/390322881"
+
+[tools.shfmt."platforms.macos-x64"]
+checksum = "sha256:6feedafc72915794163114f512348e2437d080d0047ef8b8fa2ec63b575f12af"
+url = "https://github.com/mvdan/sh/releases/download/v3.13.1/shfmt_v3.13.1_darwin_amd64"
+url_api = "https://api.github.com/repos/mvdan/sh/releases/assets/390322886"
+
+[tools.shfmt."platforms.windows-x64"]
+checksum = "sha256:60cd368533d0ad73fa86d93d5bbf95ef40587245ce684ed138c1b31557b5fe97"
+url = "https://github.com/mvdan/sh/releases/download/v3.13.1/shfmt_v3.13.1_windows_amd64.exe"
+url_api = "https://api.github.com/repos/mvdan/sh/releases/assets/390322844"
+
+[[tools.taplo]]
+version = "0.10.0"
+backend = "aqua:tamasfe/taplo"
+
+[tools.taplo."platforms.linux-arm64"]
+url = "https://github.com/tamasfe/taplo/releases/download/0.10.0/taplo-linux-aarch64.gz"
+url_api = "https://api.github.com/repos/tamasfe/taplo/releases/assets/257322597"
+
+[tools.taplo."platforms.linux-arm64-musl"]
+url = "https://github.com/tamasfe/taplo/releases/download/0.10.0/taplo-linux-aarch64.gz"
+url_api = "https://api.github.com/repos/tamasfe/taplo/releases/assets/257322597"
+
+[tools.taplo."platforms.linux-x64"]
+url = "https://github.com/tamasfe/taplo/releases/download/0.10.0/taplo-linux-x86_64.gz"
+url_api = "https://api.github.com/repos/tamasfe/taplo/releases/assets/257322600"
+
+[tools.taplo."platforms.linux-x64-musl"]
+url = "https://github.com/tamasfe/taplo/releases/download/0.10.0/taplo-linux-x86_64.gz"
+url_api = "https://api.github.com/repos/tamasfe/taplo/releases/assets/257322600"
+
+[tools.taplo."platforms.macos-arm64"]
+url = "https://github.com/tamasfe/taplo/releases/download/0.10.0/taplo-darwin-aarch64.gz"
+url_api = "https://api.github.com/repos/tamasfe/taplo/releases/assets/257323110"
+
+[tools.taplo."platforms.macos-x64"]
+url = "https://github.com/tamasfe/taplo/releases/download/0.10.0/taplo-darwin-x86_64.gz"
+url_api = "https://api.github.com/repos/tamasfe/taplo/releases/assets/257323116"
+
+[tools.taplo."platforms.windows-x64"]
+url = "https://github.com/tamasfe/taplo/releases/download/0.10.0/taplo-windows-x86_64.zip"
+url_api = "https://api.github.com/repos/tamasfe/taplo/releases/assets/257323062"
+
+[[tools.typos]]
+version = "1.49.0"
+backend = "aqua:crate-ci/typos"
+
+[tools.typos."platforms.linux-arm64"]
+checksum = "sha256:85c8b87b22a0fb1da130cd4d495e0beba7f1225eb580933184509e146ec4c509"
+url = "https://github.com/crate-ci/typos/releases/download/v1.49.0/typos-v1.49.0-aarch64-unknown-linux-musl.tar.gz"
+url_api = "https://api.github.com/repos/crate-ci/typos/releases/assets/500258983"
+
+[tools.typos."platforms.linux-arm64-musl"]
+checksum = "sha256:85c8b87b22a0fb1da130cd4d495e0beba7f1225eb580933184509e146ec4c509"
+url = "https://github.com/crate-ci/typos/releases/download/v1.49.0/typos-v1.49.0-aarch64-unknown-linux-musl.tar.gz"
+url_api = "https://api.github.com/repos/crate-ci/typos/releases/assets/500258983"
+
+[tools.typos."platforms.linux-x64"]
+checksum = "sha256:48bd2d58e02ce713b8c0f1aa239e68ee4f7d8c551013135806e6aed3938d9e10"
+url = "https://github.com/crate-ci/typos/releases/download/v1.49.0/typos-v1.49.0-x86_64-unknown-linux-musl.tar.gz"
+url_api = "https://api.github.com/repos/crate-ci/typos/releases/assets/500261019"
+
+[tools.typos."platforms.linux-x64-musl"]
+checksum = "sha256:48bd2d58e02ce713b8c0f1aa239e68ee4f7d8c551013135806e6aed3938d9e10"
+url = "https://github.com/crate-ci/typos/releases/download/v1.49.0/typos-v1.49.0-x86_64-unknown-linux-musl.tar.gz"
+url_api = "https://api.github.com/repos/crate-ci/typos/releases/assets/500261019"
+
+[tools.typos."platforms.macos-arm64"]
+checksum = "sha256:8c0e7bd40b2b60c0b0cfe9f74dd814b4d4385c956ce86860f7da9e62d91fdc73"
+url = "https://github.com/crate-ci/typos/releases/download/v1.49.0/typos-v1.49.0-aarch64-apple-darwin.tar.gz"
+url_api = "https://api.github.com/repos/crate-ci/typos/releases/assets/500262463"
+
+[tools.typos."platforms.macos-x64"]
+checksum = "sha256:4cecbf653a9fc45f023abf57f4e2e2f6b138c2d2387b09289beacdd3f0ea7bfd"
+url = "https://github.com/crate-ci/typos/releases/download/v1.49.0/typos-v1.49.0-x86_64-apple-darwin.tar.gz"
+url_api = "https://api.github.com/repos/crate-ci/typos/releases/assets/500259181"
+
+[tools.typos."platforms.windows-x64"]
+checksum = "sha256:06d3a1b71c282e021671070696a72696d5c60ea485b47dc4f8f1fbcf90144d02"
+url = "https://github.com/crate-ci/typos/releases/download/v1.49.0/typos-v1.49.0-x86_64-pc-windows-msvc.zip"
+url_api = "https://api.github.com/repos/crate-ci/typos/releases/assets/500264256"
+
+[[tools.zizmor]]
+version = "1.29.0"
+backend = "aqua:zizmorcore/zizmor"
+
+[tools.zizmor."platforms.linux-arm64"]
+checksum = "sha256:415eaa7c0a06479a701b8e44a3e812c1047decc848ec4bede7bd6bbf49f22d20"
+url = "https://github.com/zizmorcore/zizmor/releases/download/v1.29.0/zizmor-aarch64-unknown-linux-gnu.tar.gz"
+url_api = "https://api.github.com/repos/zizmorcore/zizmor/releases/assets/498263143"
+provenance = "github-attestations"
+
+[tools.zizmor."platforms.linux-arm64-musl"]
+provenance = "github-attestations"
+
+[tools.zizmor."platforms.linux-x64"]
+checksum = "sha256:dd96df044a6e8538d5f423790f453bdd03d49e5b2bcc38214acc41a2f1297839"
+url = "https://github.com/zizmorcore/zizmor/releases/download/v1.29.0/zizmor-x86_64-unknown-linux-gnu.tar.gz"
+url_api = "https://api.github.com/repos/zizmorcore/zizmor/releases/assets/498263145"
+provenance = "github-attestations"
+
+[tools.zizmor."platforms.linux-x64-musl"]
+provenance = "github-attestations"
+
+[tools.zizmor."platforms.macos-arm64"]
+checksum = "sha256:720322fade9e83a9c7953944c438f2ba942636b86b96a8f0e6b15ce94c8a6b6f"
+url = "https://github.com/zizmorcore/zizmor/releases/download/v1.29.0/zizmor-aarch64-apple-darwin.tar.gz"
+url_api = "https://api.github.com/repos/zizmorcore/zizmor/releases/assets/498263141"
+provenance = "github-attestations"
+
+[tools.zizmor."platforms.macos-x64"]
+checksum = "sha256:648b72ab9941a7f2a8d65d7b68a8e76cef789538c8df3a3950384d38423375b0"
+url = "https://github.com/zizmorcore/zizmor/releases/download/v1.29.0/zizmor-x86_64-apple-darwin.tar.gz"
+url_api = "https://api.github.com/repos/zizmorcore/zizmor/releases/assets/498263144"
+provenance = "github-attestations"
+
+[tools.zizmor."platforms.windows-x64"]
+checksum = "sha256:68a6bc6888f10bf0d53658c75885e7c1b7a0588d4c1fbc3f0ca280ad7324bf06"
+url = "https://github.com/zizmorcore/zizmor/releases/download/v1.29.0/zizmor-x86_64-pc-windows-msvc.zip"
+url_api = "https://api.github.com/repos/zizmorcore/zizmor/releases/assets/498263142"
+provenance = "github-attestations"
diff --git a/mise.native.toml b/mise.native.toml
deleted file mode 100644
index 67ad3940f..000000000
--- a/mise.native.toml
+++ /dev/null
@@ -1,8 +0,0 @@
-[tools]
-java = "graalvm-community-24.0.1"
-
-[tasks.test]
-depends = "build"
-run = "../../mvnw test -PnativeTest"
-dir = "integration-tests/it-spring-boot-smoke-test"
-
diff --git a/mise.toml b/mise.toml
index 59326b432..29085c417 100644
--- a/mise.toml
+++ b/mise.toml
@@ -1,10 +1,46 @@
+monorepo_root = true
+
+[monorepo]
+config_roots = [".mise/envs/*"]
+
[tools]
-"cargo:zizmor" = "1.9.0"
-"go:github.com/gohugoio/hugo" = "v0.148.2"
-"go:github.com/grafana/oats" = "0.4.0"
-java = "temurin-21.0.8+9.0.LTS"
-lychee = "0.19.1"
-protoc = "31.1"
+"aqua:grafana/gcx" = "v1.1.0"
+"aqua:grafana/oats" = "0.10.0"
+hugo = "0.165.0"
+java = "temurin-25.0.3+9.0.LTS"
+node = "24.19.0"
+protoc = "36.0"
+
+# Linters
+actionlint = "1.7.12"
+"aqua:grafana/flint" = "0.22.10"
+"aqua:jonwiggins/xmloxide" = "v0.5.0"
+"aqua:owenlamont/ryl" = "0.21.0"
+biome = "2.5.8"
+checkstyle = "13.8.0"
+editorconfig-checker = "3.11.1"
+google-java-format = "1.36.1"
+lychee = "0.24.2"
+# @yarnpkg/libzip 3.2.2 is npm-signed and matches Yarn's release commit, but
+# was published without provenance; mise's no-downgrade policy otherwise rejects Renovate.
+"npm:renovate" = { version = "44.29.4", trust_policy_excludes = ["@yarnpkg/libzip@3.2.2"] }
+ruff = "0.16.3"
+rumdl = "v0.2.55"
+shellcheck = "v0.11.0"
+shfmt = "3.13.1"
+taplo = "0.10.0"
+typos = "1.49.0"
+zizmor = "1.29.0"
+
+[env]
+FLINT_CONFIG_DIR = ".github/config"
+# renovate: datasource=github-releases depName=grafana/docker-otel-lgtm
+LGTM_VERSION = "0.30.2"
+# Latest JMX Exporter release; used as the default ref for the compatibility job.
+# renovate: datasource=github-tags depName=prometheus/jmx_exporter versioning=semver-coerced
+DEFAULT_JMX_EXPORTER_VERSION = "v1.6.0"
+# renovate: datasource=github-tags depName=micrometer-metrics/micrometer versioning=semver-coerced
+DEFAULT_MICROMETER_VERSION = "v1.17.1"
[tasks.ci]
description = "CI Build"
@@ -12,65 +48,69 @@ run = "./mvnw clean install"
env.REQUIRE_PROTO_UP_TO_DATE = "true"
env.PROTO_GENERATION = "true"
-[tasks.format]
-description = "format source code"
-run = "./mvnw spotless:apply"
+[tasks.clean]
+description = "clean all modules"
+run = "./mvnw clean"
[tasks.compile]
description = "bare compile, ignoring formatting and linters"
-run = "./mvnw install -DskipTests -Dspotless.check.skip=true -Dcoverage.skip=true -Dcheckstyle.skip=true -Dwarnings=-nowarn"
+run = "./mvnw install -DskipTests -Dcoverage.skip=true -Dwarnings=-nowarn"
[tasks.generate]
-description = "bare compile, ignoring formatting and linters"
-run = "./mvnw install -DskipTests -Dspotless.check.skip=true -Dcoverage.skip=true -Dcheckstyle.skip=true -Dwarnings=-nowarn"
+description = "regenerate protobuf sources"
+run = "./mvnw clean install -DskipTests -Dcoverage.skip=true -Dwarnings=-nowarn"
env.PROTO_GENERATION = "true"
[tasks.test]
description = "run unit tests, ignoring formatting and linters"
-run = "./mvnw test -Dspotless.check.skip=true -Dcoverage.skip=true -Dcheckstyle.skip=true -Dwarnings=-nowarn"
+run = "./mvnw test -Dcoverage.skip=true -Dwarnings=-nowarn"
[tasks.test-all]
description = "run all tests"
run = "./mvnw verify"
[tasks.build]
-description = "build all modules wihthout tests"
-run = "./mvnw install -DskipTests"
-
-[tasks.lint]
-run = "scripts/super-linter.sh"
-
-[tasks.lint-links]
-run = "lychee --include-fragments ."
-
-[tasks.lint-gh-actions]
-run = "zizmor .github/"
-
-[tasks.lint-bom]
-run = "scripts/lint-bom.sh"
-
-[tasks.lint-rest]
-description = "All lints not covered by super linter"
-depends = ["lint-links", "lint-gh-actions", "lint-bom"]
+description = "build all modules without tests"
+run = "./mvnw install -DskipTests -Dcoverage.skip=true"
+
+[tasks."api-diff"]
+description = "Compare published API against the japicmp baseline and refresh docs/apidiffs"
+run = """
+# Baseline version comes from the property in pom.xml.
+# Set API_DIFF_BASELINE_VERSION only to override it ad hoc (e.g. workflow_dispatch).
+BASELINE_OVERRIDE="${API_DIFF_BASELINE_VERSION:+-Dapi.diff.baseline.version=${API_DIFF_BASELINE_VERSION}}"
+./mvnw -B verify \
+ -P 'api-diff,!examples-and-integration-tests' \
+ ${BASELINE_OVERRIDE} \
+ -DskipTests \
+ -Dcoverage.skip=true \
+ -Dwarnings=-nowarn
+./.github/scripts/sync-api-diffs.sh
+"""
+
+[tasks."lint"]
+description = "Run all lints"
+depends = ["lint:bom"]
+raw_args = true
+run = "flint run"
+
+[tasks."lint:fix"]
+description = "Auto-fix lint issues"
+run = "flint run --fix"
[tasks.acceptance-test]
description = "Run OATs acceptance tests"
depends = "build"
-run = "oats -timeout 5m examples/"
-
-[tasks.set-version]
-run = './scripts/set-version.sh {{arg(name="version")}}'
+run = "oats --no-cache --lgtm-version $LGTM_VERSION --timeout 5m ."
[tasks.javadoc]
+description = "Generate Javadoc"
run = [
"./mvnw -B clean compile javadoc:javadoc javadoc:aggregate -P 'javadoc,!default'",
"rm -rf ./docs/static/api",
- "mv ./target/reports/apidocs ./docs/static/api && echo && echo 'ls ./docs/static/api' && ls ./docs/static/api"
+ "mv ./target/reports/apidocs ./docs/static/api && echo && echo 'ls ./docs/static/api' && ls ./docs/static/api",
]
-[tasks.set-gh-pages-version]
-run = "./scripts/set-release-version-github-pages.sh"
-
[tasks.gh-pages-dev]
description = "Build GitHub pages for dev"
run = "hugo server -D"
@@ -78,21 +118,29 @@ dir = "docs"
[tasks.build-gh-pages]
description = "Build GitHub pages"
-depends = ["javadoc", "set-gh-pages-version"]
+depends = ["javadoc", "set-release-version-github-pages"]
# For maximum backward compatibility with Hugo modules
env = { HUGO_ENVIRONMENT = "production", HUGO_ENV = "production" }
dir = "docs"
-run = [
- "hugo --gc --minify --baseURL ${BASE_URL}/",
- "echo 'ls ./public/api' && ls ./public/api"
-]
-
-[tasks.build-release]
-description = "Build release"
-run = "./scripts/build-release.sh"
-env.TAG = "1.4.0-SNAPSHOT"
-
-[settings]
-# to get lock file support and for go backend
-experimental = true
-
+run = ["hugo --gc --minify --baseURL ${BASE_URL}/", "echo 'ls ./public/api' && ls ./public/api"]
+
+[tasks."benchmark:quick"]
+description = "Run benchmarks with reduced iterations (quick smoke test, ~10 min)"
+run = "python3 ./.mise/tasks/update_benchmarks.py --jmh-args '-f 1 -wi 1 -i 3 -prof gc'"
+
+[tasks."benchmark:ci"]
+description = "Run benchmarks with CI configuration (3 forks, 3 warmup, 5 measurement iterations (~60 min total)"
+run = "python3 ./.mise/tasks/update_benchmarks.py --jmh-args '-f 3 -wi 3 -i 5 -prof gc'"
+
+[tasks."benchmark:ci-json"]
+description = "Run benchmarks with CI configuration and JSON output (for workflow/testing)"
+run = """
+./mvnw -pl benchmarks -am -DskipTests clean package
+JMH_ARGS="${JMH_ARGS:--f 3 -wi 3 -i 5}"
+echo "Running benchmarks with args: $JMH_ARGS"
+java -jar ./benchmarks/target/benchmarks.jar -rf json -rff benchmark-results.json $JMH_ARGS -prof gc
+"""
+
+[tasks."benchmark:generate-summary"]
+description = "Generate summary from existing benchmark-results.json"
+run = "python3 ./.mise/tasks/generate_benchmark_summary.py"
diff --git a/mvnw b/mvnw
index 6fdd4d2b2..bd8896bf2 100755
--- a/mvnw
+++ b/mvnw
@@ -19,7 +19,7 @@
# ----------------------------------------------------------------------------
# ----------------------------------------------------------------------------
-# Apache Maven Wrapper startup batch script, version 3.3.2
+# Apache Maven Wrapper startup batch script, version 3.3.4
#
# Optional ENV vars
# -----------------
@@ -36,101 +36,104 @@ set -euf
native_path() { printf %s\\n "$1"; }
case "$(uname)" in
CYGWIN* | MINGW*)
- [ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")"
- native_path() { cygpath --path --windows "$1"; }
- ;;
+ [ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")"
+ native_path() { cygpath --path --windows "$1"; }
+ ;;
esac
# set JAVACMD and JAVACCMD
set_java_home() {
- # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched
- if [ -n "${JAVA_HOME-}" ]; then
- if [ -x "$JAVA_HOME/jre/sh/java" ]; then
- # IBM's JDK on AIX uses strange locations for the executables
- JAVACMD="$JAVA_HOME/jre/sh/java"
- JAVACCMD="$JAVA_HOME/jre/sh/javac"
- else
- JAVACMD="$JAVA_HOME/bin/java"
- JAVACCMD="$JAVA_HOME/bin/javac"
-
- if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then
- echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2
- echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2
- return 1
- fi
- fi
- else
- JAVACMD="$(
- 'set' +e
- 'unset' -f command 2>/dev/null
- 'command' -v java
- )" || :
- JAVACCMD="$(
- 'set' +e
- 'unset' -f command 2>/dev/null
- 'command' -v javac
- )" || :
-
- if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then
- echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2
- return 1
- fi
- fi
+ # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched
+ if [ -n "${JAVA_HOME-}" ]; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ]; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ JAVACCMD="$JAVA_HOME/jre/sh/javac"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ JAVACCMD="$JAVA_HOME/bin/javac"
+
+ if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then
+ echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2
+ echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2
+ return 1
+ fi
+ fi
+ else
+ JAVACMD="$(
+ 'set' +e
+ 'unset' -f command 2>/dev/null
+ 'command' -v java
+ )" || :
+ JAVACCMD="$(
+ 'set' +e
+ 'unset' -f command 2>/dev/null
+ 'command' -v javac
+ )" || :
+
+ if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then
+ echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2
+ return 1
+ fi
+ fi
}
# hash string like Java String::hashCode
hash_string() {
- str="${1:-}" h=0
- while [ -n "$str" ]; do
- char="${str%"${str#?}"}"
- h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296))
- str="${str#?}"
- done
- printf %x\\n $h
+ str="${1:-}" h=0
+ while [ -n "$str" ]; do
+ char="${str%"${str#?}"}"
+ h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296))
+ str="${str#?}"
+ done
+ printf %x\\n $h
}
verbose() { :; }
[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; }
die() {
- printf %s\\n "$1" >&2
- exit 1
+ printf %s\\n "$1" >&2
+ exit 1
}
trim() {
- # MWRAPPER-139:
- # Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds.
- # Needed for removing poorly interpreted newline sequences when running in more
- # exotic environments such as mingw bash on Windows.
- printf "%s" "${1}" | tr -d '[:space:]'
+ # MWRAPPER-139:
+ # Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds.
+ # Needed for removing poorly interpreted newline sequences when running in more
+ # exotic environments such as mingw bash on Windows.
+ printf "%s" "${1}" | tr -d '[:space:]'
}
+scriptDir="$(dirname "$0")"
+scriptName="$(basename "$0")"
+
# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties
while IFS="=" read -r key value; do
- case "${key-}" in
- distributionUrl) distributionUrl=$(trim "${value-}") ;;
- distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;;
- esac
-done <"${0%/*}/.mvn/wrapper/maven-wrapper.properties"
-[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in ${0%/*}/.mvn/wrapper/maven-wrapper.properties"
+ case "${key-}" in
+ distributionUrl) distributionUrl=$(trim "${value-}") ;;
+ distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;;
+ esac
+done <"$scriptDir/.mvn/wrapper/maven-wrapper.properties"
+[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties"
case "${distributionUrl##*/}" in
maven-mvnd-*bin.*)
- MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/
- case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in
- *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;;
- :Darwin*x86_64) distributionPlatform=darwin-amd64 ;;
- :Darwin*arm64) distributionPlatform=darwin-aarch64 ;;
- :Linux*x86_64*) distributionPlatform=linux-amd64 ;;
- *)
- echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2
- distributionPlatform=linux-amd64
- ;;
- esac
- distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip"
- ;;
+ MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/
+ case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in
+ *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;;
+ :Darwin*x86_64) distributionPlatform=darwin-amd64 ;;
+ :Darwin*arm64) distributionPlatform=darwin-aarch64 ;;
+ :Linux*x86_64*) distributionPlatform=linux-amd64 ;;
+ *)
+ echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2
+ distributionPlatform=linux-amd64
+ ;;
+ esac
+ distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip"
+ ;;
maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;;
-*) MVN_CMD="mvn${0##*/mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;;
+*) MVN_CMD="mvn${scriptName#mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;;
esac
# apply MVNW_REPOURL and calculate MAVEN_HOME
@@ -143,13 +146,13 @@ MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}"
MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")"
exec_maven() {
- unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || :
- exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD"
+ unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || :
+ exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD"
}
if [ -d "$MAVEN_HOME" ]; then
- verbose "found existing MAVEN_HOME at $MAVEN_HOME"
- exec_maven "$@"
+ verbose "found existing MAVEN_HOME at $MAVEN_HOME"
+ exec_maven "$@"
fi
case "${distributionUrl-}" in
@@ -159,10 +162,10 @@ esac
# prepare tmp dir
if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then
- clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; }
- trap clean HUP INT TERM EXIT
+ clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; }
+ trap clean HUP INT TERM EXIT
else
- die "cannot create temp dir"
+ die "cannot create temp dir"
fi
mkdir -p -- "${MAVEN_HOME%/*}"
@@ -174,8 +177,8 @@ verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName"
# select .zip or .tar.gz
if ! command -v unzip >/dev/null; then
- distributionUrl="${distributionUrl%.zip}.tar.gz"
- distributionUrlName="${distributionUrl##*/}"
+ distributionUrl="${distributionUrl%.zip}.tar.gz"
+ distributionUrlName="${distributionUrl##*/}"
fi
# verbose opt
@@ -189,71 +192,104 @@ has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;;
esac
if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then
- verbose "Found wget ... using wget"
- wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl"
+ verbose "Found wget ... using wget"
+ wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl"
elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then
- verbose "Found curl ... using curl"
- curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl"
+ verbose "Found curl ... using curl"
+ curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl"
elif set_java_home; then
- verbose "Falling back to use Java to download"
- javaSource="$TMP_DOWNLOAD_DIR/Downloader.java"
- targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName"
- cat >"$javaSource" <<-END
- public class Downloader extends java.net.Authenticator
- {
- protected java.net.PasswordAuthentication getPasswordAuthentication()
- {
- return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() );
- }
- public static void main( String[] args ) throws Exception
- {
- setDefault( new Downloader() );
- java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() );
- }
- }
+ verbose "Falling back to use Java to download"
+ javaSource="$TMP_DOWNLOAD_DIR/Downloader.java"
+ targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName"
+ cat >"$javaSource" <<-END
+ public class Downloader extends java.net.Authenticator
+ {
+ protected java.net.PasswordAuthentication getPasswordAuthentication()
+ {
+ return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() );
+ }
+ public static void main( String[] args ) throws Exception
+ {
+ setDefault( new Downloader() );
+ java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() );
+ }
+ }
END
- # For Cygwin/MinGW, switch paths to Windows format before running javac and java
- verbose " - Compiling Downloader.java ..."
- "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java"
- verbose " - Running Downloader.java ..."
- "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")"
+ # For Cygwin/MinGW, switch paths to Windows format before running javac and java
+ verbose " - Compiling Downloader.java ..."
+ "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java"
+ verbose " - Running Downloader.java ..."
+ "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")"
fi
# If specified, validate the SHA-256 sum of the Maven distribution zip file
if [ -n "${distributionSha256Sum-}" ]; then
- distributionSha256Result=false
- if [ "$MVN_CMD" = mvnd.sh ]; then
- echo "Checksum validation is not supported for maven-mvnd." >&2
- echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2
- exit 1
- elif command -v sha256sum >/dev/null; then
- if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c >/dev/null 2>&1; then
- distributionSha256Result=true
- fi
- elif command -v shasum >/dev/null; then
- if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then
- distributionSha256Result=true
- fi
- else
- echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2
- echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2
- exit 1
- fi
- if [ $distributionSha256Result = false ]; then
- echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2
- echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2
- exit 1
- fi
+ distributionSha256Result=false
+ if [ "$MVN_CMD" = mvnd.sh ]; then
+ echo "Checksum validation is not supported for maven-mvnd." >&2
+ echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2
+ exit 1
+ elif command -v sha256sum >/dev/null; then
+ if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c - >/dev/null 2>&1; then
+ distributionSha256Result=true
+ fi
+ elif command -v shasum >/dev/null; then
+ if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then
+ distributionSha256Result=true
+ fi
+ else
+ echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2
+ echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2
+ exit 1
+ fi
+ if [ $distributionSha256Result = false ]; then
+ echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2
+ echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2
+ exit 1
+ fi
fi
# unzip and move
if command -v unzip >/dev/null; then
- unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip"
+ unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip"
else
- tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar"
+ tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar"
+fi
+
+# Find the actual extracted directory name (handles snapshots where filename != directory name)
+actualDistributionDir=""
+
+# First try the expected directory name (for regular distributions)
+if [ -d "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" ]; then
+ if [ -f "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/bin/$MVN_CMD" ]; then
+ actualDistributionDir="$distributionUrlNameMain"
+ fi
fi
-printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/mvnw.url"
-mv -- "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME"
+
+# If not found, search for any directory with the Maven executable (for snapshots)
+if [ -z "$actualDistributionDir" ]; then
+ # enable globbing to iterate over items
+ set +f
+ for dir in "$TMP_DOWNLOAD_DIR"/*; do
+ if [ -d "$dir" ]; then
+ if [ -f "$dir/bin/$MVN_CMD" ]; then
+ actualDistributionDir="$(basename "$dir")"
+ break
+ fi
+ fi
+ done
+ set -f
+fi
+
+if [ -z "$actualDistributionDir" ]; then
+ verbose "Contents of $TMP_DOWNLOAD_DIR:"
+ verbose "$(ls -la "$TMP_DOWNLOAD_DIR")"
+ die "Could not find Maven distribution directory in extracted archive"
+fi
+
+verbose "Found extracted Maven distribution directory: $actualDistributionDir"
+printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$actualDistributionDir/mvnw.url"
+mv -- "$TMP_DOWNLOAD_DIR/$actualDistributionDir" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME"
clean || :
exec_maven "$@"
diff --git a/mvnw.cmd b/mvnw.cmd
index 249bdf382..5761d9489 100644
--- a/mvnw.cmd
+++ b/mvnw.cmd
@@ -1,149 +1,189 @@
-<# : batch portion
-@REM ----------------------------------------------------------------------------
-@REM Licensed to the Apache Software Foundation (ASF) under one
-@REM or more contributor license agreements. See the NOTICE file
-@REM distributed with this work for additional information
-@REM regarding copyright ownership. The ASF licenses this file
-@REM to you under the Apache License, Version 2.0 (the
-@REM "License"); you may not use this file except in compliance
-@REM with the License. You may obtain a copy of the License at
-@REM
-@REM http://www.apache.org/licenses/LICENSE-2.0
-@REM
-@REM Unless required by applicable law or agreed to in writing,
-@REM software distributed under the License is distributed on an
-@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-@REM KIND, either express or implied. See the License for the
-@REM specific language governing permissions and limitations
-@REM under the License.
-@REM ----------------------------------------------------------------------------
-
-@REM ----------------------------------------------------------------------------
-@REM Apache Maven Wrapper startup batch script, version 3.3.2
-@REM
-@REM Optional ENV vars
-@REM MVNW_REPOURL - repo url base for downloading maven distribution
-@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven
-@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output
-@REM ----------------------------------------------------------------------------
-
-@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0)
-@SET __MVNW_CMD__=
-@SET __MVNW_ERROR__=
-@SET __MVNW_PSMODULEP_SAVE=%PSModulePath%
-@SET PSModulePath=
-@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @(
- IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B)
-)
-@SET PSModulePath=%__MVNW_PSMODULEP_SAVE%
-@SET __MVNW_PSMODULEP_SAVE=
-@SET __MVNW_ARG0_NAME__=
-@SET MVNW_USERNAME=
-@SET MVNW_PASSWORD=
-@IF NOT "%__MVNW_CMD__%"=="" (%__MVNW_CMD__% %*)
-@echo Cannot start maven from wrapper >&2 && exit /b 1
-@GOTO :EOF
-: end batch / begin powershell #>
-
-$ErrorActionPreference = "Stop"
-if ($env:MVNW_VERBOSE -eq "true") {
- $VerbosePreference = "Continue"
-}
-
-# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties
-$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl
-if (!$distributionUrl) {
- Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties"
-}
-
-switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) {
- "maven-mvnd-*" {
- $USE_MVND = $true
- $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip"
- $MVN_CMD = "mvnd.cmd"
- break
- }
- default {
- $USE_MVND = $false
- $MVN_CMD = $script -replace '^mvnw','mvn'
- break
- }
-}
-
-# apply MVNW_REPOURL and calculate MAVEN_HOME
-# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/
-if ($env:MVNW_REPOURL) {
- $MVNW_REPO_PATTERN = if ($USE_MVND) { "/org/apache/maven/" } else { "/maven/mvnd/" }
- $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace '^.*'+$MVNW_REPO_PATTERN,'')"
-}
-$distributionUrlName = $distributionUrl -replace '^.*/',''
-$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$',''
-$MAVEN_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain"
-if ($env:MAVEN_USER_HOME) {
- $MAVEN_HOME_PARENT = "$env:MAVEN_USER_HOME/wrapper/dists/$distributionUrlNameMain"
-}
-$MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join ''
-$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME"
-
-if (Test-Path -Path "$MAVEN_HOME" -PathType Container) {
- Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME"
- Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"
- exit $?
-}
-
-if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) {
- Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl"
-}
-
-# prepare tmp dir
-$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile
-$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir"
-$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null
-trap {
- if ($TMP_DOWNLOAD_DIR.Exists) {
- try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
- catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
- }
-}
-
-New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null
-
-# Download and Install Apache Maven
-Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..."
-Write-Verbose "Downloading from: $distributionUrl"
-Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName"
-
-$webclient = New-Object System.Net.WebClient
-if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) {
- $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD)
-}
-[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
-$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null
-
-# If specified, validate the SHA-256 sum of the Maven distribution zip file
-$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum
-if ($distributionSha256Sum) {
- if ($USE_MVND) {
- Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties."
- }
- Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash
- if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) {
- Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property."
- }
-}
-
-# unzip and move
-Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null
-Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -NewName $MAVEN_HOME_NAME | Out-Null
-try {
- Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null
-} catch {
- if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) {
- Write-Error "fail to move MAVEN_HOME"
- }
-} finally {
- try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
- catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
-}
-
-Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"
+<# : batch portion
+@REM ----------------------------------------------------------------------------
+@REM Licensed to the Apache Software Foundation (ASF) under one
+@REM or more contributor license agreements. See the NOTICE file
+@REM distributed with this work for additional information
+@REM regarding copyright ownership. The ASF licenses this file
+@REM to you under the Apache License, Version 2.0 (the
+@REM "License"); you may not use this file except in compliance
+@REM with the License. You may obtain a copy of the License at
+@REM
+@REM http://www.apache.org/licenses/LICENSE-2.0
+@REM
+@REM Unless required by applicable law or agreed to in writing,
+@REM software distributed under the License is distributed on an
+@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+@REM KIND, either express or implied. See the License for the
+@REM specific language governing permissions and limitations
+@REM under the License.
+@REM ----------------------------------------------------------------------------
+
+@REM ----------------------------------------------------------------------------
+@REM Apache Maven Wrapper startup batch script, version 3.3.4
+@REM
+@REM Optional ENV vars
+@REM MVNW_REPOURL - repo url base for downloading maven distribution
+@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven
+@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output
+@REM ----------------------------------------------------------------------------
+
+@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0)
+@SET __MVNW_CMD__=
+@SET __MVNW_ERROR__=
+@SET __MVNW_PSMODULEP_SAVE=%PSModulePath%
+@SET PSModulePath=
+@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @(
+ IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B)
+)
+@SET PSModulePath=%__MVNW_PSMODULEP_SAVE%
+@SET __MVNW_PSMODULEP_SAVE=
+@SET __MVNW_ARG0_NAME__=
+@SET MVNW_USERNAME=
+@SET MVNW_PASSWORD=
+@IF NOT "%__MVNW_CMD__%"=="" ("%__MVNW_CMD__%" %*)
+@echo Cannot start maven from wrapper >&2 && exit /b 1
+@GOTO :EOF
+: end batch / begin powershell #>
+
+$ErrorActionPreference = "Stop"
+if ($env:MVNW_VERBOSE -eq "true") {
+ $VerbosePreference = "Continue"
+}
+
+# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties
+$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl
+if (!$distributionUrl) {
+ Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties"
+}
+
+switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) {
+ "maven-mvnd-*" {
+ $USE_MVND = $true
+ $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip"
+ $MVN_CMD = "mvnd.cmd"
+ break
+ }
+ default {
+ $USE_MVND = $false
+ $MVN_CMD = $script -replace '^mvnw','mvn'
+ break
+ }
+}
+
+# apply MVNW_REPOURL and calculate MAVEN_HOME
+# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/
+if ($env:MVNW_REPOURL) {
+ $MVNW_REPO_PATTERN = if ($USE_MVND -eq $False) { "/org/apache/maven/" } else { "/maven/mvnd/" }
+ $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace "^.*$MVNW_REPO_PATTERN",'')"
+}
+$distributionUrlName = $distributionUrl -replace '^.*/',''
+$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$',''
+
+$MAVEN_M2_PATH = "$HOME/.m2"
+if ($env:MAVEN_USER_HOME) {
+ $MAVEN_M2_PATH = "$env:MAVEN_USER_HOME"
+}
+
+if (-not (Test-Path -Path $MAVEN_M2_PATH)) {
+ New-Item -Path $MAVEN_M2_PATH -ItemType Directory | Out-Null
+}
+
+$MAVEN_WRAPPER_DISTS = $null
+if ((Get-Item $MAVEN_M2_PATH).Target[0] -eq $null) {
+ $MAVEN_WRAPPER_DISTS = "$MAVEN_M2_PATH/wrapper/dists"
+} else {
+ $MAVEN_WRAPPER_DISTS = (Get-Item $MAVEN_M2_PATH).Target[0] + "/wrapper/dists"
+}
+
+$MAVEN_HOME_PARENT = "$MAVEN_WRAPPER_DISTS/$distributionUrlNameMain"
+$MAVEN_HOME_NAME = ([System.Security.Cryptography.SHA256]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join ''
+$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME"
+
+if (Test-Path -Path "$MAVEN_HOME" -PathType Container) {
+ Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME"
+ Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"
+ exit $?
+}
+
+if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) {
+ Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl"
+}
+
+# prepare tmp dir
+$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile
+$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir"
+$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null
+trap {
+ if ($TMP_DOWNLOAD_DIR.Exists) {
+ try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
+ catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
+ }
+}
+
+New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null
+
+# Download and Install Apache Maven
+Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..."
+Write-Verbose "Downloading from: $distributionUrl"
+Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName"
+
+$webclient = New-Object System.Net.WebClient
+if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) {
+ $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD)
+}
+[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
+$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null
+
+# If specified, validate the SHA-256 sum of the Maven distribution zip file
+$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum
+if ($distributionSha256Sum) {
+ if ($USE_MVND) {
+ Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties."
+ }
+ Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash
+ if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) {
+ Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property."
+ }
+}
+
+# unzip and move
+Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null
+
+# Find the actual extracted directory name (handles snapshots where filename != directory name)
+$actualDistributionDir = ""
+
+# First try the expected directory name (for regular distributions)
+$expectedPath = Join-Path "$TMP_DOWNLOAD_DIR" "$distributionUrlNameMain"
+$expectedMvnPath = Join-Path "$expectedPath" "bin/$MVN_CMD"
+if ((Test-Path -Path $expectedPath -PathType Container) -and (Test-Path -Path $expectedMvnPath -PathType Leaf)) {
+ $actualDistributionDir = $distributionUrlNameMain
+}
+
+# If not found, search for any directory with the Maven executable (for snapshots)
+if (!$actualDistributionDir) {
+ Get-ChildItem -Path "$TMP_DOWNLOAD_DIR" -Directory | ForEach-Object {
+ $testPath = Join-Path $_.FullName "bin/$MVN_CMD"
+ if (Test-Path -Path $testPath -PathType Leaf) {
+ $actualDistributionDir = $_.Name
+ }
+ }
+}
+
+if (!$actualDistributionDir) {
+ Write-Error "Could not find Maven distribution directory in extracted archive"
+}
+
+Write-Verbose "Found extracted Maven distribution directory: $actualDistributionDir"
+Rename-Item -Path "$TMP_DOWNLOAD_DIR/$actualDistributionDir" -NewName $MAVEN_HOME_NAME | Out-Null
+try {
+ Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null
+} catch {
+ if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) {
+ Write-Error "fail to move MAVEN_HOME"
+ }
+} finally {
+ try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
+ catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
+}
+
+Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"
diff --git a/oats-config.yaml b/oats-config.yaml
new file mode 100644
index 000000000..d23490ffb
--- /dev/null
+++ b/oats-config.yaml
@@ -0,0 +1,6 @@
+meta:
+ version: 3
+
+cases:
+ - examples/example-exporter-opentelemetry/oats-tests/http/oats-case.yaml
+ - examples/example-exporter-opentelemetry/oats-tests/agent/oats-case.yaml
diff --git a/pom.xml b/pom.xml
index 941f6df2b..53781fb59 100644
--- a/pom.xml
+++ b/pom.xml
@@ -1,18 +1,15 @@
-
+
pom
4.0.0
io.prometheus
client_java_parent
- 1.4.0-SNAPSHOT
+ 1.8.1-SNAPSHOT
prometheus-metrics-parent/pom.xml
- 1.4.0-SNAPSHOT
client_java
Prometheus Metrics Library
@@ -23,21 +20,26 @@
UTF-8
--module-name-need-to-be-overridden--
- 4.31.1
- 33.4.8-jre
- 5.13.4
- 2.16.0-alpha
+ 4.36.0
+ 33.7.1-jre
+ 2.3.0
+ 4.3.0
+ 3.13.2
+ 6.1.3
+ 2.31.0-alpha
8
+ 25
+
+ 1.8.0
0.70
- false
false
- false
false
-Werror
prometheus-metrics-parent
+ prometheus-metrics-annotations
prometheus-metrics-bom
prometheus-metrics-core
prometheus-metrics-config
@@ -60,6 +62,7 @@
prometheus-metrics-instrumentation-dropwizard
prometheus-metrics-instrumentation-guava
prometheus-metrics-simpleclient-bridge
+ prometheus-metrics-otel-support
@@ -69,6 +72,37 @@
3.0.2
provided
+
+
+ org.junit.jupiter
+ junit-jupiter
+ ${junit-jupiter.version}
+ test
+
+
+ org.junit.jupiter
+ junit-jupiter-params
+ ${junit-jupiter.version}
+ test
+
+
+ org.mockito
+ mockito-core
+ 5.23.0
+ test
+
+
+ org.assertj
+ assertj-core
+ 3.27.7
+ test
+
+
+ org.slf4j
+ slf4j-simple
+ 2.0.18
+ test
+
@@ -82,19 +116,19 @@
maven-resources-plugin
- 3.3.1
+ 3.5.0
maven-compiler-plugin
- 3.14.0
+ 3.15.0
maven-surefire-plugin
- 3.5.3
+ 3.5.6
maven-jar-plugin
- 3.4.2
+ 3.5.1
maven-deploy-plugin
@@ -106,28 +140,28 @@
maven-site-plugin
- 3.21.0
+ 3.22.0
maven-shade-plugin
- 3.6.0
+ 3.6.2
maven-failsafe-plugin
- 3.5.3
+ 3.5.6
maven-dependency-plugin
- 3.8.1
+ 3.11.0
maven-javadoc-plugin
- 3.11.2
+ 3.12.0
maven-enforcer-plugin
- 3.6.1
+ 3.6.3
org.codehaus.mojo
@@ -137,42 +171,27 @@
org.codehaus.mojo
exec-maven-plugin
- 3.5.1
+ 3.6.3
+
+
+ com.github.siom79.japicmp
+ japicmp-maven-plugin
+ 0.26.1
-
- org.apache.maven.plugins
- maven-checkstyle-plugin
- 3.6.0
-
- true
- google_checks.xml
- checkstyle.xml
- warning
- true
- ${checkstyle.skip}
- checkstyle-suppressions.xml
- **/generated/**,**/jmh_generated/*
-
-
-
-
- check
-
-
-
-
org.jacoco
jacoco-maven-plugin
- 0.8.13
+ 0.8.15
${coverage.skip}
**/generated/**
**/*BlockingRejectedExecutionHandler*
+ **/*AllocationCountingNotificationListener*
+ **/*MapperConfig*
@@ -203,6 +222,11 @@
COVEREDRATIO
${jacoco.line-coverage}
+
+ BRANCH
+ COVEREDRATIO
+ 0.50
+
@@ -234,7 +258,7 @@
org.apache.felix
maven-bundle-plugin
- 6.0.0
+ 6.1.0
true
@@ -252,56 +276,62 @@
${java.version}
${java.version}
${java.version}
- 17
- 17
- 17
+ ${test.java.version}
+ ${test.java.version}
+ ${test.java.version}
true
-Xlint:all,-serial,-processing,-options
${warnings}
--should-stop=ifError=FLOW
-XDcompilePolicy=simple
-
- -Xplugin:ErrorProne
- -Xep:AlmostJavadoc:OFF
- -Xep:MissingSummary:OFF
- -Xep:LongDoubleConversion:OFF
- -Xep:StringSplitter:OFF
- -XepExcludedPaths:.*/generated/.*
-
-
-
- com.google.errorprone
- error_prone_core
- 2.40.0
-
-
-
org.codehaus.mojo
versions-maven-plugin
- 2.18.0
+ 2.21.0
file://${project.basedir}/version-rules.xml
+
+ maven-javadoc-plugin
+
+ ${javadoc.skip}
+
+
+
+
+
+ org.junit
+ junit-bom
+ ${junit-jupiter.version}
+ pom
+ import
+
+
+ io.opentelemetry.instrumentation
+ opentelemetry-instrumentation-bom-alpha
+ ${otel.instrumentation.version}
+ pom
+ import
+
+
+ io.opentelemetry
+ opentelemetry-proto
+ 1.7.1-alpha
+ test
+
+
+
+
-
-
- maven-project-info-reports-plugin
- 3.9.0
-
maven-javadoc-plugin
@@ -325,102 +355,15 @@
- default
+ examples-and-integration-tests
- true
+ [25,)
examples
benchmarks
integration-tests
-
-
-
- org.junit
- junit-bom
- ${junit-jupiter.version}
- pom
- import
-
-
- io.opentelemetry.instrumentation
- opentelemetry-instrumentation-bom-alpha
- ${otel.instrumentation.version}
- pom
- import
-
-
- io.opentelemetry
- opentelemetry-proto
- 1.7.1-alpha
- test
-
-
-
-
-
-
- org.junit.jupiter
- junit-jupiter
- ${junit-jupiter.version}
- test
-
-
- org.junit.jupiter
- junit-jupiter-params
- ${junit-jupiter.version}
- test
-
-
- org.mockito
- mockito-core
- 5.18.0
- test
-
-
- org.assertj
- assertj-core
- 3.27.3
- test
-
-
- com.google.guava
- guava
- ${guava.version}
- test
-
-
- org.slf4j
- slf4j-simple
- 2.0.17
- test
-
-
- org.junit-pioneer
- junit-pioneer
- 2.3.0
- test
-
-
- org.awaitility
- awaitility
- 4.3.0
- test
-
-
- org.wiremock
- wiremock
- 3.13.1
- test
-
-
- org.hamcrest
- hamcrest-core
-
-
-
-
javadoc
@@ -429,6 +372,7 @@
maven-javadoc-plugin
+ ${javadoc.skip}
UTF-8
UTF-8
true
@@ -442,6 +386,110 @@
+
+ api-diff
+
+
+
+ com.github.siom79.japicmp
+ japicmp-maven-plugin
+
+
+
+ ${project.groupId}
+ ${project.artifactId}
+ ${api.diff.baseline.version}
+ jar
+
+
+
+
+ ${project.build.directory}/${project.build.finalName}.jar
+
+
+
+ public
+ true
+
+ io.prometheus.metrics.annotations.StableApi
+ @io.prometheus.metrics.annotations.StableApi
+
+
+ io.prometheus.metrics.expositionformats.generated
+ io.prometheus.metrics.shaded
+
+ false
+
+ false
+
+
+ false
+
+ false
+ true
+ true
+ true
+ true
+ true
+ true
+
+ bundle
+ jar
+
+
+
+
+
+ api-diff
+ verify
+
+ cmp
+
+
+
+
+
+
+
+
+ errorprone
+
+ [21,)
+
+
+
+
+ maven-compiler-plugin
+
+
+ -XDaddTypeAnnotationsToSymbol=true
+
+ -Xplugin:ErrorProne
+ -Xep:AlmostJavadoc:OFF
+ -Xep:MissingSummary:OFF
+ -Xep:LongDoubleConversion:OFF
+ -Xep:StringSplitter:OFF
+ -XepExcludedPaths:(.*/generated/.*|.*/src/test/java/.*|.*/examples/.*|.*/integration-tests/.*)
+ -XepOpt:NullAway:AnnotatedPackages=io.prometheus.metrics
+
+
+
+
+ com.google.errorprone
+ error_prone_core
+ 2.50.0
+
+
+ com.uber.nullaway
+ nullaway
+ 0.13.8
+
+
+
+
+
+
+
release
diff --git a/prometheus-metrics-annotations/pom.xml b/prometheus-metrics-annotations/pom.xml
new file mode 100644
index 000000000..3552a8ddb
--- /dev/null
+++ b/prometheus-metrics-annotations/pom.xml
@@ -0,0 +1,22 @@
+
+
+ 4.0.0
+
+
+ io.prometheus
+ client_java
+ 1.8.1-SNAPSHOT
+
+
+ prometheus-metrics-annotations
+ bundle
+
+ Prometheus Metrics Annotations
+
+ Annotations for Prometheus Metrics library API contracts.
+
+
+
+ io.prometheus.metrics.annotations
+
+
diff --git a/prometheus-metrics-annotations/src/main/java/io/prometheus/metrics/annotations/StableApi.java b/prometheus-metrics-annotations/src/main/java/io/prometheus/metrics/annotations/StableApi.java
new file mode 100644
index 000000000..a3bfe89ae
--- /dev/null
+++ b/prometheus-metrics-annotations/src/main/java/io/prometheus/metrics/annotations/StableApi.java
@@ -0,0 +1,22 @@
+package io.prometheus.metrics.annotations;
+
+import static java.lang.annotation.ElementType.CONSTRUCTOR;
+import static java.lang.annotation.ElementType.FIELD;
+import static java.lang.annotation.ElementType.METHOD;
+import static java.lang.annotation.ElementType.TYPE;
+import static java.lang.annotation.RetentionPolicy.CLASS;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+/**
+ * Marks a Java element as part of the stable, published Prometheus Metrics API.
+ *
+ * Use this on public or protected types to publish the type and its members. Use it on
+ * individual constructors, methods, and fields when only part of a public type is stable.
+ */
+@Documented
+@Retention(CLASS)
+@Target({TYPE, CONSTRUCTOR, METHOD, FIELD})
+public @interface StableApi {}
diff --git a/prometheus-metrics-bom/pom.xml b/prometheus-metrics-bom/pom.xml
index 242710819..c2c9935f7 100644
--- a/prometheus-metrics-bom/pom.xml
+++ b/prometheus-metrics-bom/pom.xml
@@ -1,12 +1,11 @@
-
+
4.0.0
io.prometheus
client_java_parent
- 1.4.0-SNAPSHOT
+ 1.8.1-SNAPSHOT
../prometheus-metrics-parent/pom.xml
@@ -18,12 +17,13 @@
Bill of Materials for the Prometheus Metrics library
-
- true
-
-
+
+ io.prometheus
+ prometheus-metrics-annotations
+ ${project.version}
+
io.prometheus
prometheus-metrics-config
@@ -119,6 +119,12 @@
prometheus-metrics-model
${project.version}
+
+ io.prometheus
+ prometheus-metrics-otel-support
+ ${project.version}
+ pom
+
io.prometheus
prometheus-metrics-simpleclient-bridge
diff --git a/prometheus-metrics-config/pom.xml b/prometheus-metrics-config/pom.xml
index 4546c3dc9..681f4a557 100644
--- a/prometheus-metrics-config/pom.xml
+++ b/prometheus-metrics-config/pom.xml
@@ -1,12 +1,11 @@
-
+
4.0.0
io.prometheus
client_java
- 1.4.0-SNAPSHOT
+ 1.8.1-SNAPSHOT
prometheus-metrics-config
@@ -21,4 +20,19 @@
io.prometheus.metrics.config
+
+
+ io.prometheus
+ prometheus-metrics-annotations
+ ${project.version}
+ true
+
+
+ org.junit-pioneer
+ junit-pioneer
+ ${junit-pioneer.version}
+ test
+
+
+
diff --git a/prometheus-metrics-config/src/main/java/io/prometheus/metrics/config/EscapingScheme.java b/prometheus-metrics-config/src/main/java/io/prometheus/metrics/config/EscapingScheme.java
new file mode 100644
index 000000000..1d6bd37cf
--- /dev/null
+++ b/prometheus-metrics-config/src/main/java/io/prometheus/metrics/config/EscapingScheme.java
@@ -0,0 +1,87 @@
+package io.prometheus.metrics.config;
+
+import io.prometheus.metrics.annotations.StableApi;
+import javax.annotation.Nullable;
+
+@StableApi
+public enum EscapingScheme {
+ /** NO_ESCAPING indicates that a name will not be escaped. */
+ ALLOW_UTF8("allow-utf-8"),
+
+ /** UNDERSCORE_ESCAPING replaces all legacy-invalid characters with underscores. */
+ UNDERSCORE_ESCAPING("underscores"),
+
+ /**
+ * DOTS_ESCAPING is similar to UNDERSCORE_ESCAPING, except that dots are converted to `_dot_` and
+ * pre-existing underscores are converted to `__`.
+ */
+ DOTS_ESCAPING("dots"),
+
+ /**
+ * VALUE_ENCODING_ESCAPING prepends the name with `U__` and replaces all invalid characters with
+ * the Unicode value, surrounded by underscores. Single underscores are replaced with double
+ * underscores.
+ */
+ VALUE_ENCODING_ESCAPING("values");
+
+ private static final String ESCAPING_KEY = "escaping";
+
+ /** Default escaping scheme for names when not specified. */
+ public static final EscapingScheme DEFAULT = UNDERSCORE_ESCAPING;
+
+ public final String getValue() {
+ return value;
+ }
+
+ private final String value;
+
+ EscapingScheme(String value) {
+ this.value = value;
+ }
+
+ /**
+ * fromAcceptHeader returns an EscapingScheme depending on the Accept header. Iff the header
+ * contains an escaping=allow-utf-8 term, it will select NO_ESCAPING. If a valid "escaping" term
+ * exists, that will be used. Otherwise, the global default will be returned.
+ */
+ public static EscapingScheme fromAcceptHeader(@Nullable String acceptHeader) {
+ if (acceptHeader != null) {
+ for (String p : acceptHeader.split(";")) {
+ String[] toks = p.split("=");
+ if (toks.length != 2) {
+ continue;
+ }
+ String key = toks[0].trim();
+ String value = toks[1].trim();
+ if (key.equals(ESCAPING_KEY)) {
+ try {
+ return EscapingScheme.forString(value);
+ } catch (IllegalArgumentException e) {
+ // If the escaping parameter is unknown, ignore it.
+ return DEFAULT;
+ }
+ }
+ }
+ }
+ return DEFAULT;
+ }
+
+ static EscapingScheme forString(String value) {
+ switch (value) {
+ case "allow-utf-8":
+ return ALLOW_UTF8;
+ case "underscores":
+ return UNDERSCORE_ESCAPING;
+ case "dots":
+ return DOTS_ESCAPING;
+ case "values":
+ return VALUE_ENCODING_ESCAPING;
+ default:
+ throw new IllegalArgumentException("Unknown escaping scheme: " + value);
+ }
+ }
+
+ public String toHeaderFormat() {
+ return "; " + ESCAPING_KEY + "=" + value;
+ }
+}
diff --git a/prometheus-metrics-config/src/main/java/io/prometheus/metrics/config/ExemplarsProperties.java b/prometheus-metrics-config/src/main/java/io/prometheus/metrics/config/ExemplarsProperties.java
index 017d67909..93190ef19 100644
--- a/prometheus-metrics-config/src/main/java/io/prometheus/metrics/config/ExemplarsProperties.java
+++ b/prometheus-metrics-config/src/main/java/io/prometheus/metrics/config/ExemplarsProperties.java
@@ -1,23 +1,25 @@
package io.prometheus.metrics.config;
-import java.util.Map;
+import io.prometheus.metrics.annotations.StableApi;
+import javax.annotation.Nullable;
/** Properties starting with io.prometheus.exemplars */
+@StableApi
public class ExemplarsProperties {
private static final String PREFIX = "io.prometheus.exemplars";
- private static final String MIN_RETENTION_PERIOD_SECONDS = "minRetentionPeriodSeconds";
- private static final String MAX_RETENTION_PERIOD_SECONDS = "maxRetentionPeriodSeconds";
- private static final String SAMPLE_INTERVAL_MILLISECONDS = "sampleIntervalMilliseconds";
+ private static final String MIN_RETENTION_PERIOD_SECONDS = "min_retention_period_seconds";
+ private static final String MAX_RETENTION_PERIOD_SECONDS = "max_retention_period_seconds";
+ private static final String SAMPLE_INTERVAL_MILLISECONDS = "sample_interval_milliseconds";
- private final Integer minRetentionPeriodSeconds;
- private final Integer maxRetentionPeriodSeconds;
- private final Integer sampleIntervalMilliseconds;
+ @Nullable private final Integer minRetentionPeriodSeconds;
+ @Nullable private final Integer maxRetentionPeriodSeconds;
+ @Nullable private final Integer sampleIntervalMilliseconds;
private ExemplarsProperties(
- Integer minRetentionPeriodSeconds,
- Integer maxRetentionPeriodSeconds,
- Integer sampleIntervalMilliseconds) {
+ @Nullable Integer minRetentionPeriodSeconds,
+ @Nullable Integer maxRetentionPeriodSeconds,
+ @Nullable Integer sampleIntervalMilliseconds) {
this.minRetentionPeriodSeconds = minRetentionPeriodSeconds;
this.maxRetentionPeriodSeconds = maxRetentionPeriodSeconds;
this.sampleIntervalMilliseconds = sampleIntervalMilliseconds;
@@ -28,6 +30,7 @@ private ExemplarsProperties(
*
* Default see {@code ExemplarSamplerConfig.DEFAULT_MIN_RETENTION_PERIOD_SECONDS}
*/
+ @Nullable
public Integer getMinRetentionPeriodSeconds() {
return minRetentionPeriodSeconds;
}
@@ -37,6 +40,7 @@ public Integer getMinRetentionPeriodSeconds() {
*
*
Default see {@code ExemplarSamplerConfig.DEFAULT_MAX_RETENTION_PERIOD_SECONDS}
*/
+ @Nullable
public Integer getMaxRetentionPeriodSeconds() {
return maxRetentionPeriodSeconds;
}
@@ -48,22 +52,23 @@ public Integer getMaxRetentionPeriodSeconds() {
*
*
Default see {@code ExemplarSamplerConfig.DEFAULT_SAMPLE_INTERVAL_MILLISECONDS}
*/
+ @Nullable
public Integer getSampleIntervalMilliseconds() {
return sampleIntervalMilliseconds;
}
/**
- * Note that this will remove entries from {@code properties}. This is because we want to know if
- * there are unused properties remaining after all properties have been loaded.
+ * Note that this will remove entries from {@code propertySource}. This is because we want to know
+ * if there are unused properties remaining after all properties have been loaded.
*/
- static ExemplarsProperties load(Map properties)
+ static ExemplarsProperties load(PropertySource propertySource)
throws PrometheusPropertiesException {
Integer minRetentionPeriodSeconds =
- Util.loadInteger(PREFIX + "." + MIN_RETENTION_PERIOD_SECONDS, properties);
+ Util.loadInteger(PREFIX, MIN_RETENTION_PERIOD_SECONDS, propertySource);
Integer maxRetentionPeriodSeconds =
- Util.loadInteger(PREFIX + "." + MAX_RETENTION_PERIOD_SECONDS, properties);
+ Util.loadInteger(PREFIX, MAX_RETENTION_PERIOD_SECONDS, propertySource);
Integer sampleIntervalMilliseconds =
- Util.loadInteger(PREFIX + "." + SAMPLE_INTERVAL_MILLISECONDS, properties);
+ Util.loadInteger(PREFIX, SAMPLE_INTERVAL_MILLISECONDS, propertySource);
Util.assertValue(
minRetentionPeriodSeconds,
@@ -108,9 +113,9 @@ public static Builder builder() {
public static class Builder {
- private Integer minRetentionPeriodSeconds;
- private Integer maxRetentionPeriodSeconds;
- private Integer sampleIntervalMilliseconds;
+ @Nullable private Integer minRetentionPeriodSeconds;
+ @Nullable private Integer maxRetentionPeriodSeconds;
+ @Nullable private Integer sampleIntervalMilliseconds;
private Builder() {}
diff --git a/prometheus-metrics-config/src/main/java/io/prometheus/metrics/config/ExporterFilterProperties.java b/prometheus-metrics-config/src/main/java/io/prometheus/metrics/config/ExporterFilterProperties.java
index c2c3d48d3..999c2c8e8 100644
--- a/prometheus-metrics-config/src/main/java/io/prometheus/metrics/config/ExporterFilterProperties.java
+++ b/prometheus-metrics-config/src/main/java/io/prometheus/metrics/config/ExporterFilterProperties.java
@@ -1,30 +1,32 @@
package io.prometheus.metrics.config;
+import io.prometheus.metrics.annotations.StableApi;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
-import java.util.Map;
+import javax.annotation.Nullable;
/** Properties starting with io.prometheus.exporter.filter */
+@StableApi
public class ExporterFilterProperties {
- public static final String METRIC_NAME_MUST_BE_EQUAL_TO = "metricNameMustBeEqualTo";
- public static final String METRIC_NAME_MUST_NOT_BE_EQUAL_TO = "metricNameMustNotBeEqualTo";
- public static final String METRIC_NAME_MUST_START_WITH = "metricNameMustStartWith";
- public static final String METRIC_NAME_MUST_NOT_START_WITH = "metricNameMustNotStartWith";
+ public static final String METRIC_NAME_MUST_BE_EQUAL_TO = "metric_name_must_be_equal_to";
+ public static final String METRIC_NAME_MUST_NOT_BE_EQUAL_TO = "metric_name_must_not_be_equal_to";
+ public static final String METRIC_NAME_MUST_START_WITH = "metric_name_must_start_with";
+ public static final String METRIC_NAME_MUST_NOT_START_WITH = "metric_name_must_not_start_with";
private static final String PREFIX = "io.prometheus.exporter.filter";
- private final List allowedNames;
- private final List excludedNames;
- private final List allowedPrefixes;
- private final List excludedPrefixes;
+ @Nullable private final List allowedNames;
+ @Nullable private final List excludedNames;
+ @Nullable private final List allowedPrefixes;
+ @Nullable private final List excludedPrefixes;
private ExporterFilterProperties(
- List allowedNames,
- List excludedNames,
- List allowedPrefixes,
- List excludedPrefixes) {
+ @Nullable List allowedNames,
+ @Nullable List excludedNames,
+ @Nullable List allowedPrefixes,
+ @Nullable List excludedPrefixes) {
this.allowedNames =
allowedNames == null ? null : Collections.unmodifiableList(new ArrayList<>(allowedNames));
this.excludedNames =
@@ -39,36 +41,40 @@ private ExporterFilterProperties(
: Collections.unmodifiableList(new ArrayList<>(excludedPrefixes));
}
+ @Nullable
public List getAllowedMetricNames() {
return allowedNames;
}
+ @Nullable
public List getExcludedMetricNames() {
return excludedNames;
}
+ @Nullable
public List getAllowedMetricNamePrefixes() {
return allowedPrefixes;
}
+ @Nullable
public List getExcludedMetricNamePrefixes() {
return excludedPrefixes;
}
/**
- * Note that this will remove entries from {@code properties}. This is because we want to know if
- * there are unused properties remaining after all properties have been loaded.
+ * Note that this will remove entries from {@code propertySource}. This is because we want to know
+ * if there are unused properties remaining after all properties have been loaded.
*/
- static ExporterFilterProperties load(Map properties)
+ static ExporterFilterProperties load(PropertySource propertySource)
throws PrometheusPropertiesException {
List allowedNames =
- Util.loadStringList(PREFIX + "." + METRIC_NAME_MUST_BE_EQUAL_TO, properties);
+ Util.loadStringList(PREFIX, METRIC_NAME_MUST_BE_EQUAL_TO, propertySource);
List excludedNames =
- Util.loadStringList(PREFIX + "." + METRIC_NAME_MUST_NOT_BE_EQUAL_TO, properties);
+ Util.loadStringList(PREFIX, METRIC_NAME_MUST_NOT_BE_EQUAL_TO, propertySource);
List allowedPrefixes =
- Util.loadStringList(PREFIX + "." + METRIC_NAME_MUST_START_WITH, properties);
+ Util.loadStringList(PREFIX, METRIC_NAME_MUST_START_WITH, propertySource);
List excludedPrefixes =
- Util.loadStringList(PREFIX + "." + METRIC_NAME_MUST_NOT_START_WITH, properties);
+ Util.loadStringList(PREFIX, METRIC_NAME_MUST_NOT_START_WITH, propertySource);
return new ExporterFilterProperties(
allowedNames, excludedNames, allowedPrefixes, excludedPrefixes);
}
@@ -79,10 +85,10 @@ public static Builder builder() {
public static class Builder {
- private List allowedNames;
- private List excludedNames;
- private List allowedPrefixes;
- private List excludedPrefixes;
+ @Nullable private List allowedNames;
+ @Nullable private List excludedNames;
+ @Nullable private List allowedPrefixes;
+ @Nullable private List excludedPrefixes;
private Builder() {}
diff --git a/prometheus-metrics-config/src/main/java/io/prometheus/metrics/config/ExporterHttpServerProperties.java b/prometheus-metrics-config/src/main/java/io/prometheus/metrics/config/ExporterHttpServerProperties.java
index 6618ab88e..4f921c2ac 100644
--- a/prometheus-metrics-config/src/main/java/io/prometheus/metrics/config/ExporterHttpServerProperties.java
+++ b/prometheus-metrics-config/src/main/java/io/prometheus/metrics/config/ExporterHttpServerProperties.java
@@ -1,31 +1,46 @@
package io.prometheus.metrics.config;
-import java.util.Map;
+import io.prometheus.metrics.annotations.StableApi;
+import javax.annotation.Nullable;
-/** Properties starting with io.prometheus.exporter.httpServer */
+/** Properties starting with io.prometheus.exporter.http_server */
+@StableApi
public class ExporterHttpServerProperties {
private static final String PORT = "port";
- private static final String PREFIX = "io.prometheus.exporter.httpServer";
- private final Integer port;
+ private static final String PREFER_UNCOMPRESSED_RESPONSE = "prefer_uncompressed_response";
+ private static final String PREFIX = "io.prometheus.exporter.http_server";
+ @Nullable private final Integer port;
+ private final boolean preferUncompressedResponse;
- private ExporterHttpServerProperties(Integer port) {
+ private ExporterHttpServerProperties(@Nullable Integer port, boolean preferUncompressedResponse) {
this.port = port;
+ this.preferUncompressedResponse = preferUncompressedResponse;
}
+ @Nullable
public Integer getPort() {
return port;
}
+ public boolean isPreferUncompressedResponse() {
+ return preferUncompressedResponse;
+ }
+
/**
- * Note that this will remove entries from {@code properties}. This is because we want to know if
- * there are unused properties remaining after all properties have been loaded.
+ * Note that this will remove entries from {@code propertySource}. This is because we want to know
+ * if there are unused properties remaining after all properties have been loaded.
*/
- static ExporterHttpServerProperties load(Map properties)
+ static ExporterHttpServerProperties load(PropertySource propertySource)
throws PrometheusPropertiesException {
- Integer port = Util.loadInteger(PREFIX + "." + PORT, properties);
+ Integer port = Util.loadInteger(PREFIX, PORT, propertySource);
Util.assertValue(port, t -> t > 0, "Expecting value > 0.", PREFIX, PORT);
- return new ExporterHttpServerProperties(port);
+
+ Boolean preferUncompressedResponse =
+ Util.loadBoolean(PREFIX, PREFER_UNCOMPRESSED_RESPONSE, propertySource);
+
+ return new ExporterHttpServerProperties(
+ port, preferUncompressedResponse != null && preferUncompressedResponse);
}
public static Builder builder() {
@@ -34,7 +49,8 @@ public static Builder builder() {
public static class Builder {
- private Integer port;
+ @Nullable private Integer port;
+ private boolean preferUncompressedResponse = false;
private Builder() {}
@@ -43,8 +59,13 @@ public Builder port(int port) {
return this;
}
+ public Builder preferUncompressedResponse(boolean preferUncompressedResponse) {
+ this.preferUncompressedResponse = preferUncompressedResponse;
+ return this;
+ }
+
public ExporterHttpServerProperties build() {
- return new ExporterHttpServerProperties(port);
+ return new ExporterHttpServerProperties(port, preferUncompressedResponse);
}
}
}
diff --git a/prometheus-metrics-config/src/main/java/io/prometheus/metrics/config/ExporterOpenTelemetryProperties.java b/prometheus-metrics-config/src/main/java/io/prometheus/metrics/config/ExporterOpenTelemetryProperties.java
index 53acb6b14..8c0bdd5c7 100644
--- a/prometheus-metrics-config/src/main/java/io/prometheus/metrics/config/ExporterOpenTelemetryProperties.java
+++ b/prometheus-metrics-config/src/main/java/io/prometheus/metrics/config/ExporterOpenTelemetryProperties.java
@@ -1,9 +1,38 @@
package io.prometheus.metrics.config;
+import io.prometheus.metrics.annotations.StableApi;
import java.util.HashMap;
import java.util.Map;
+import javax.annotation.Nullable;
-// TODO: JavaDoc is currently only in OpenTelemetryExporter.Builder. Look there for reference.
+/**
+ * Properties for configuring the OpenTelemetry exporter.
+ *
+ * These properties can be configured via {@code prometheus.properties}, system properties, or
+ * programmatically.
+ *
+ *
All properties are prefixed with {@code io.prometheus.exporter.opentelemetry}.
+ *
+ *
Available properties:
+ *
+ *
+ * {@code protocol} - OTLP protocol: {@code "grpc"} or {@code "http/protobuf"}
+ * {@code endpoint} - OTLP endpoint URL
+ * {@code headers} - HTTP headers for outgoing requests
+ * {@code intervalSeconds} - Export interval in seconds
+ * {@code timeoutSeconds} - Request timeout in seconds
+ * {@code serviceName} - Service name resource attribute
+ * {@code serviceNamespace} - Service namespace resource attribute
+ * {@code serviceInstanceId} - Service instance ID resource attribute
+ * {@code serviceVersion} - Service version resource attribute
+ * {@code resourceAttributes} - Additional resource attributes
+ *
+ *
+ * @see OpenTelemetry
+ * SDK Environment Variables
+ */
+@StableApi
public class ExporterOpenTelemetryProperties {
// See
@@ -11,38 +40,41 @@ public class ExporterOpenTelemetryProperties {
private static final String PROTOCOL = "protocol"; // otel.exporter.otlp.protocol
private static final String ENDPOINT = "endpoint"; // otel.exporter.otlp.endpoint
private static final String HEADERS = "headers"; // otel.exporter.otlp.headers
- private static final String INTERVAL_SECONDS = "intervalSeconds"; // otel.metric.export.interval
- private static final String TIMEOUT_SECONDS = "timeoutSeconds"; // otel.exporter.otlp.timeout
- private static final String SERVICE_NAME = "serviceName"; // otel.service.name
- private static final String SERVICE_NAMESPACE = "serviceNamespace";
- private static final String SERVICE_INSTANCE_ID = "serviceInstanceId";
- private static final String SERVICE_VERSION = "serviceVersion";
+ private static final String INTERVAL_SECONDS = "interval_seconds"; // otel.metric.export.interval
+ private static final String TIMEOUT_SECONDS = "timeout_seconds"; // otel.exporter.otlp.timeout
+ private static final String SERVICE_NAME = "service_name"; // otel.service.name
+ private static final String SERVICE_NAMESPACE = "service_namespace";
+ private static final String SERVICE_INSTANCE_ID = "service_instance_id";
+ private static final String SERVICE_VERSION = "service_version";
private static final String RESOURCE_ATTRIBUTES =
- "resourceAttributes"; // otel.resource.attributes
+ "resource_attributes"; // otel.resource.attributes
+ private static final String PRESERVE_NAMES = "preserve_names";
private static final String PREFIX = "io.prometheus.exporter.opentelemetry";
- private final String protocol;
- private final String endpoint;
+ @Nullable private final String endpoint;
+ @Nullable private final String protocol;
private final Map headers;
- private final String interval;
- private final String timeout;
- private final String serviceName;
- private final String serviceNamespace;
- private final String serviceInstanceId;
- private final String serviceVersion;
+ @Nullable private final String interval;
+ @Nullable private final String timeout;
+ @Nullable private final String serviceName;
+ @Nullable private final String serviceNamespace;
+ @Nullable private final String serviceInstanceId;
+ @Nullable private final String serviceVersion;
private final Map resourceAttributes;
+ @Nullable private final Boolean preserveNames;
private ExporterOpenTelemetryProperties(
- String protocol,
- String endpoint,
+ @Nullable String protocol,
+ @Nullable String endpoint,
Map headers,
- String interval,
- String timeout,
- String serviceName,
- String serviceNamespace,
- String serviceInstanceId,
- String serviceVersion,
- Map resourceAttributes) {
+ @Nullable String interval,
+ @Nullable String timeout,
+ @Nullable String serviceName,
+ @Nullable String serviceNamespace,
+ @Nullable String serviceInstanceId,
+ @Nullable String serviceVersion,
+ Map resourceAttributes,
+ @Nullable Boolean preserveNames) {
this.protocol = protocol;
this.endpoint = endpoint;
this.headers = headers;
@@ -53,12 +85,15 @@ private ExporterOpenTelemetryProperties(
this.serviceInstanceId = serviceInstanceId;
this.serviceVersion = serviceVersion;
this.resourceAttributes = resourceAttributes;
+ this.preserveNames = preserveNames;
}
+ @Nullable
public String getProtocol() {
return protocol;
}
+ @Nullable
public String getEndpoint() {
return endpoint;
}
@@ -67,26 +102,32 @@ public Map getHeaders() {
return headers;
}
+ @Nullable
public String getInterval() {
return interval;
}
+ @Nullable
public String getTimeout() {
return timeout;
}
+ @Nullable
public String getServiceName() {
return serviceName;
}
+ @Nullable
public String getServiceNamespace() {
return serviceNamespace;
}
+ @Nullable
public String getServiceInstanceId() {
return serviceInstanceId;
}
+ @Nullable
public String getServiceVersion() {
return serviceVersion;
}
@@ -96,22 +137,33 @@ public Map getResourceAttributes() {
}
/**
- * Note that this will remove entries from {@code properties}. This is because we want to know if
- * there are unused properties remaining after all properties have been loaded.
+ * When {@code true}, metric names are preserved as-is (including suffixes like {@code _total}).
+ * When {@code false} (default), standard OTel name normalization is applied (stripping unit
+ * suffix).
*/
- static ExporterOpenTelemetryProperties load(Map properties)
+ @Nullable
+ public Boolean getPreserveNames() {
+ return preserveNames;
+ }
+
+ /**
+ * Note that this will remove entries from {@code propertySource}. This is because we want to know
+ * if there are unused properties remaining after all properties have been loaded.
+ */
+ static ExporterOpenTelemetryProperties load(PropertySource propertySource)
throws PrometheusPropertiesException {
- String protocol = Util.loadString(PREFIX + "." + PROTOCOL, properties);
- String endpoint = Util.loadString(PREFIX + "." + ENDPOINT, properties);
- Map headers = Util.loadMap(PREFIX + "." + HEADERS, properties);
- String interval = Util.loadStringAddSuffix(PREFIX + "." + INTERVAL_SECONDS, properties, "s");
- String timeout = Util.loadStringAddSuffix(PREFIX + "." + TIMEOUT_SECONDS, properties, "s");
- String serviceName = Util.loadString(PREFIX + "." + SERVICE_NAME, properties);
- String serviceNamespace = Util.loadString(PREFIX + "." + SERVICE_NAMESPACE, properties);
- String serviceInstanceId = Util.loadString(PREFIX + "." + SERVICE_INSTANCE_ID, properties);
- String serviceVersion = Util.loadString(PREFIX + "." + SERVICE_VERSION, properties);
+ String protocol = Util.loadString(PREFIX, PROTOCOL, propertySource);
+ String endpoint = Util.loadString(PREFIX, ENDPOINT, propertySource);
+ Map headers = Util.loadMap(PREFIX, HEADERS, propertySource);
+ String interval = Util.loadStringAddSuffix(PREFIX, INTERVAL_SECONDS, propertySource, "s");
+ String timeout = Util.loadStringAddSuffix(PREFIX, TIMEOUT_SECONDS, propertySource, "s");
+ String serviceName = Util.loadString(PREFIX, SERVICE_NAME, propertySource);
+ String serviceNamespace = Util.loadString(PREFIX, SERVICE_NAMESPACE, propertySource);
+ String serviceInstanceId = Util.loadString(PREFIX, SERVICE_INSTANCE_ID, propertySource);
+ String serviceVersion = Util.loadString(PREFIX, SERVICE_VERSION, propertySource);
Map resourceAttributes =
- Util.loadMap(PREFIX + "." + RESOURCE_ATTRIBUTES, properties);
+ Util.loadMap(PREFIX, RESOURCE_ATTRIBUTES, propertySource);
+ Boolean preserveNames = Util.loadBoolean(PREFIX, PRESERVE_NAMES, propertySource);
return new ExporterOpenTelemetryProperties(
protocol,
endpoint,
@@ -122,7 +174,8 @@ static ExporterOpenTelemetryProperties load(Map properties)
serviceNamespace,
serviceInstanceId,
serviceVersion,
- resourceAttributes);
+ resourceAttributes,
+ preserveNames);
}
public static Builder builder() {
@@ -131,19 +184,28 @@ public static Builder builder() {
public static class Builder {
- private String protocol;
- private String endpoint;
+ @Nullable private String protocol;
+ @Nullable private String endpoint;
private final Map headers = new HashMap<>();
- private String interval;
- private String timeout;
- private String serviceName;
- private String serviceNamespace;
- private String serviceInstanceId;
- private String serviceVersion;
+ @Nullable private String interval;
+ @Nullable private String timeout;
+ @Nullable private String serviceName;
+ @Nullable private String serviceNamespace;
+ @Nullable private String serviceInstanceId;
+ @Nullable private String serviceVersion;
private final Map resourceAttributes = new HashMap<>();
+ @Nullable private Boolean preserveNames;
private Builder() {}
+ /**
+ * The OTLP protocol to use.
+ *
+ * Supported values: {@code "grpc"} or {@code "http/protobuf"}.
+ *
+ *
See OpenTelemetry's OTEL_EXPORTER_OTLP_PROTOCOL .
+ */
public Builder protocol(String protocol) {
if (!protocol.equals("grpc") && !protocol.equals("http/protobuf")) {
throw new IllegalArgumentException(
@@ -153,17 +215,43 @@ public Builder protocol(String protocol) {
return this;
}
+ /**
+ * The OTLP endpoint to send metric data to.
+ *
+ *
The default depends on the protocol:
+ *
+ *
+ * {@code "grpc"}: {@code "http://localhost:4317"}
+ * {@code "http/protobuf"}: {@code "http://localhost:4318/v1/metrics"}
+ *
+ *
+ * See OpenTelemetry's OTEL_EXPORTER_OTLP_METRICS_ENDPOINT .
+ */
public Builder endpoint(String endpoint) {
this.endpoint = endpoint;
return this;
}
- /** Add a request header. Call multiple times to add multiple headers. */
+ /**
+ * Add an HTTP header to be applied to outgoing requests. Call multiple times to add multiple
+ * headers.
+ *
+ *
See OpenTelemetry's OTEL_EXPORTER_OTLP_HEADERS .
+ */
public Builder header(String name, String value) {
this.headers.put(name, value);
return this;
}
+ /**
+ * The interval between the start of two export attempts. Default is 60 seconds.
+ *
+ *
Like OpenTelemetry's OTEL_METRIC_EXPORT_INTERVAL
+ * (which defaults to 60000 milliseconds), but specified in seconds rather than milliseconds.
+ */
public Builder intervalSeconds(int intervalSeconds) {
if (intervalSeconds <= 0) {
throw new IllegalArgumentException(intervalSeconds + ": Expecting intervalSeconds > 0");
@@ -172,6 +260,13 @@ public Builder intervalSeconds(int intervalSeconds) {
return this;
}
+ /**
+ * The timeout for outgoing requests. Default is 10.
+ *
+ *
Like OpenTelemetry's OTEL_EXPORTER_OTLP_METRICS_TIMEOUT ,
+ * but in seconds rather than milliseconds.
+ */
public Builder timeoutSeconds(int timeoutSeconds) {
if (timeoutSeconds <= 0) {
throw new IllegalArgumentException(timeoutSeconds + ": Expecting timeoutSeconds > 0");
@@ -180,31 +275,77 @@ public Builder timeoutSeconds(int timeoutSeconds) {
return this;
}
+ /**
+ * The {@code service.name} resource attribute.
+ *
+ *
If not explicitly specified, {@code client_java} will try to initialize it with a
+ * reasonable default, like the JAR file name.
+ *
+ *
See {@code service.name} in OpenTelemetry's Resource
+ * Semantic Conventions .
+ */
public Builder serviceName(String serviceName) {
this.serviceName = serviceName;
return this;
}
+ /**
+ * The {@code service.namespace} resource attribute.
+ *
+ *
See {@code service.namespace} in OpenTelemetry's Resource
+ * Semantic Conventions .
+ */
public Builder serviceNamespace(String serviceNamespace) {
this.serviceNamespace = serviceNamespace;
return this;
}
+ /**
+ * The {@code service.instance.id} resource attribute.
+ *
+ *
See {@code service.instance.id} in OpenTelemetry's Resource
+ * Semantic Conventions .
+ */
public Builder serviceInstanceId(String serviceInstanceId) {
this.serviceInstanceId = serviceInstanceId;
return this;
}
+ /**
+ * The {@code service.version} resource attribute.
+ *
+ *
See {@code service.version} in OpenTelemetry's Resource
+ * Semantic Conventions .
+ */
public Builder serviceVersion(String serviceVersion) {
this.serviceVersion = serviceVersion;
return this;
}
+ /**
+ * Add a resource attribute. Call multiple times to add multiple resource attributes.
+ *
+ *
See OpenTelemetry's OTEL_RESOURCE_ATTRIBUTES .
+ */
public Builder resourceAttribute(String name, String value) {
this.resourceAttributes.put(name, value);
return this;
}
+ /**
+ * When {@code true}, metric names are preserved as-is (including suffixes like {@code _total}).
+ * When {@code false} (default), standard OTel name normalization is applied.
+ */
+ public Builder preserveNames(boolean preserveNames) {
+ this.preserveNames = preserveNames;
+ return this;
+ }
+
public ExporterOpenTelemetryProperties build() {
return new ExporterOpenTelemetryProperties(
protocol,
@@ -216,7 +357,8 @@ public ExporterOpenTelemetryProperties build() {
serviceNamespace,
serviceInstanceId,
serviceVersion,
- resourceAttributes);
+ resourceAttributes,
+ preserveNames);
}
}
}
diff --git a/prometheus-metrics-config/src/main/java/io/prometheus/metrics/config/ExporterProperties.java b/prometheus-metrics-config/src/main/java/io/prometheus/metrics/config/ExporterProperties.java
index d35f970ff..a1c67266a 100644
--- a/prometheus-metrics-config/src/main/java/io/prometheus/metrics/config/ExporterProperties.java
+++ b/prometheus-metrics-config/src/main/java/io/prometheus/metrics/config/ExporterProperties.java
@@ -1,24 +1,26 @@
package io.prometheus.metrics.config;
-import java.util.Map;
+import io.prometheus.metrics.annotations.StableApi;
+import javax.annotation.Nullable;
/** Properties starting with io.prometheus.exporter */
+@StableApi
public class ExporterProperties {
- private static final String INCLUDE_CREATED_TIMESTAMPS = "includeCreatedTimestamps";
+ private static final String INCLUDE_CREATED_TIMESTAMPS = "include_created_timestamps";
// milliseconds is the default - we only provide a boolean flag to avoid a breaking change
- private static final String PROMETHEUS_TIMESTAMPS_IN_MS = "prometheusTimestampsInMs";
- private static final String EXEMPLARS_ON_ALL_METRIC_TYPES = "exemplarsOnAllMetricTypes";
+ private static final String PROMETHEUS_TIMESTAMPS_IN_MS = "prometheus_timestamps_in_ms";
+ private static final String EXEMPLARS_ON_ALL_METRIC_TYPES = "exemplars_on_all_metric_types";
private static final String PREFIX = "io.prometheus.exporter";
- private final Boolean includeCreatedTimestamps;
- private final Boolean prometheusTimestampsInMs;
- private final Boolean exemplarsOnAllMetricTypes;
+ @Nullable private final Boolean includeCreatedTimestamps;
+ @Nullable private final Boolean prometheusTimestampsInMs;
+ @Nullable private final Boolean exemplarsOnAllMetricTypes;
private ExporterProperties(
- Boolean includeCreatedTimestamps,
- Boolean prometheusTimestampsInMs,
- Boolean exemplarsOnAllMetricTypes) {
+ @Nullable Boolean includeCreatedTimestamps,
+ @Nullable Boolean prometheusTimestampsInMs,
+ @Nullable Boolean exemplarsOnAllMetricTypes) {
this.includeCreatedTimestamps = includeCreatedTimestamps;
this.prometheusTimestampsInMs = prometheusTimestampsInMs;
this.exemplarsOnAllMetricTypes = exemplarsOnAllMetricTypes;
@@ -43,17 +45,16 @@ public boolean getExemplarsOnAllMetricTypes() {
}
/**
- * Note that this will remove entries from {@code properties}. This is because we want to know if
- * there are unused properties remaining after all properties have been loaded.
+ * Note that this will remove entries from {@code propertySource}. This is because we want to know
+ * if there are unused properties remaining after all properties have been loaded.
*/
- static ExporterProperties load(Map properties)
+ static ExporterProperties load(PropertySource propertySource)
throws PrometheusPropertiesException {
Boolean includeCreatedTimestamps =
- Util.loadBoolean(PREFIX + "." + INCLUDE_CREATED_TIMESTAMPS, properties);
- Boolean timestampsInMs =
- Util.loadBoolean(PREFIX + "." + PROMETHEUS_TIMESTAMPS_IN_MS, properties);
+ Util.loadBoolean(PREFIX, INCLUDE_CREATED_TIMESTAMPS, propertySource);
+ Boolean timestampsInMs = Util.loadBoolean(PREFIX, PROMETHEUS_TIMESTAMPS_IN_MS, propertySource);
Boolean exemplarsOnAllMetricTypes =
- Util.loadBoolean(PREFIX + "." + EXEMPLARS_ON_ALL_METRIC_TYPES, properties);
+ Util.loadBoolean(PREFIX, EXEMPLARS_ON_ALL_METRIC_TYPES, propertySource);
return new ExporterProperties(
includeCreatedTimestamps, timestampsInMs, exemplarsOnAllMetricTypes);
}
@@ -64,8 +65,8 @@ public static Builder builder() {
public static class Builder {
- private Boolean includeCreatedTimestamps;
- private Boolean exemplarsOnAllMetricTypes;
+ @Nullable private Boolean includeCreatedTimestamps;
+ @Nullable private Boolean exemplarsOnAllMetricTypes;
boolean prometheusTimestampsInMs;
private Builder() {}
diff --git a/prometheus-metrics-config/src/main/java/io/prometheus/metrics/config/ExporterPushgatewayProperties.java b/prometheus-metrics-config/src/main/java/io/prometheus/metrics/config/ExporterPushgatewayProperties.java
index 8aafba3a4..e97ade191 100644
--- a/prometheus-metrics-config/src/main/java/io/prometheus/metrics/config/ExporterPushgatewayProperties.java
+++ b/prometheus-metrics-config/src/main/java/io/prometheus/metrics/config/ExporterPushgatewayProperties.java
@@ -1,24 +1,43 @@
package io.prometheus.metrics.config;
-import java.util.Map;
+import io.prometheus.metrics.annotations.StableApi;
+import java.time.Duration;
+import javax.annotation.Nullable;
+@StableApi
public class ExporterPushgatewayProperties {
private static final String ADDRESS = "address";
private static final String JOB = "job";
private static final String SCHEME = "scheme";
+ private static final String ESCAPING_SCHEME = "escaping_scheme";
+ private static final String READ_TIMEOUT = "read_timeout_seconds";
+ private static final String CONNECT_TIMEOUT = "connect_timeout_seconds";
private static final String PREFIX = "io.prometheus.exporter.pushgateway";
- private final String scheme;
- private final String address;
- private final String job;
+ @Nullable private final String scheme;
+ @Nullable private final String address;
+ @Nullable private final String job;
+ @Nullable private final EscapingScheme escapingScheme;
+ @Nullable private final Duration connectTimeout;
+ @Nullable private final Duration readTimeout;
- private ExporterPushgatewayProperties(String address, String job, String scheme) {
+ private ExporterPushgatewayProperties(
+ @Nullable String address,
+ @Nullable String job,
+ @Nullable String scheme,
+ @Nullable EscapingScheme escapingScheme,
+ @Nullable Duration connectTimeout,
+ @Nullable Duration readTimeout) {
this.address = address;
this.job = job;
this.scheme = scheme;
+ this.escapingScheme = escapingScheme;
+ this.connectTimeout = connectTimeout;
+ this.readTimeout = readTimeout;
}
/** Address of the Pushgateway in the form {@code host:port}. Default is {@code localhost:9091} */
+ @Nullable
public String getAddress() {
return address;
}
@@ -27,6 +46,7 @@ public String getAddress() {
* {@code job} label for metrics being pushed. Default is the name of the JAR file that is
* running.
*/
+ @Nullable
public String getJob() {
return job;
}
@@ -35,27 +55,122 @@ public String getJob() {
* Scheme to be used when pushing metrics to the pushgateway. Must be "http" or "https". Default
* is "http".
*/
+ @Nullable
public String getScheme() {
return scheme;
}
+ /** Escaping scheme to be used when pushing metric data to the pushgateway. */
+ @Nullable
+ public EscapingScheme getEscapingScheme() {
+ return escapingScheme;
+ }
+
+ /** Connection timeout for connections to the Pushgateway. */
+ @Nullable
+ public Duration getConnectTimeout() {
+ return connectTimeout;
+ }
+
+ /** Read timeout for connections to the Pushgateway. */
+ @Nullable
+ public Duration getReadTimeout() {
+ return readTimeout;
+ }
+
/**
- * Note that this will remove entries from {@code properties}. This is because we want to know if
- * there are unused properties remaining after all properties have been loaded.
+ * Note that this will remove entries from {@code propertySource}. This is because we want to know
+ * if there are unused properties remaining after all properties have been loaded.
*/
- static ExporterPushgatewayProperties load(Map properties)
+ static ExporterPushgatewayProperties load(PropertySource propertySource)
throws PrometheusPropertiesException {
- String address = Util.loadString(PREFIX + "." + ADDRESS, properties);
- String job = Util.loadString(PREFIX + "." + JOB, properties);
- String scheme = Util.loadString(PREFIX + "." + SCHEME, properties);
+ String address = Util.loadString(PREFIX, ADDRESS, propertySource);
+ String job = Util.loadString(PREFIX, JOB, propertySource);
+ String scheme = Util.loadString(PREFIX, SCHEME, propertySource);
+ String escapingScheme = Util.loadString(PREFIX, ESCAPING_SCHEME, propertySource);
+ Duration connectTimeout = Util.loadOptionalDuration(PREFIX, CONNECT_TIMEOUT, propertySource);
+ Duration readTimeout = Util.loadOptionalDuration(PREFIX, READ_TIMEOUT, propertySource);
+
if (scheme != null) {
if (!scheme.equals("http") && !scheme.equals("https")) {
throw new PrometheusPropertiesException(
- String.format(
- "%s.%s: Illegal value. Expecting 'http' or 'https'. Found: %s",
- PREFIX, SCHEME, scheme));
+ Util.invalidValueMessage(
+ PREFIX + "." + SCHEME, "Illegal value. Expecting 'http' or 'https'."));
}
}
- return new ExporterPushgatewayProperties(address, job, scheme);
+
+ return new ExporterPushgatewayProperties(
+ address, job, scheme, parseEscapingScheme(escapingScheme), connectTimeout, readTimeout);
+ }
+
+ private static @Nullable EscapingScheme parseEscapingScheme(@Nullable String scheme) {
+ if (scheme == null) {
+ return null;
+ }
+ switch (scheme) {
+ case "allow-utf-8":
+ return EscapingScheme.ALLOW_UTF8;
+ case "values":
+ return EscapingScheme.VALUE_ENCODING_ESCAPING;
+ case "underscores":
+ return EscapingScheme.UNDERSCORE_ESCAPING;
+ case "dots":
+ return EscapingScheme.DOTS_ESCAPING;
+ default:
+ throw new PrometheusPropertiesException(
+ Util.invalidValueMessage(
+ PREFIX + "." + ESCAPING_SCHEME,
+ "Illegal value. Expecting 'allow-utf-8', 'values', 'underscores', or 'dots'."));
+ }
+ }
+
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ public static class Builder {
+ @Nullable private String address;
+ @Nullable private String job;
+ @Nullable private String scheme;
+ @Nullable private EscapingScheme escapingScheme;
+ @Nullable private Duration connectTimeout;
+ @Nullable private Duration readTimeout;
+
+ private Builder() {}
+
+ public Builder address(String address) {
+ this.address = address;
+ return this;
+ }
+
+ public Builder job(String job) {
+ this.job = job;
+ return this;
+ }
+
+ public Builder scheme(String scheme) {
+ this.scheme = scheme;
+ return this;
+ }
+
+ public Builder escapingScheme(EscapingScheme escapingScheme) {
+ this.escapingScheme = escapingScheme;
+ return this;
+ }
+
+ public Builder connectTimeout(Duration connectTimeout) {
+ this.connectTimeout = connectTimeout;
+ return this;
+ }
+
+ public Builder readTimeout(Duration readTimeout) {
+ this.readTimeout = readTimeout;
+ return this;
+ }
+
+ public ExporterPushgatewayProperties build() {
+ return new ExporterPushgatewayProperties(
+ address, job, scheme, escapingScheme, connectTimeout, readTimeout);
+ }
}
}
diff --git a/prometheus-metrics-config/src/main/java/io/prometheus/metrics/config/MetricsProperties.java b/prometheus-metrics-config/src/main/java/io/prometheus/metrics/config/MetricsProperties.java
index 7667fadce..c2758bd87 100644
--- a/prometheus-metrics-config/src/main/java/io/prometheus/metrics/config/MetricsProperties.java
+++ b/prometheus-metrics-config/src/main/java/io/prometheus/metrics/config/MetricsProperties.java
@@ -2,60 +2,83 @@
import static java.util.Collections.unmodifiableList;
+import io.prometheus.metrics.annotations.StableApi;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
-import java.util.Map;
+import javax.annotation.Nullable;
/** Properties starting with io.prometheus.metrics */
+@StableApi
public class MetricsProperties {
- private static final String EXEMPLARS_ENABLED = "exemplarsEnabled";
- private static final String HISTOGRAM_NATIVE_ONLY = "histogramNativeOnly";
- private static final String HISTOGRAM_CLASSIC_ONLY = "histogramClassicOnly";
- private static final String HISTOGRAM_CLASSIC_UPPER_BOUNDS = "histogramClassicUpperBounds";
- private static final String HISTOGRAM_NATIVE_INITIAL_SCHEMA = "histogramNativeInitialSchema";
+ private static final String EXEMPLARS_ENABLED = "exemplars_enabled";
+ private static final String HISTOGRAM_NATIVE_ONLY = "histogram_native_only";
+ private static final String HISTOGRAM_CLASSIC_ONLY = "histogram_classic_only";
+ private static final String HISTOGRAM_CLASSIC_UPPER_BOUNDS = "histogram_classic_upper_bounds";
+ private static final String HISTOGRAM_NATIVE_INITIAL_SCHEMA = "histogram_native_initial_schema";
private static final String HISTOGRAM_NATIVE_MIN_ZERO_THRESHOLD =
- "histogramNativeMinZeroThreshold";
+ "histogram_native_min_zero_threshold";
private static final String HISTOGRAM_NATIVE_MAX_ZERO_THRESHOLD =
- "histogramNativeMaxZeroThreshold";
+ "histogram_native_max_zero_threshold";
private static final String HISTOGRAM_NATIVE_MAX_NUMBER_OF_BUCKETS =
- "histogramNativeMaxNumberOfBuckets"; // 0 means unlimited number of buckets
+ "histogram_native_max_number_of_buckets"; // 0 means unlimited number of buckets
private static final String HISTOGRAM_NATIVE_RESET_DURATION_SECONDS =
- "histogramNativeResetDurationSeconds"; // 0 means no reset
- private static final String SUMMARY_QUANTILES = "summaryQuantiles";
- private static final String SUMMARY_QUANTILE_ERRORS = "summaryQuantileErrors";
- private static final String SUMMARY_MAX_AGE_SECONDS = "summaryMaxAgeSeconds";
- private static final String SUMMARY_NUMBER_OF_AGE_BUCKETS = "summaryNumberOfAgeBuckets";
-
- private final Boolean exemplarsEnabled;
- private final Boolean histogramNativeOnly;
- private final Boolean histogramClassicOnly;
- private final List histogramClassicUpperBounds;
- private final Integer histogramNativeInitialSchema;
- private final Double histogramNativeMinZeroThreshold;
- private final Double histogramNativeMaxZeroThreshold;
- private final Integer histogramNativeMaxNumberOfBuckets;
- private final Long histogramNativeResetDurationSeconds;
- private final List