Skip to content

OpenMetrics 2.0: option to keep _total and unit suffixes, so switching from OM1 doesn't rename series #2518

Open

Description

@bwplotka

With io.prometheus.openmetrics2.enabled, the OM2 writer exposes names exactly as passed to the builder. It never appends _total or the unit suffix:

private void writeCounter(Writer writer, CounterSnapshot snapshot, EscapingScheme scheme)
throws IOException {
MetricMetadata metadata = snapshot.getMetadata();
// OM2: use the original name, no _total or unit suffix appending.
String counterName = getOriginalMetadataName(metadata, scheme);
writeMetadataWithName(writer, counterName, "counter", metadata);

This is documented in

## Naming Behavior
OpenMetrics 2.0 removes OM1 suffix rewriting.
- Counters do not get `_total` appended automatically.
- Units do not get appended automatically.
- Info metrics still end in `_info` because that is required by the spec.
Examples:
| Metric builder input | OM1 output | OM2 output |
| ---------------------------------- | ----------------- | -------------- |
| `Counter("events")` | `events_total` | `events` |
| `Counter("events_total")` | `events_total` | `events_total` |
| `Counter("req").unit(BYTES)` | `req_bytes_total` | `req` |
| `Counter("req_bytes").unit(BYTES)` | `req_bytes_total` | `req_bytes` |
| `Info("target")` | `target_info` | `target_info` |
This means OpenMetrics 2.0 does not apply OM1 suffix behavior such as appending `_total` or unit
suffixes, while the legacy OpenMetrics 1.0 and Prometheus text formats keep that existing suffix
behavior.
, e.g. Counter("req").unit(BYTES) is req_bytes_total in OM1 but req in OM2.

The problem is that switching a target from OM1 to OM2 renames every counter, and every metric with a unit, that was instrumented the way client_java has always recommended (Counter.builder().name("events")). Prometheus then stores them as new series, so existing queries, dashboards and alerts silently stop matching.

Note

I hit this with a demo that scrapes the same client_java app with OM1 and OM2 and diffs the stored series: the JVM metrics match (they already have the suffixes in their names), but http_requests_total (OM1) became http_requests (OM2), and http_request_size_bytes_total became http_request_size. Demo: https://35-204-166-191.sslip.io/, code: bwplotka/prometheus#7.

The OM2 spec relaxed _total and the unit suffix from MUST to SHOULD, mainly for OpenTelemetry compatibility (https://github.com/prometheus/docs/blob/605cf81fefc2e8e91f8ba89bb1555ae52a43a318/docs/guides/open_metrics_2_0_migration.md?plain=1#L140). Both are still recommended though (https://github.com/prometheus/docs/blob/605cf81fefc2e8e91f8ba89bb1555ae52a43a318/docs/specs/om/open_metrics_spec_2_0.md?plain=1#L220 and https://github.com/prometheus/docs/blob/605cf81fefc2e8e91f8ba89bb1555ae52a43a318/docs/specs/om/open_metrics_spec_2_0.md?plain=1#L664). Today there is no way to get them in OM2 other than renaming every metric in code; OpenMetrics2Properties has no option for it.

Proposal:

  1. add a property, e.g. io.prometheus.openmetrics2.suffixes (or a naming mode), that keeps the OM1 suffix behaviour in the OM2 writer: append _total to counters and the unit suffix where missing, as expositionBaseName already does for OM1.
  2. I'd argue it should default to on, so that negotiating OM2 doesn't change any series names, and users who want names exactly as written (e.g. for OTel-style names) can opt out.

For comparison, client_golang keeps the names users register (which by convention include _total and the unit), so there OM1 and OM2 produce the same series.

Activity

  1. zeitlinger commented on Oct 7, 2026

    @zeitlinger
    Member

    For comparison, client_golang keeps the names users register

    • this is exactly the goal of the redesign - keep names as passed by the user
    • this is a major goal of the redesign
    • this was the expressed wish of the prometheus maintainers when we discussed this about a year ago

    For that reason I would argue that whenever options we add - if any - they should make migration easier for existing applications.

    The migration story I planned for would be that everyone who switches to OM2 would also adopt the new convention that metric names are not modified.

    Would that work?

  2. arnabnandy7 commented on Oct 7, 2026

    @arnabnandy7
    Contributor

    Keeping application-provided names unchanged as the long-term OM2 behavior makes sense.

    The migration concern raised here still seems relevant, especially with content negotiation enabled. The same endpoint can serve OM1 and OM2 at the same time, so an existing Counter("events") could appear as events_total to one scraper and events to another. Updating applications, dashboards, alerts, and recording rules may not always be possible in one step.

    Would an opt-in migration flag be a reasonable compromise? OM2 could preserve registered names by default, while io.prometheus.openmetrics2.suffixes=true temporarily retains the existing exposition names. Users could enable it while introducing OM2, migrate metric registrations and downstream queries, and then disable it.

    The current PR defaults this option to true, but I can change it to false and document it specifically as a migration aid if that better matches the intended design.

  3. zeitlinger commented on Oct 9, 2026

    @zeitlinger
    Member

    I'm spelling it out in more detail to make it easier to discuss in the upcoming sig meeting.
    @arnabnandy7 please double check I'm getting the use case correctly.

    I want to clarify the intended default: OM2 should preserve metric names exactly as registered, like the Go client. Suffixing should not be on by default.

    The migration question is whether an opt-in compatibility setting, such as io.prometheus.openmetrics2.suffixes=true, would help existing Java users move over gradually:

    Migration step Producer registers OM1 exposes OM2 with default (suffixes=false) OM2 with opt-in compatibility (suffixes=true)
    Before OM2 is enabled Counter("events") events_total — —
    Enable OM2; keep compatibility mode off Counter("events") events_total events —
    Enable OM2 compatibility mode Counter("events") events_total — events_total
    Update producers gradually; keep compatibility mode on A mix of events and events_total events_total — events_total
    All producers use canonical names; turn compatibility mode off Counter("events_total") events_total events_total —

    With the default, switching to OM2 changes the exposed name for producers still registering events. With compatibility mode enabled, OM2 can retain the OM1 name while producer registrations are updated; consumers can keep querying events_total. Once registrations use canonical names, compatibility mode can be turned off without changing the exposed name.

  4. arnabnandy7 commented on Oct 9, 2026

    @arnabnandy7
    Contributor

    4. I'm spelling it out in more detail to make it easier to discuss in the upcoming sig meeting.
    @arnabnandy7 please double check I'm getting the use case correctly.

    @zeitlinger yes, this captures the use case correctly.

    One small clarification the mix of events and events_total during a gradual migration would normally be across application instances or deployment versions. Registering both in the same registry would cause a collision because they resolve to the same exposition name in compatibility mode.

    The same migration flow should also apply to unit suffixes. For example, compatibility mode would keep Counter("req").unit(BYTES) exposed as req_bytes_total while producers gradually move to registering the complete name explicitly.

    With those clarifications, suffixes=false by default and suffixes=true as an opt-in migration aid matches the intended behavior.

  5. bwplotka commented on Oct 12, 2026

    @bwplotka
    MemberAuthor

    I want to clarify the intended default: OM2 should preserve metric names exactly as registered, like the Go client. Suffixing should not be on by default.

    Totally makes sense.

    Sounds like you have a solid migration plan with the suffixes=true and asking users to add manually the suffixes over time 👍🏽

  6. bwplotka commented on Oct 13, 2026

    @bwplotka
    MemberAuthor

    I double checked -- I didn't know this but apparently with the existing API events counter automatically adding _total suffix is a norm for ALL exposition formats, right?

    Metric registration Prometheus Text 0.0.4 Prometheus Protobuf (MetricFamily) OpenMetrics 1.0 Text OpenMetrics 2.0 Text (main)
    Counter("events") # TYPE events_total counter
    events_total
    name: "events_total"
    type: COUNTER
    # TYPE events counter
    events_total
    # TYPE events counter
    events
    Counter("events_total") # TYPE events_total counter
    events_total
    name: "events_total"
    type: COUNTER
    # TYPE events counter
    events_total
    # TYPE events_total counter
    events_total
    Counter("req").unit(BYTES) # TYPE req_bytes_total counter
    req_bytes_total
    name: "req_bytes_total"
    type: COUNTER
    # TYPE req_bytes counter
    # UNIT req_bytes bytes
    req_bytes_total
    # TYPE req counter
    # UNIT req bytes
    req
    Gauge("mem").unit(BYTES) # TYPE mem_bytes gauge
    mem_bytes
    name: "mem_bytes"
    type: GAUGE
    # TYPE mem_bytes gauge
    # UNIT mem_bytes bytes
    mem_bytes
    # TYPE mem gauge
    # UNIT mem bytes
    mem
    Histogram("lat").unit(SECONDS) # TYPE lat_seconds histogram
    lat_seconds_bucket / _count / _sum
    name: "lat_seconds"
    type: HISTOGRAM
    # TYPE lat_seconds histogram
    # UNIT lat_seconds seconds
    lat_seconds_bucket / _count / _sum
    # TYPE lat histogram
    # UNIT lat seconds
    lat_bucket / _count / _sum (or composite lat {...})
    Info("target") # TYPE target_info gauge
    target_info
    name: "target_info"
    type: GAUGE
    # TYPE target info
    target_info
    # TYPE target_info info
    target_info

    In this case, in your case I would propose sticking to suffix=true by default.

    It's not exposition protocol that is glueing those suffixes. It's your API so this should not change IMO, until the next API version. Instead I would add some API option for preserving name which allows your intend.

    Have you considered that?

  7. zeitlinger commented on Oct 13, 2026

    @zeitlinger
    Member

    7. I double checked -- I didn't know this but apparently with the existing API events counter automatically adding _total suffix is a norm for ALL exposition formats, right?

    all but 2.0 - yes

    In this case, in your case I would propose sticking to suffix=true by default.

    • the goal is to align with the go client which doesn't add suffixes
    • adding this with OM2 is a natural way to do so that avoids a breaking change in the client itself

    It's not exposition protocol that is glueing those suffixes.

    not true - OM2 doesn't add the suffixes

  8. bwplotka commented on Oct 13, 2026

    @bwplotka
    MemberAuthor

    the goal is to align with the go client which doesn't add suffixes

    I love this goal, but the question is -- do you need to break implementers to get there so fast. I don't see how you can do this "cleanly" with existing API doing opposite.

    Is it realistic for asking people to rewrite their code in order to adopt OM2? Wouldn't it be more productive to add OM2 by default (!) with existing glue-ing behaviour and add opt-outs and v2 of API one day?

    adding this with OM2 is a natural way to do so that avoids a breaking change in the client itself

    Is it? Going forward we envisioned Proto and OM 2(+) to be the main exposition formats we support and maintain, so having different behavior across those for the same API code seems odd?

    It's not exposition protocol that is glueing those suffixes.
    not true - OM2 doesn't add the suffixes

    I'd argue it's true. There's nothing in OM2 that tells anyone to stop adding suffixes, similarly there's nothing in OM1 that asked to add automatically add them if users missed them (there was just ask to have them). That's a choice of a client and it just happened that in client_golang we didn't make this automatic append.

    TL;DR -- I am just exploring options, after all it's your decision. I think asking users to change their metric names and only then allow OM2 is a lot of work. This is not the worst choice, just might delay OM2 adoption and potentially bring some surprises. Perhaps acceptable 💪🏽

    Up to you and thank you for adopting OM2 so quickly so we can learn!**

  9. arnabnandy7 commented on Oct 13, 2026

    @arnabnandy7
    Contributor

    I took another look at how naming works internally, and it seems suffix normalization is already part of the client_java API model rather than something specific to the OM1 writer.

    For example, Counter.Builder.name() strips _total, MetricMetadata.Builder.name() treats its input as a base name without the type suffix, and setting a unit automatically incorporates that unit into the exposition name. The registry also uses these normalized exposition names when checking for collisions.

    So the current OM2 behavior is a little surprising because it goes back to the original, pre-normalized input, while every other format uses the normalized name. That means the same builder call has different naming semantics depending on which format is negotiated.

    Given that, I think keeping suffixes=true by default for the current API would be the safer and more consistent behavior, while still allowing users to opt out when they explicitly want exact-name OM2 semantics. Making exact-name behavior the default could then be considered for a future API version where the naming contract can be changed consistently across all formats.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions