-
Notifications
You must be signed in to change notification settings - Fork 832
Description
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
_totalfrom 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 toevents. - 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
likeGauge("events_total")+Counter("events")which
both produceevents_totalin 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:
Gauge("foo_total")+Histogram("foo")→ no collision
(OM1:foo_totalvsfoo_count/foo_sum/foo_bucket).
Fixes Issue when using @Timed annotation with prometheus-metrics-instrumentation-dropwizard #1321.Gauge("foo_total")+Counter("foo")→ collision in
OM1 (foo_totalvsfoo_total). Correctly rejected.Gauge("foo_count")+Histogram("foo")→ collision in
OM1. Fixes the known gap documented inPrometheusNaming.
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
_totaland_infofrom
RESERVED_METRIC_NAME_SUFFIXESinPrometheusNaming validateMetricName()stops rejecting these suffixessanitizeMetricName()stops stripping them (automatically)- Store the original user-provided name alongside the stripped
prometheusNamefor use by format writers Counter.builder().name()andInfo.builder().name()keep
stripping (preservesgetPrometheusName()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_totalfrom 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(wasfoo).
Fixes Issue when using @Timed annotation with prometheus-metrics-instrumentation-dropwizard #1321.- Simpleclient bridge:
sanitizeMetricNamereturns
events_total→ goes intoCounterSnapshotmetadata →
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.