Prometheus demo

OpenMetrics 1.0 vs 2.0 scrape demo

One Go and one Java application, both instrumented with the Prometheus client library from main, are each scraped twice by Prometheus from main: once with OpenMetrics 1.0 and once with OpenMetrics 2.0. The views below compare what the two scrapes store, series by series.

Setup

ComponentVersion
Prometheus3.14.0-om2-demo (64fb7167541d)
client_golangv1.24.2-0.20261003105250-0ecb5c28f53c
client_java1.9.1-SNAPSHOT (9e9deb6b9e59)

Prometheus runs with the flags below and scrapes every 5s with the 4 jobs below, see its configuration. Each job allows only one protocol in scrape_protocols, so the target has to answer in that format.

--enable-feature=openmetrics2,exemplar-storage
JobScrape protocolAccept header sent
go-om1metadataOpenMetricsText1.0.0application/openmetrics-text;version=1.0.0;escaping=allow-utf-8
go-om2metadataOpenMetricsText2.0.0application/openmetrics-text;version=2.0.0
java-om1metadataOpenMetricsText1.0.0application/openmetrics-text;version=1.0.0;escaping=allow-utf-8
java-om2metadataOpenMetricsText2.0.0application/openmetrics-text;version=2.0.0

Both applications expose the same metrics: a counter with exemplars, a counter with a unit, a gauge with a UTF-8 name and label, a histogram with classic and native buckets, a summary and an info metric, plus the runtime metrics of their SDK.

Raw exposition

The /metrics endpoints of both applications are public. The om1 and om2 paths set the Accept header for you, and serve the result as text/plain so that browsers show it; the Accept header sent is in the X-Metrics-Accept response header. The plain path passes your own Accept header through.

# OpenMetrics 2.0 from the Go application, as served to Prometheus:
curl -sH 'application/openmetrics-text;version=2.0.0;escaping=allow-utf-8' https://<this host>/go/metrics
# The same, through the om2 path that sets the Accept header:
curl -s https://<this host>/go/om2/metrics

1Same series?

Series per job

table

The number of series each scrape stores. If OpenMetrics 1.0 and 2.0 carried the same data, go-om1 would match go-om2 and java-om1 would match java-om2.

Allpanel 1
count by (job) ({job=~"(go|java)-om[12]"})
Open in Prometheus

Go: series stored by only one scrape

table

Both panels are empty when the two scrapes store exactly the same series. The Go example does not enable _created series for OM1 (EnableOpenMetricsTextCreatedSamples), while OM2 always carries start timestamps inline as st@, which Prometheus does not store as series. The UTF-8 gauge http.server.active_requests is stored as is with OM1, but as http_server_active_requests with OM2, as Prometheus does not ask for escaping=allow-utf-8 when scraping OM2.

Only OM1panel 1
group without (job) (label_replace({job="go-om1"}, "name", "$1", "__name__", "(.+)"))
unless
group without (job) (label_replace({job="go-om2"}, "name", "$1", "__name__", "(.+)"))
Only OM2panel 2
group without (job) (label_replace({job="go-om2"}, "name", "$1", "__name__", "(.+)"))
unless
group without (job) (label_replace({job="go-om1"}, "name", "$1", "__name__", "(.+)"))
Open in Prometheus

Java: series stored by only one scrape

table

For client_java with OpenMetrics 2.0 enabled, OM2 exposes counters without the _total suffix, e.g. http_requests instead of http_requests_total, so they are different series. The same UTF-8 escaping difference as for Go applies.

Only OM1panel 1
group without (job) (label_replace({job="java-om1"}, "name", "$1", "__name__", "(.+)"))
unless
group without (job) (label_replace({job="java-om2"}, "name", "$1", "__name__", "(.+)"))
Only OM2panel 2
group without (job) (label_replace({job="java-om2"}, "name", "$1", "__name__", "(.+)"))
unless
group without (job) (label_replace({job="java-om1"}, "name", "$1", "__name__", "(.+)"))
Open in Prometheus

2Same values?

Counters

graph

The rates of all 4 jobs line up, with a regex on the name to also match the Java OM2 counters without _total. They only differ by when each job scrapes.

Requestspanel 1
sum by (job) (rate({__name__=~"http_requests(_total)?", job=~"(go|java)-om[12]"}[1m]))
Bytespanel 2
sum by (job) (rate({__name__=~"http_request_size(_bytes_total)?", job=~"(go|java)-om[12]"}[1m]))
Open in Prometheus

UTF-8 gauge

table

The same gauge, with the same value 3, is found under its UTF-8 name for the OM1 jobs and under its escaped name for the OM2 jobs.

UTF-8 namepanel 1
{"http.server.active_requests"}
Escaped namepanel 2
http_server_active_requests
Open in Prometheus

Histograms

graph

OM2 exposes each histogram as one composite sample, {count:...,sum:...,bucket:[...]}, which Prometheus stores as the same classic _bucket, _count and _sum series as the OM1 ones. The native buckets of the composite sample are ignored, as scrape_native_histograms is off.

p90panel 1
histogram_quantile(0.9, sum by (job, le) (rate(http_request_duration_seconds_bucket{job=~"(go|java)-om[12]"}[1m])))
Ratepanel 2
sum by (job) (rate(http_request_duration_seconds_count{job=~"(go|java)-om[12]"}[1m]))
Open in Prometheus

Summaries

graph

Summaries are composite samples in OM2 too, {count:...,sum:...,quantile:[...]}, stored as the same quantile, _count and _sum series.

p99panel 1
rpc_latency_seconds{quantile="0.99", job=~"(go|java)-om[12]"}
Averagepanel 2
sum by (job) (rate(rpc_latency_seconds_sum{job=~"(go|java)-om[12]"}[1m])) / sum by (job) (rate(rpc_latency_seconds_count{job=~"(go|java)-om[12]"}[1m]))
Open in Prometheus

3Same exemplars?

Histogram exemplars

graph

Both opened with exemplars shown. OM1 attaches histogram exemplars to the _bucket series of their bucket, while the exemplars of an OM2 composite sample are stored on the _count series, so the OM1 jobs show them in the first panel and the OM2 jobs in the second.

Bucketspanel 1
sum by (job, le) (rate(http_request_duration_seconds_bucket{le=~"0.05|0.1|0.25", job=~"(go|java)-om[12]"}[1m]))
Countpanel 2
sum by (job) (rate(http_request_duration_seconds_count{job=~"(go|java)-om[12]"}[1m]))
Open in Prometheus