Series per job
tableThe 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.
count by (job) ({job=~"(go|java)-om[12]"})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.
| Component | Version |
|---|---|
| Prometheus | 3.14.0-om2-demo (64fb7167541d) |
| client_golang | v1.24.2-0.20261003105250-0ecb5c28f53c |
| client_java | 1.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
| Job | Scrape protocol | Accept header sent |
|---|---|---|
go-om1metadata | OpenMetricsText1.0.0 | application/openmetrics-text;version=1.0.0;escaping=allow-utf-8 |
go-om2metadata | OpenMetricsText2.0.0 | application/openmetrics-text;version=2.0.0 |
java-om1metadata | OpenMetricsText1.0.0 | application/openmetrics-text;version=1.0.0;escaping=allow-utf-8 |
java-om2metadata | OpenMetricsText2.0.0 | application/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.
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.
| Application | OpenMetrics 1.0 | OpenMetrics 2.0 | Negotiated |
|---|---|---|---|
| Goclient_golang | /go/om1/metrics | /go/om2/metrics | /go/metrics |
| Javaclient_java | /java/om1/metrics | /java/om2/metrics | /java/metrics |
# 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
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.
count by (job) ({job=~"(go|java)-om[12]"})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.
group without (job) (label_replace({job="go-om1"}, "name", "$1", "__name__", "(.+)"))
unless
group without (job) (label_replace({job="go-om2"}, "name", "$1", "__name__", "(.+)"))group without (job) (label_replace({job="go-om2"}, "name", "$1", "__name__", "(.+)"))
unless
group without (job) (label_replace({job="go-om1"}, "name", "$1", "__name__", "(.+)"))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.
group without (job) (label_replace({job="java-om1"}, "name", "$1", "__name__", "(.+)"))
unless
group without (job) (label_replace({job="java-om2"}, "name", "$1", "__name__", "(.+)"))group without (job) (label_replace({job="java-om2"}, "name", "$1", "__name__", "(.+)"))
unless
group without (job) (label_replace({job="java-om1"}, "name", "$1", "__name__", "(.+)"))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.
sum by (job) (rate({__name__=~"http_requests(_total)?", job=~"(go|java)-om[12]"}[1m]))sum by (job) (rate({__name__=~"http_request_size(_bytes_total)?", job=~"(go|java)-om[12]"}[1m]))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.
{"http.server.active_requests"}http_server_active_requestsOM2 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.
histogram_quantile(0.9, sum by (job, le) (rate(http_request_duration_seconds_bucket{job=~"(go|java)-om[12]"}[1m])))sum by (job) (rate(http_request_duration_seconds_count{job=~"(go|java)-om[12]"}[1m]))Summaries are composite samples in OM2 too, {count:...,sum:...,quantile:[...]}, stored as the same quantile, _count and _sum series.
rpc_latency_seconds{quantile="0.99", job=~"(go|java)-om[12]"}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]))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.
sum by (job, le) (rate(http_request_duration_seconds_bucket{le=~"0.05|0.1|0.25", job=~"(go|java)-om[12]"}[1m]))sum by (job) (rate(http_request_duration_seconds_count{job=~"(go|java)-om[12]"}[1m]))