```
-
-
## 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 45df21431..80a7739fa 100644
--- a/docs/content/exporters/spring.md
+++ b/docs/content/exporters/spring.md
@@ -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
index 80cbdc2ef..026292c39 100644
--- a/docs/content/exporters/unicode.md
+++ b/docs/content/exporters/unicode.md
@@ -3,11 +3,16 @@ 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).
+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
@@ -23,7 +28,7 @@ other UTF-8 characters, without any replacements.
## PushGateway
-When using the [Pushgateway](/exporters/pushgateway/), Unicode support has to be enabled
+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 a5425e07f..66e40f7e2 100644
--- a/docs/content/otel/names.md
+++ b/docs/content/otel/names.md
@@ -5,15 +5,16 @@ 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:
@@ -21,11 +22,31 @@ The main steps when converting OpenTelemetry metric names to Prometheus metric n
- 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`.
@@ -34,3 +55,4 @@ Dots in metric and label names are now supported in the Prometheus Java client l
described in [Unicode support].
[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 ae5a887e3..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.133.0@sha256:98274b756324abdb2473fa0c898247a246091e861e61d1548f9be483198eecea
+ 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.2@sha256:0ef775495967cd5d7a6b2e146b6ea695d624803c8db8349fb8ce4164f719f9b7
+ 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.1@sha256:a1701c2180249361737a99a01bc770db39381640e4d631825d38ff4535efa47d
+ 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:0dd39a1d74c9399dfea568723cd59fc5ff7812b628a60e90a7dbaadb18a7c409
+ 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 594ce9ab8..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.5.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.10
+ 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 601ef7f3b..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.5.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.10
+ 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 8a739e939..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.5.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 f9ee206f0..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.5.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 ac13ff2a1..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.5.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 a65ba5227..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.133.0@sha256:98274b756324abdb2473fa0c898247a246091e861e61d1548f9be483198eecea
+ 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 443f6315f..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.8_9-jre@sha256:ea350fe1d8bc9b18dea4ad46bb9a0f4d6fbd0dbb3548092e3ff8f1a3fef09efe
+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 ef61ba52d..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.8_9-jre@sha256:ea350fe1d8bc9b18dea4ad46bb9a0f4d6fbd0dbb3548092e3ff8f1a3fef09efe
+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 8e8fa6891..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.5.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 8e3b7385d..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.5.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.10
+ 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 b1b34f9bf..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.8_9-jre@sha256:ea350fe1d8bc9b18dea4ad46bb9a0f4d6fbd0dbb3548092e3ff8f1a3fef09efe
+ 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.1@sha256:a1701c2180249361737a99a01bc770db39381640e4d631825d38ff4535efa47d
+ 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 5324ffb91..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.5.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 b02b8fa03..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.5.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 f6d2c4207..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.5.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 f88b313a2..80594e550 100644
--- a/examples/pom.xml
+++ b/examples/pom.xml
@@ -1,12 +1,11 @@
-
+
4.0.0
io.prometheus
client_java
- 1.5.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 2faac1769..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.5.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 e467c8c41..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_32_0.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 f52c8434e..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.5.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 f75c398a5..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.5.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 e34fadfc4..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.5.0-SNAPSHOT
+ 1.8.1-SNAPSHOT
it-exporter-servlet-jetty-sample
@@ -16,8 +15,8 @@
Jetty Sample for the Exporter Integration Test
- 12.1.0
- 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 49a1d827b..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.5.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.10
+ 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 1929a9c6d..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.5.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 1157cc254..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_32_0.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 6321799db..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.5.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 a442b9086..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.5.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 861674427..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.5.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 8071c0ff7..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.5
+ 4.1.1
io.prometheus
it-spring-boot-smoke-test
- 1.5.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 9e294192f..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_32_0.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 59575e31b..c3f299d69 100644
--- a/integration-tests/pom.xml
+++ b/integration-tests/pom.xml
@@ -1,12 +1,11 @@
-
+
4.0.0
io.prometheus
client_java
- 1.5.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 68d86e16a..29085c417 100644
--- a/mise.toml
+++ b/mise.toml
@@ -1,9 +1,46 @@
+monorepo_root = true
+
+[monorepo]
+config_roots = [".mise/envs/*"]
+
[tools]
-"go:github.com/gohugoio/hugo" = "v0.149.0"
-"go:github.com/grafana/oats" = "0.4.1"
-java = "temurin-21.0.8+9.0.LTS"
-lychee = "0.20.1"
-protoc = "32.0"
+"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"
@@ -11,66 +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"
+description = "build all modules without tests"
run = "./mvnw install -DskipTests -Dcoverage.skip=true"
-[tasks.lint]
-run = "scripts/super-linter.sh"
-
-[tasks.lint-links]
-run = "lychee --include-fragments ."
-
-[tasks.lint-bom]
-run = "scripts/lint-bom.sh"
-
-[tasks.lint-rest]
-description = "All lints not covered by super linter"
-depends = ["lint-links", "lint-bom"]
+[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.5.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 e9cf8d330..bd8896bf2 100755
--- a/mvnw
+++ b/mvnw
@@ -19,7 +19,7 @@
# ----------------------------------------------------------------------------
# ----------------------------------------------------------------------------
-# Apache Maven Wrapper startup batch script, version 3.3.3
+# Apache Maven Wrapper startup batch script, version 3.3.4
#
# Optional ENV vars
# -----------------
diff --git a/mvnw.cmd b/mvnw.cmd
index 3fd2be860..5761d9489 100644
--- a/mvnw.cmd
+++ b/mvnw.cmd
@@ -19,7 +19,7 @@
@REM ----------------------------------------------------------------------------
@REM ----------------------------------------------------------------------------
-@REM Apache Maven Wrapper startup batch script, version 3.3.3
+@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
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 d7f07a4af..53781fb59 100644
--- a/pom.xml
+++ b/pom.xml
@@ -1,18 +1,15 @@
-
+
pom
4.0.0
io.prometheus
client_java_parent
- 1.5.0-SNAPSHOT
+ 1.8.1-SNAPSHOT
prometheus-metrics-parent/pom.xml
- 1.5.0-SNAPSHOT
client_java
Prometheus Metrics Library
@@ -23,21 +20,26 @@
UTF-8
--module-name-need-to-be-overridden--
- 4.32.0
- 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.3
+ 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,62 +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/.*|.*/src/test/java/.*|.*/examples/.*|.*/integration-tests/.*)
- -XepOpt:NullAway:AnnotatedPackages=io.prometheus.metrics
-
-
-
- com.google.errorprone
- error_prone_core
- 2.41.0
-
-
- com.uber.nullaway
- nullaway
- 0.12.9
-
-
-
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
@@ -331,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.19.0
- test
-
-
- org.assertj
- assertj-core
- 3.27.4
- 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
@@ -435,6 +372,7 @@
maven-javadoc-plugin
+ ${javadoc.skip}
UTF-8
UTF-8
true
@@ -448,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 a13398228..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.5.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 a07c83f0b..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.5.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
index 1cd037bf3..1d6bd37cf 100644
--- 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
@@ -1,7 +1,9 @@
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"),
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 20933bfdc..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,15 +1,16 @@
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";
@Nullable private final Integer minRetentionPeriodSeconds;
@Nullable private final Integer maxRetentionPeriodSeconds;
@@ -57,17 +58,17 @@ public Integer getSampleIntervalMilliseconds() {
}
/**
- * 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,
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 7f3b46842..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,19 +1,20 @@
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";
@Nullable private final List allowedNames;
@@ -61,19 +62,19 @@ public List getExcludedMetricNamePrefixes() {
}
/**
- * 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);
}
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 55c15a60c..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,17 +1,21 @@
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 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(@Nullable Integer port) {
+ private ExporterHttpServerProperties(@Nullable Integer port, boolean preferUncompressedResponse) {
this.port = port;
+ this.preferUncompressedResponse = preferUncompressedResponse;
}
@Nullable
@@ -19,15 +23,24 @@ 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() {
@@ -37,6 +50,7 @@ public static Builder builder() {
public static class Builder {
@Nullable private Integer port;
+ private boolean preferUncompressedResponse = false;
private Builder() {}
@@ -45,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 be09b5a63..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,10 +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
@@ -12,14 +40,15 @@ 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";
@Nullable private final String endpoint;
@@ -32,6 +61,7 @@ public class ExporterOpenTelemetryProperties {
@Nullable private final String serviceInstanceId;
@Nullable private final String serviceVersion;
private final Map resourceAttributes;
+ @Nullable private final Boolean preserveNames;
private ExporterOpenTelemetryProperties(
@Nullable String protocol,
@@ -43,7 +73,8 @@ private ExporterOpenTelemetryProperties(
@Nullable String serviceNamespace,
@Nullable String serviceInstanceId,
@Nullable String serviceVersion,
- Map resourceAttributes) {
+ Map resourceAttributes,
+ @Nullable Boolean preserveNames) {
this.protocol = protocol;
this.endpoint = endpoint;
this.headers = headers;
@@ -54,6 +85,7 @@ private ExporterOpenTelemetryProperties(
this.serviceInstanceId = serviceInstanceId;
this.serviceVersion = serviceVersion;
this.resourceAttributes = resourceAttributes;
+ this.preserveNames = preserveNames;
}
@Nullable
@@ -105,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,
@@ -131,7 +174,8 @@ static ExporterOpenTelemetryProperties load(Map properties)
serviceNamespace,
serviceInstanceId,
serviceVersion,
- resourceAttributes);
+ resourceAttributes,
+ preserveNames);
}
public static Builder builder() {
@@ -150,9 +194,18 @@ public static class Builder {
@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(
@@ -162,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");
@@ -181,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");
@@ -189,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,
@@ -225,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 6b081816d..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,15 +1,16 @@
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";
@Nullable private final Boolean includeCreatedTimestamps;
@@ -44,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);
}
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 9d7b380d1..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,29 +1,39 @@
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 = "escapingScheme";
+ 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";
@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(
@Nullable String address,
@Nullable String job,
@Nullable String scheme,
- @Nullable EscapingScheme escapingScheme) {
+ @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} */
@@ -56,28 +66,41 @@ 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 escapingScheme = Util.loadString(PREFIX + "." + ESCAPING_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, parseEscapingScheme(escapingScheme));
+ address, job, scheme, parseEscapingScheme(escapingScheme), connectTimeout, readTimeout);
}
private static @Nullable EscapingScheme parseEscapingScheme(@Nullable String scheme) {
@@ -95,10 +118,9 @@ static ExporterPushgatewayProperties load(Map properties)
return EscapingScheme.DOTS_ESCAPING;
default:
throw new PrometheusPropertiesException(
- String.format(
- "%s.%s: Illegal value. Expecting 'allow-utf-8', 'values', 'underscores', "
- + "or 'dots'. Found: %s",
- PREFIX, ESCAPING_SCHEME, scheme));
+ Util.invalidValueMessage(
+ PREFIX + "." + ESCAPING_SCHEME,
+ "Illegal value. Expecting 'allow-utf-8', 'values', 'underscores', or 'dots'."));
}
}
@@ -111,6 +133,8 @@ public static class Builder {
@Nullable private String job;
@Nullable private String scheme;
@Nullable private EscapingScheme escapingScheme;
+ @Nullable private Duration connectTimeout;
+ @Nullable private Duration readTimeout;
private Builder() {}
@@ -134,8 +158,19 @@ public Builder escapingScheme(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);
+ 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 bae38ab86..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,32 +2,54 @@
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";
+ "histogram_native_reset_duration_seconds"; // 0 means no reset
+ private static final String SUMMARY_QUANTILES = "summary_quantiles";
+ private static final String SUMMARY_QUANTILE_ERRORS = "summary_quantile_errors";
+ private static final String SUMMARY_MAX_AGE_SECONDS = "summary_max_age_seconds";
+ private static final String SUMMARY_NUMBER_OF_AGE_BUCKETS = "summary_number_of_age_buckets";
+
+ /**
+ * All known property suffixes that can be configured for metrics.
+ *
+ * This list is used to parse metric-specific configuration keys from environment variables.
+ */
+ static final String[] PROPERTY_SUFFIXES = {
+ EXEMPLARS_ENABLED,
+ HISTOGRAM_NATIVE_ONLY,
+ HISTOGRAM_CLASSIC_ONLY,
+ HISTOGRAM_CLASSIC_UPPER_BOUNDS,
+ HISTOGRAM_NATIVE_INITIAL_SCHEMA,
+ HISTOGRAM_NATIVE_MIN_ZERO_THRESHOLD,
+ HISTOGRAM_NATIVE_MAX_ZERO_THRESHOLD,
+ HISTOGRAM_NATIVE_MAX_NUMBER_OF_BUCKETS,
+ HISTOGRAM_NATIVE_RESET_DURATION_SECONDS,
+ SUMMARY_QUANTILES,
+ SUMMARY_QUANTILE_ERRORS,
+ SUMMARY_MAX_AGE_SECONDS,
+ SUMMARY_NUMBER_OF_AGE_BUCKETS
+ };
@Nullable private final Boolean exemplarsEnabled;
@Nullable private final Boolean histogramNativeOnly;
@@ -226,17 +248,16 @@ private void validate(String prefix) throws PrometheusPropertiesException {
+ SUMMARY_QUANTILES);
}
if (summaryQuantileErrors.size() != summaryQuantiles.size()) {
+ String fullKey =
+ prefix.isEmpty() ? SUMMARY_QUANTILE_ERRORS : prefix + "." + SUMMARY_QUANTILE_ERRORS;
throw new PrometheusPropertiesException(
- prefix
- + "."
- + SUMMARY_QUANTILE_ERRORS
- + ": must have the same length as "
- + SUMMARY_QUANTILES);
+ fullKey + ": must have the same length as " + SUMMARY_QUANTILES);
}
for (double error : summaryQuantileErrors) {
if (error < 0 || error > 1) {
- throw new PrometheusPropertiesException(
- prefix + "." + SUMMARY_QUANTILE_ERRORS + ": Expecting 0.0 <= error <= 1.0");
+ String fullKey =
+ prefix.isEmpty() ? SUMMARY_QUANTILE_ERRORS : prefix + "." + SUMMARY_QUANTILE_ERRORS;
+ throw new PrometheusPropertiesException(fullKey + ": Expecting 0.0 <= error <= 1.0");
}
}
}
@@ -263,7 +284,7 @@ public Boolean getHistogramClassicOnly() {
return histogramClassicOnly;
}
- /** See {@code Histogram.Builder.classicBuckets()} */
+ /** See {@code Histogram.Builder.classicUpperBounds()} */
@Nullable
public List getHistogramClassicUpperBounds() {
return histogramClassicUpperBounds;
@@ -335,25 +356,25 @@ public Integer getSummaryNumberOfAgeBuckets() {
}
/**
- * 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 MetricsProperties load(String prefix, Map properties)
+ static MetricsProperties load(String prefix, PropertySource propertySource)
throws PrometheusPropertiesException {
return new MetricsProperties(
- Util.loadBoolean(prefix + "." + EXEMPLARS_ENABLED, properties),
- Util.loadBoolean(prefix + "." + HISTOGRAM_NATIVE_ONLY, properties),
- Util.loadBoolean(prefix + "." + HISTOGRAM_CLASSIC_ONLY, properties),
- Util.loadDoubleList(prefix + "." + HISTOGRAM_CLASSIC_UPPER_BOUNDS, properties),
- Util.loadInteger(prefix + "." + HISTOGRAM_NATIVE_INITIAL_SCHEMA, properties),
- Util.loadDouble(prefix + "." + HISTOGRAM_NATIVE_MIN_ZERO_THRESHOLD, properties),
- Util.loadDouble(prefix + "." + HISTOGRAM_NATIVE_MAX_ZERO_THRESHOLD, properties),
- Util.loadInteger(prefix + "." + HISTOGRAM_NATIVE_MAX_NUMBER_OF_BUCKETS, properties),
- Util.loadLong(prefix + "." + HISTOGRAM_NATIVE_RESET_DURATION_SECONDS, properties),
- Util.loadDoubleList(prefix + "." + SUMMARY_QUANTILES, properties),
- Util.loadDoubleList(prefix + "." + SUMMARY_QUANTILE_ERRORS, properties),
- Util.loadLong(prefix + "." + SUMMARY_MAX_AGE_SECONDS, properties),
- Util.loadInteger(prefix + "." + SUMMARY_NUMBER_OF_AGE_BUCKETS, properties),
+ Util.loadBoolean(prefix, EXEMPLARS_ENABLED, propertySource),
+ Util.loadBoolean(prefix, HISTOGRAM_NATIVE_ONLY, propertySource),
+ Util.loadBoolean(prefix, HISTOGRAM_CLASSIC_ONLY, propertySource),
+ Util.loadDoubleList(prefix, HISTOGRAM_CLASSIC_UPPER_BOUNDS, propertySource),
+ Util.loadInteger(prefix, HISTOGRAM_NATIVE_INITIAL_SCHEMA, propertySource),
+ Util.loadDouble(prefix, HISTOGRAM_NATIVE_MIN_ZERO_THRESHOLD, propertySource),
+ Util.loadDouble(prefix, HISTOGRAM_NATIVE_MAX_ZERO_THRESHOLD, propertySource),
+ Util.loadInteger(prefix, HISTOGRAM_NATIVE_MAX_NUMBER_OF_BUCKETS, propertySource),
+ Util.loadLong(prefix, HISTOGRAM_NATIVE_RESET_DURATION_SECONDS, propertySource),
+ Util.loadDoubleList(prefix, SUMMARY_QUANTILES, propertySource),
+ Util.loadDoubleList(prefix, SUMMARY_QUANTILE_ERRORS, propertySource),
+ Util.loadLong(prefix, SUMMARY_MAX_AGE_SECONDS, propertySource),
+ Util.loadInteger(prefix, SUMMARY_NUMBER_OF_AGE_BUCKETS, propertySource),
prefix);
}
diff --git a/prometheus-metrics-config/src/main/java/io/prometheus/metrics/config/OpenMetrics2Properties.java b/prometheus-metrics-config/src/main/java/io/prometheus/metrics/config/OpenMetrics2Properties.java
new file mode 100644
index 000000000..1f5080f05
--- /dev/null
+++ b/prometheus-metrics-config/src/main/java/io/prometheus/metrics/config/OpenMetrics2Properties.java
@@ -0,0 +1,141 @@
+package io.prometheus.metrics.config;
+
+import io.prometheus.metrics.annotations.StableApi;
+import javax.annotation.Nullable;
+
+/**
+ * Properties starting with io.prometheus.openmetrics2. These properties are experimental and
+ * subject to change.
+ */
+@StableApi
+public class OpenMetrics2Properties {
+
+ private static final String PREFIX = "io.prometheus.openmetrics2";
+ private static final String ENABLED = "enabled";
+ private static final String CONTENT_NEGOTIATION = "content_negotiation";
+ private static final String COMPOSITE_VALUES = "composite_values";
+ private static final String EXEMPLAR_COMPLIANCE = "exemplar_compliance";
+ private static final String NATIVE_HISTOGRAMS = "native_histograms";
+
+ @Nullable private final Boolean enabled;
+ @Nullable private final Boolean contentNegotiation;
+ @Nullable private final Boolean compositeValues;
+ @Nullable private final Boolean exemplarCompliance;
+ @Nullable private final Boolean nativeHistograms;
+
+ private OpenMetrics2Properties(
+ @Nullable Boolean enabled,
+ @Nullable Boolean contentNegotiation,
+ @Nullable Boolean compositeValues,
+ @Nullable Boolean exemplarCompliance,
+ @Nullable Boolean nativeHistograms) {
+ this.enabled = enabled;
+ this.contentNegotiation = contentNegotiation;
+ this.compositeValues = compositeValues;
+ this.exemplarCompliance = exemplarCompliance;
+ this.nativeHistograms = nativeHistograms;
+ }
+
+ /**
+ * Enable the OpenMetrics 2.0 text format writer. When {@code true}, the OM2 writer is used
+ * instead of OM1 for OpenMetrics responses. Default is {@code false}.
+ */
+ public boolean getEnabled() {
+ return enabled != null && enabled;
+ }
+
+ /** Gate OM2 features behind content negotiation. Default is {@code false}. */
+ public boolean getContentNegotiation() {
+ return contentNegotiation != null && contentNegotiation;
+ }
+
+ /** Single-line histogram/summary with {@code st@}. Default is {@code false}. */
+ public boolean getCompositeValues() {
+ return compositeValues != null && compositeValues;
+ }
+
+ /** Mandatory timestamps, no 128-char limit for exemplars. Default is {@code false}. */
+ public boolean getExemplarCompliance() {
+ return exemplarCompliance != null && exemplarCompliance;
+ }
+
+ /** Exponential buckets support for native histograms. Default is {@code false}. */
+ public boolean getNativeHistograms() {
+ return nativeHistograms != null && nativeHistograms;
+ }
+
+ /**
+ * 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 OpenMetrics2Properties load(PropertySource propertySource)
+ throws PrometheusPropertiesException {
+ Boolean enabled = Util.loadBoolean(PREFIX, ENABLED, propertySource);
+ Boolean contentNegotiation = Util.loadBoolean(PREFIX, CONTENT_NEGOTIATION, propertySource);
+ Boolean compositeValues = Util.loadBoolean(PREFIX, COMPOSITE_VALUES, propertySource);
+ Boolean exemplarCompliance = Util.loadBoolean(PREFIX, EXEMPLAR_COMPLIANCE, propertySource);
+ Boolean nativeHistograms = Util.loadBoolean(PREFIX, NATIVE_HISTOGRAMS, propertySource);
+ return new OpenMetrics2Properties(
+ enabled, contentNegotiation, compositeValues, exemplarCompliance, nativeHistograms);
+ }
+
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ public static class Builder {
+
+ @Nullable private Boolean enabled;
+ @Nullable private Boolean contentNegotiation;
+ @Nullable private Boolean compositeValues;
+ @Nullable private Boolean exemplarCompliance;
+ @Nullable private Boolean nativeHistograms;
+
+ private Builder() {}
+
+ /** See {@link #getEnabled()} */
+ public Builder enabled(boolean enabled) {
+ this.enabled = enabled;
+ return this;
+ }
+
+ /** See {@link #getContentNegotiation()} */
+ public Builder contentNegotiation(boolean contentNegotiation) {
+ this.contentNegotiation = contentNegotiation;
+ return this;
+ }
+
+ /** See {@link #getCompositeValues()} */
+ public Builder compositeValues(boolean compositeValues) {
+ this.compositeValues = compositeValues;
+ return this;
+ }
+
+ /** See {@link #getExemplarCompliance()} */
+ public Builder exemplarCompliance(boolean exemplarCompliance) {
+ this.exemplarCompliance = exemplarCompliance;
+ return this;
+ }
+
+ /** See {@link #getNativeHistograms()} */
+ public Builder nativeHistograms(boolean nativeHistograms) {
+ this.nativeHistograms = nativeHistograms;
+ return this;
+ }
+
+ /** Enable all OpenMetrics 2.0 features */
+ public Builder enableAll() {
+ this.enabled = true;
+ this.contentNegotiation = true;
+ this.compositeValues = true;
+ this.exemplarCompliance = true;
+ this.nativeHistograms = true;
+ return this;
+ }
+
+ public OpenMetrics2Properties build() {
+ return new OpenMetrics2Properties(
+ enabled, contentNegotiation, compositeValues, exemplarCompliance, nativeHistograms);
+ }
+ }
+}
diff --git a/prometheus-metrics-config/src/main/java/io/prometheus/metrics/config/PrometheusProperties.java b/prometheus-metrics-config/src/main/java/io/prometheus/metrics/config/PrometheusProperties.java
index ab78b48e3..aa145d07c 100644
--- a/prometheus-metrics-config/src/main/java/io/prometheus/metrics/config/PrometheusProperties.java
+++ b/prometheus-metrics-config/src/main/java/io/prometheus/metrics/config/PrometheusProperties.java
@@ -1,7 +1,9 @@
package io.prometheus.metrics.config;
+import io.prometheus.metrics.annotations.StableApi;
import java.util.HashMap;
import java.util.Map;
+import java.util.function.Consumer;
import javax.annotation.Nullable;
/**
@@ -9,18 +11,81 @@
*
* This class represents the runtime configuration.
*/
+@StableApi
public class PrometheusProperties {
private static final PrometheusProperties instance = PrometheusPropertiesLoader.load();
private final MetricsProperties defaultMetricsProperties;
- private final Map metricProperties = new HashMap<>();
+ private final MetricPropertiesMap metricProperties;
private final ExemplarsProperties exemplarProperties;
private final ExporterProperties exporterProperties;
private final ExporterFilterProperties exporterFilterProperties;
private final ExporterHttpServerProperties exporterHttpServerProperties;
private final ExporterOpenTelemetryProperties exporterOpenTelemetryProperties;
private final ExporterPushgatewayProperties exporterPushgatewayProperties;
+ private final OpenMetrics2Properties openMetrics2Properties;
+
+ /**
+ * Map that stores metric-specific properties keyed by metric name in exposition format
+ * (underscores instead of dots).
+ *
+ * This wrapper makes it explicit that metric names are normalized to underscore format for
+ * storage, so that environment variables and properties with dots in metric names can be
+ * correctly looked up using normalized names.
+ */
+ static class MetricPropertiesMap {
+ private final Map map = new HashMap<>();
+
+ void set(Map properties) {
+ map.clear();
+ properties.forEach(this::put);
+ }
+
+ void put(String metricName, MetricsProperties properties) {
+ map.put(normalize(metricName), properties);
+ }
+
+ /**
+ * Get metric properties by metric name.
+ *
+ * Accepts metric names in any format (with dots or underscores) and automatically converts
+ * them to the normalized underscore format used for storage.
+ *
+ * @param metricName the metric name (dots will be converted to underscores)
+ * @return the metric properties, or null if not configured
+ */
+ @Nullable
+ MetricsProperties get(String metricName) {
+ return map.get(normalize(metricName));
+ }
+
+ // copied from PrometheusNaming - but we can't reuse that class here because it's in a module
+ // that
+ // depends on PrometheusProperties, which would create a circular dependency.
+ private static String normalize(String name) {
+ StringBuilder escaped = new StringBuilder();
+
+ for (int i = 0; i < name.length(); ) {
+ int c = name.codePointAt(i);
+ if (isValidLegacyChar(c, i)) {
+ escaped.appendCodePoint(c);
+ } else {
+ escaped.append('_');
+ }
+ i += Character.charCount(c);
+ }
+ return escaped.toString();
+ }
+ }
+
+ private static boolean isValidLegacyChar(int c, int i) {
+ return (c >= 'a' && c <= 'z')
+ || (c >= 'A' && c <= 'Z')
+ || c == '_'
+ || c == ':'
+ || (c >= '0' && c <= '9' && i > 0);
+ }
/**
* Get the properties instance. When called for the first time, {@code get()} loads the properties
@@ -41,23 +106,26 @@ public static Builder builder() {
return new Builder();
}
- public PrometheusProperties(
+ // Package-private constructor for PrometheusPropertiesLoader and Builder
+ PrometheusProperties(
MetricsProperties defaultMetricsProperties,
- Map metricProperties,
+ MetricPropertiesMap metricProperties,
ExemplarsProperties exemplarProperties,
ExporterProperties exporterProperties,
ExporterFilterProperties exporterFilterProperties,
ExporterHttpServerProperties httpServerConfig,
ExporterPushgatewayProperties pushgatewayProperties,
- ExporterOpenTelemetryProperties otelConfig) {
+ ExporterOpenTelemetryProperties otelConfig,
+ OpenMetrics2Properties openMetrics2Properties) {
this.defaultMetricsProperties = defaultMetricsProperties;
- this.metricProperties.putAll(metricProperties);
+ this.metricProperties = metricProperties;
this.exemplarProperties = exemplarProperties;
this.exporterProperties = exporterProperties;
this.exporterFilterProperties = exporterFilterProperties;
this.exporterHttpServerProperties = httpServerConfig;
this.exporterPushgatewayProperties = pushgatewayProperties;
this.exporterOpenTelemetryProperties = otelConfig;
+ this.openMetrics2Properties = openMetrics2Properties;
}
/**
@@ -72,10 +140,13 @@ public MetricsProperties getDefaultMetricProperties() {
* Properties specific for one metric. Should be merged with {@link
* #getDefaultMetricProperties()}. May return {@code null} if no metric-specific properties are
* configured for a metric name.
+ *
+ * @param metricName the metric name (dots will be automatically converted to underscores to match
+ * exposition format)
*/
@Nullable
public MetricsProperties getMetricProperties(String metricName) {
- return metricProperties.get(metricName.replace(".", "_"));
+ return metricProperties.get(metricName);
}
public ExemplarsProperties getExemplarProperties() {
@@ -102,9 +173,13 @@ public ExporterOpenTelemetryProperties getExporterOpenTelemetryProperties() {
return exporterOpenTelemetryProperties;
}
+ public OpenMetrics2Properties getOpenMetrics2Properties() {
+ return openMetrics2Properties;
+ }
+
public static class Builder {
private MetricsProperties defaultMetricsProperties = MetricsProperties.builder().build();
- private Map metricProperties = new HashMap<>();
+ private final MetricPropertiesMap metricProperties = new MetricPropertiesMap();
private ExemplarsProperties exemplarProperties = ExemplarsProperties.builder().build();
private ExporterProperties exporterProperties = ExporterProperties.builder().build();
private ExporterFilterProperties exporterFilterProperties =
@@ -115,6 +190,8 @@ public static class Builder {
ExporterPushgatewayProperties.builder().build();
private ExporterOpenTelemetryProperties otelConfig =
ExporterOpenTelemetryProperties.builder().build();
+ private OpenMetrics2Properties openMetrics2Properties =
+ OpenMetrics2Properties.builder().build();
private Builder() {}
@@ -124,7 +201,7 @@ public Builder defaultMetricsProperties(MetricsProperties defaultMetricsProperti
}
public Builder metricProperties(Map metricProperties) {
- this.metricProperties = metricProperties;
+ this.metricProperties.set(metricProperties);
return this;
}
@@ -166,6 +243,19 @@ public Builder exporterOpenTelemetryProperties(
return this;
}
+ public Builder enableOpenMetrics2(Consumer configurator) {
+ OpenMetrics2Properties.Builder openMetrics2Builder =
+ OpenMetrics2Properties.builder().enabled(true);
+ configurator.accept(openMetrics2Builder);
+ this.openMetrics2Properties = openMetrics2Builder.build();
+ return this;
+ }
+
+ public Builder openMetrics2Properties(OpenMetrics2Properties openMetrics2Properties) {
+ this.openMetrics2Properties = openMetrics2Properties;
+ return this;
+ }
+
public PrometheusProperties build() {
return new PrometheusProperties(
defaultMetricsProperties,
@@ -175,7 +265,8 @@ public PrometheusProperties build() {
exporterFilterProperties,
exporterHttpServerProperties,
pushgatewayProperties,
- otelConfig);
+ otelConfig,
+ openMetrics2Properties);
}
}
}
diff --git a/prometheus-metrics-config/src/main/java/io/prometheus/metrics/config/PrometheusPropertiesException.java b/prometheus-metrics-config/src/main/java/io/prometheus/metrics/config/PrometheusPropertiesException.java
index bdb024514..b6f6fac4b 100644
--- a/prometheus-metrics-config/src/main/java/io/prometheus/metrics/config/PrometheusPropertiesException.java
+++ b/prometheus-metrics-config/src/main/java/io/prometheus/metrics/config/PrometheusPropertiesException.java
@@ -1,5 +1,8 @@
package io.prometheus.metrics.config;
+import io.prometheus.metrics.annotations.StableApi;
+
+@StableApi
public class PrometheusPropertiesException extends RuntimeException {
private static final long serialVersionUID = 0L;
diff --git a/prometheus-metrics-config/src/main/java/io/prometheus/metrics/config/PrometheusPropertiesLoader.java b/prometheus-metrics-config/src/main/java/io/prometheus/metrics/config/PrometheusPropertiesLoader.java
index a847a8dba..04bbfe4a5 100644
--- a/prometheus-metrics-config/src/main/java/io/prometheus/metrics/config/PrometheusPropertiesLoader.java
+++ b/prometheus-metrics-config/src/main/java/io/prometheus/metrics/config/PrometheusPropertiesLoader.java
@@ -1,16 +1,14 @@
package io.prometheus.metrics.config;
+import io.prometheus.metrics.annotations.StableApi;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.HashMap;
-import java.util.HashSet;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
/**
* The Properties Loader is early stages.
@@ -20,6 +18,7 @@
* Boot's Externalized Configuration, like support for YAML, Properties, and env vars, or
* support for Spring's naming conventions for properties.
*/
+@StableApi
public class PrometheusPropertiesLoader {
/** See {@link PrometheusProperties#get()}. */
@@ -29,20 +28,22 @@ public static PrometheusProperties load() throws PrometheusPropertiesException {
public static PrometheusProperties load(Map externalProperties)
throws PrometheusPropertiesException {
- Map properties = loadProperties(externalProperties);
- Map metricsConfigs = loadMetricsConfigs(properties);
+ PropertySource propertySource = loadProperties(externalProperties);
+ PrometheusProperties.MetricPropertiesMap metricsConfigs = loadMetricsConfigs(propertySource);
MetricsProperties defaultMetricsProperties =
- MetricsProperties.load("io.prometheus.metrics", properties);
- ExemplarsProperties exemplarConfig = ExemplarsProperties.load(properties);
- ExporterProperties exporterProperties = ExporterProperties.load(properties);
- ExporterFilterProperties exporterFilterProperties = ExporterFilterProperties.load(properties);
+ MetricsProperties.load("io.prometheus.metrics", propertySource);
+ ExemplarsProperties exemplarConfig = ExemplarsProperties.load(propertySource);
+ ExporterProperties exporterProperties = ExporterProperties.load(propertySource);
+ ExporterFilterProperties exporterFilterProperties =
+ ExporterFilterProperties.load(propertySource);
ExporterHttpServerProperties exporterHttpServerProperties =
- ExporterHttpServerProperties.load(properties);
+ ExporterHttpServerProperties.load(propertySource);
ExporterPushgatewayProperties exporterPushgatewayProperties =
- ExporterPushgatewayProperties.load(properties);
+ ExporterPushgatewayProperties.load(propertySource);
ExporterOpenTelemetryProperties exporterOpenTelemetryProperties =
- ExporterOpenTelemetryProperties.load(properties);
- validateAllPropertiesProcessed(properties);
+ ExporterOpenTelemetryProperties.load(propertySource);
+ OpenMetrics2Properties openMetrics2Properties = OpenMetrics2Properties.load(propertySource);
+ validateAllPropertiesProcessed(propertySource);
return new PrometheusProperties(
defaultMetricsProperties,
metricsConfigs,
@@ -51,31 +52,53 @@ public static PrometheusProperties load(Map externalProperties)
exporterFilterProperties,
exporterHttpServerProperties,
exporterPushgatewayProperties,
- exporterOpenTelemetryProperties);
+ exporterOpenTelemetryProperties,
+ openMetrics2Properties);
}
- // This will remove entries from properties when they are processed.
- private static Map loadMetricsConfigs(Map properties) {
- Map result = new HashMap<>();
+ // This will remove entries from propertySource when they are processed.
+ static PrometheusProperties.MetricPropertiesMap loadMetricsConfigs(
+ PropertySource propertySource) {
+ PrometheusProperties.MetricPropertiesMap result =
+ new PrometheusProperties.MetricPropertiesMap();
// Note that the metric name in the properties file must be as exposed in the Prometheus
// exposition formats,
// i.e. all dots replaced with underscores.
- Pattern pattern = Pattern.compile("io\\.prometheus\\.metrics\\.([^.]+)\\.");
- // Create a copy of the keySet() for iterating. We cannot iterate directly over keySet()
- // because entries are removed when MetricsConfig.load(...) is called.
- Set propertyNames = new HashSet<>();
- for (Object key : properties.keySet()) {
- propertyNames.add(key.toString());
- }
+
+ // Get a snapshot of all keys for pattern matching. Entries will be removed
+ // when MetricsProperties.load(...) is called.
+ Set propertyNames = propertySource.getAllKeys();
for (String propertyName : propertyNames) {
- Matcher matcher = pattern.matcher(propertyName);
- if (matcher.find()) {
- String metricName = matcher.group(1).replace(".", "_");
- if (!result.containsKey(metricName)) {
- result.put(
- metricName,
- MetricsProperties.load("io.prometheus.metrics." + metricName, properties));
+ String metricName = null;
+
+ if (propertyName.startsWith("io.prometheus.metrics.")) {
+ // Dot-separated format (from regular properties, system properties, or files)
+ String remainder = propertyName.substring("io.prometheus.metrics.".length());
+ // Try to match against known property suffixes
+ for (String suffix : MetricsProperties.PROPERTY_SUFFIXES) {
+ if (remainder.endsWith("." + suffix)) {
+ // Metric name in dot format, convert dots to underscores for exposition format
+ metricName =
+ remainder.substring(0, remainder.length() - suffix.length() - 1).replace(".", "_");
+ break;
+ }
}
+ } else if (propertyName.startsWith("io_prometheus_metrics_")) {
+ // Underscore-separated format (from environment variables)
+ String remainder = propertyName.substring("io_prometheus_metrics_".length());
+ // Try to match against known property suffixes
+ for (String suffix : MetricsProperties.PROPERTY_SUFFIXES) {
+ if (remainder.endsWith("_" + suffix)) {
+ metricName = remainder.substring(0, remainder.length() - suffix.length() - 1);
+ break;
+ }
+ }
+ }
+
+ if (metricName != null && result.get(metricName) == null) {
+ result.put(
+ metricName,
+ MetricsProperties.load("io.prometheus.metrics." + metricName, propertySource));
}
}
return result;
@@ -84,26 +107,43 @@ private static Map loadMetricsConfigs(Map properties) {
- for (Object key : properties.keySet()) {
- if (key.toString().startsWith("io.prometheus")) {
+ private static void validateAllPropertiesProcessed(PropertySource propertySource) {
+ for (String key : propertySource.getRemainingKeys()) {
+ if (key.startsWith("io.prometheus") || key.startsWith("io_prometheus")) {
throw new PrometheusPropertiesException(key + ": Unknown property");
}
}
}
- private static Map loadProperties(Map externalProperties) {
- Map properties = new HashMap<>();
- properties.putAll(loadPropertiesFromClasspath());
- properties.putAll(loadPropertiesFromFile()); // overriding the entries from the classpath file
+ private static PropertySource loadProperties(Map externalProperties) {
+ // Regular properties (lowest priority): classpath, file, system properties
+ Map regularProperties = new HashMap<>();
+ // Normalize all properties at load time to handle camelCase in files for backward compatibility
+ normalizeAndPutAll(regularProperties, loadPropertiesFromClasspath());
+ normalizeAndPutAll(
+ regularProperties,
+ loadPropertiesFromFile()); // overriding the entries from the classpath file
// overriding the entries from the properties file
// copy System properties to avoid ConcurrentModificationException
+ // normalize camelCase system properties to snake_case for backward compatibility
System.getProperties().stringPropertyNames().stream()
.filter(key -> key.startsWith("io.prometheus"))
- .forEach(key -> properties.put(key, System.getProperty(key)));
- properties.putAll(externalProperties); // overriding all the entries above
- // TODO: Add environment variables like EXEMPLARS_ENABLED.
- return properties;
+ .forEach(key -> regularProperties.put(normalizePropertyKey(key), System.getProperty(key)));
+
+ // Environment variables (second priority): just lowercase, keep underscores
+ Map envVarProperties = loadPropertiesFromEnvironment();
+
+ // External properties (highest priority): normalize camelCase for backward compatibility
+ Map normalizedExternalProperties = new HashMap<>();
+ externalProperties.forEach(
+ (key, value) ->
+ normalizedExternalProperties.put(normalizePropertyKey(key.toString()), value));
+
+ return new PropertySource(normalizedExternalProperties, envVarProperties, regularProperties);
+ }
+
+ private static void normalizeAndPutAll(Map