Skip to content

Move suffix handling from creation time to scrape time (current release) #1941

@zeitlinger

Description

@zeitlinger

Part of #1942 (full design rationale).

Summary

Today, Counter.Builder strips _total and PrometheusNaming
rejects names ending with _total/_info at creation time. Then
at exposition time, the format writers append _total back. This
conflicts with OM2's principle that metric names should not be
modified.

The fix: stop rejecting these suffixes, and let each format writer
own its suffix conventions at scrape time.

Key table

Format writers adapt at scrape time.

User provides prometheusName OM1 OM2 OTel legacy OTel passthrough
Counter("events") events events_total events events events
Counter("events_total") events events_total events_total events events_total
Gauge("connections") connections connections connections connections connections
Gauge("events_total") events_total events_total events_total events_total events_total
  • Rows 1+2 cannot coexist — the registry detects the
    collision and rejects the second registration.
  • OTel legacy strips _total from Counters only
    (type-aware). Gauges keep _total.
  • OTel passthrough = OM2 = name as provided.

Design decisions

getPrometheusName() stays unchanged

Counter.builder().name() and Info.builder().name() continue
to strip _total/_info so that getPrometheusName() returns
the base name. This avoids a public API break.

The original (unstripped) name is stored separately and used by
format writers for smart-append logic.

Two-layer collision detection

  • Primary key: prometheusName (the stripped base name,
    same as today). Counter("events") and
    Counter("events_total") both map to events.
  • Secondary index: set of all OM1 names a metric would
    produce. When registering, check for intersection with
    already-reserved names. This catches cross-type collisions
    like Gauge("events_total") + Counter("events") which
    both produce events_total in OM1.

This is necessary because removing _total/_info from
reserved suffixes allows Gauge("events_total") for the first
time. Without the secondary index, the cross-type collision
would go undetected — a new bug we'd be introducing.

sanitizeMetricName follows naturally

sanitizeMetricName() strips whatever is in
RESERVED_METRIC_NAME_SUFFIXES. Once _total/_info are
removed from that list, it stops stripping them automatically.
The contract ("returns a name that passes validateMetricName")
is unchanged. No new method needed.

What to change

1. Collision detection in PrometheusRegistry

When registering a metric, compute the OM1 names it would
produce. Reject if any collide with an already-registered
metric's OM1 names.

This replaces today's blanket rejection of _total names with
precise collision checks:

OM1 suffixes to check: _total (Counter), _info (Info),
_bucket/_count/_sum (Histogram/Summary), _created
(Counter/Histogram/Summary).

2. Smart-append in OM1/protobuf writers

Change _total appending from "always append" to "append only
if not already present." Same for _info.

3. Remove reserved suffixes + store original name

  • Remove _total and _info from
    RESERVED_METRIC_NAME_SUFFIXES in PrometheusNaming
  • validateMetricName() stops rejecting these suffixes
  • sanitizeMetricName() stops stripping them (automatically)
  • Store the original user-provided name alongside the stripped
    prometheusName for use by format writers
  • Counter.builder().name() and Info.builder().name() keep
    stripping (preserves getPrometheusName() behavior)

_bucket, _count, _sum, _created remain reserved
(structurally meaningful in OM1).

4. Scrape filter compatibility

scrape(Predicate<String> includedNames) tests both the
original name and the prometheusName against the predicate,
so filtering works regardless of which form the caller uses.

5. OTel exporter: preserve_names flag

Add preserve_names to ExporterOpenTelemetryProperties:

  • false (default): strip _total from Counters + strip
    unit suffix. Preserves current behavior.
  • true (opt-in): no stripping. Name as provided, same
    as OM2.

preserve_names becomes the default in the next major release
(#1943).

Backward compatibility

  • Counter.builder().name("events")prometheusName
    events, OM1: events_total, OTel: events.
    Same as today.
  • Counter.builder().name("events_total")prometheusName
    events, OM1: events_total, OTel legacy: events.
    Same as today.
  • Gauge("events_total") → OM1: events_total, OTel:
    events_total. New capability (previously rejected).
  • sanitizeMetricName("foo_total")foo_total (was foo).
    Fixes Issue when using @Timed annotation with prometheus-metrics-instrumentation-dropwizard #1321.
  • Simpleclient bridge: sanitizeMetricName returns
    events_total → goes into CounterSnapshot metadata →
    format writers smart-append correctly. Output unchanged.

disableSuffixAppending not needed

Suffix behavior is per-format, not a standalone config flag.
OM2 config (#1939) should focus on contentNegotiation,
compositeValues, exemplarCompliance, nativeHistograms.
Suffix handling follows the format automatically.

Metadata

Metadata

Assignees

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