Skip to content

Commit 5a5106c

Browse files
authored
feat: move suffix handling to scrape time (#1955)
## Summary Moves metric name suffix handling (`_total`, `_info`, unit suffixes) from creation time to scrape time. Closes #1941, part of #1942. - **OM1**: smart-appends suffixes (skips if already present) - **Registry**: detects cross-format name collisions at registration time ### Key changes - Remove all reserved metric name suffixes from `PrometheusNaming` - Store original user-provided name separately from exposition base name in `MetricMetadata` (`originalName` vs `expositionBaseName`) - Smart-append logic in OM1/protobuf writers for `_total` and `_info` - Two-layer collision detection in `PrometheusRegistry` (base name + exposition names) ### Key table | User provides | OM1 | |---|---| | `Counter("events")` | `events_total` | | `Counter("events_total")` | `events_total` | | `Counter("req").unit(BYTES)` | `req_bytes_total` | | `Counter("req_bytes").unit(BYTES)` | `req_bytes_total` | | `Gauge("events_total")` | `events_total` | ### PR stack 1. **This PR** — core model + OM1/protobuf writers 2. OTel `preserve_names` (stacked on this) 3. OM2 writer no-suffix (stacked on this) ## Test plan - [x] `mise run compile` passes - [x] New tests for `MetricMetadata` 5-arg constructor and field accessors - [x] New `PrometheusRegistryTest` covers collision detection - [x] Existing snapshot tests updated Part of #1912. --------- Signed-off-by: Gregor Zeitlinger <gregor.zeitlinger@grafana.com>
1 parent 165c921 commit 5a5106c

File tree

17 files changed

+657
-192
lines changed

17 files changed

+657
-192
lines changed

prometheus-metrics-core/src/main/java/io/prometheus/metrics/core/metrics/Counter.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -241,7 +241,7 @@ private Builder(PrometheusProperties properties) {
241241
*/
242242
@Override
243243
public Builder name(String name) {
244-
return super.name(stripTotalSuffix(name));
244+
return super.nameWithOriginal(stripTotalSuffix(name), name);
245245
}
246246

247247
@Override

prometheus-metrics-core/src/main/java/io/prometheus/metrics/core/metrics/Info.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ private Builder(PrometheusProperties config) {
146146
*/
147147
@Override
148148
public Builder name(String name) {
149-
return super.name(stripInfoSuffix(name));
149+
return super.nameWithOriginal(stripInfoSuffix(name), name);
150150
}
151151

152152
/** Throws an {@link UnsupportedOperationException} because Info metrics cannot have a unit. */

prometheus-metrics-core/src/main/java/io/prometheus/metrics/core/metrics/MetricWithFixedMetadata.java

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,14 @@ public abstract class MetricWithFixedMetadata extends Metric {
2525

2626
protected MetricWithFixedMetadata(Builder<?, ?> builder) {
2727
super(builder);
28+
String name = makeName(builder.name, builder.unit);
29+
if (builder.originalName == null) {
30+
throw new IllegalArgumentException("Missing required field: name is null");
31+
}
32+
String originalName = builder.originalName;
33+
String expositionBaseName = makeExpositionBaseName(originalName, builder.unit);
2834
this.metadata =
29-
new MetricMetadata(makeName(builder.name, builder.unit), builder.help, builder.unit);
35+
new MetricMetadata(name, expositionBaseName, originalName, builder.help, builder.unit);
3036
this.labelNames = Arrays.copyOf(builder.labelNames, builder.labelNames.length);
3137
}
3238

@@ -47,6 +53,18 @@ private String makeName(@Nullable String name, @Nullable Unit unit) {
4753
return name;
4854
}
4955

56+
private String makeExpositionBaseName(@Nullable String expositionBaseName, @Nullable Unit unit) {
57+
if (expositionBaseName == null) {
58+
throw new IllegalArgumentException("Missing required field: name is null");
59+
}
60+
if (unit != null) {
61+
if (!expositionBaseName.endsWith("_" + unit) && !expositionBaseName.endsWith("." + unit)) {
62+
expositionBaseName += "_" + unit;
63+
}
64+
}
65+
return expositionBaseName;
66+
}
67+
5068
@Override
5169
public String getPrometheusName() {
5270
return metadata.getPrometheusName();
@@ -68,6 +86,7 @@ public abstract static class Builder<B extends Builder<B, M>, M extends MetricWi
6886
extends Metric.Builder<B, M> {
6987

7088
@Nullable private String name;
89+
@Nullable private String originalName;
7190
@Nullable private Unit unit;
7291
@Nullable private String help;
7392
private String[] labelNames = new String[0];
@@ -82,6 +101,25 @@ public B name(String name) {
82101
throw new IllegalArgumentException("'" + name + "': Illegal metric name: " + error);
83102
}
84103
this.name = name;
104+
this.originalName = name;
105+
return self();
106+
}
107+
108+
/**
109+
* Set the metric name and original name separately. Used by Counter and Info builders which
110+
* strip type suffixes from the name but preserve the original for exposition.
111+
*/
112+
protected B nameWithOriginal(String name, String originalName) {
113+
String error = PrometheusNaming.validateMetricName(name);
114+
if (error != null) {
115+
throw new IllegalArgumentException("'" + name + "': Illegal metric name: " + error);
116+
}
117+
error = PrometheusNaming.validateMetricName(originalName);
118+
if (error != null) {
119+
throw new IllegalArgumentException("'" + originalName + "': Illegal metric name: " + error);
120+
}
121+
this.name = name;
122+
this.originalName = originalName;
85123
return self();
86124
}
87125

prometheus-metrics-exposition-formats/src/main/java/io/prometheus/metrics/expositionformats/internal/PrometheusProtobufWriterImpl.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -296,7 +296,12 @@ private void setMetadataUnlessEmpty(
296296
if (nameSuffix == null) {
297297
builder.setName(SnapshotEscaper.getMetadataName(metadata, scheme));
298298
} else {
299-
builder.setName(SnapshotEscaper.getMetadataName(metadata, scheme) + nameSuffix);
299+
String expositionBaseName = SnapshotEscaper.getExpositionBaseMetadataName(metadata, scheme);
300+
if (expositionBaseName.endsWith(nameSuffix)) {
301+
builder.setName(expositionBaseName);
302+
} else {
303+
builder.setName(SnapshotEscaper.getMetadataName(metadata, scheme) + nameSuffix);
304+
}
300305
}
301306
if (metadata.getHelp() != null) {
302307
builder.setHelp(metadata.getHelp());

prometheus-metrics-exposition-textformats/src/main/java/io/prometheus/metrics/expositionformats/OpenMetricsTextFormatWriter.java

Lines changed: 46 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import static io.prometheus.metrics.expositionformats.TextFormatUtil.writeLong;
77
import static io.prometheus.metrics.expositionformats.TextFormatUtil.writeName;
88
import static io.prometheus.metrics.expositionformats.TextFormatUtil.writeOpenMetricsTimestamp;
9+
import static io.prometheus.metrics.model.snapshots.SnapshotEscaper.getExpositionBaseMetadataName;
910
import static io.prometheus.metrics.model.snapshots.SnapshotEscaper.getMetadataName;
1011
import static io.prometheus.metrics.model.snapshots.SnapshotEscaper.getSnapshotLabelName;
1112

@@ -141,13 +142,14 @@ public void write(OutputStream out, MetricSnapshots metricSnapshots, EscapingSch
141142
private void writeCounter(Writer writer, CounterSnapshot snapshot, EscapingScheme scheme)
142143
throws IOException {
143144
MetricMetadata metadata = snapshot.getMetadata();
144-
writeMetadata(writer, "counter", metadata, scheme);
145+
String counterName = resolveExpositionName(metadata, "_total", scheme);
146+
String baseName = resolveBaseName(counterName, "_total");
147+
writeMetadataWithName(writer, baseName, "counter", metadata);
145148
for (CounterSnapshot.CounterDataPointSnapshot data : snapshot.getDataPoints()) {
146-
writeNameAndLabels(
147-
writer, getMetadataName(metadata, scheme), "_total", data.getLabels(), scheme);
149+
writeNameAndLabels(writer, counterName, null, data.getLabels(), scheme);
148150
writeDouble(writer, data.getValue());
149151
writeScrapeTimestampAndExemplar(writer, data, data.getExemplar(), scheme);
150-
writeCreated(writer, metadata, data, scheme);
152+
writeCreated(writer, baseName, data, scheme);
151153
}
152154
}
153155

@@ -274,10 +276,11 @@ private void writeSummary(Writer writer, SummarySnapshot snapshot, EscapingSchem
274276
private void writeInfo(Writer writer, InfoSnapshot snapshot, EscapingScheme scheme)
275277
throws IOException {
276278
MetricMetadata metadata = snapshot.getMetadata();
277-
writeMetadata(writer, "info", metadata, scheme);
279+
String infoName = resolveExpositionName(metadata, "_info", scheme);
280+
String baseName = resolveBaseName(infoName, "_info");
281+
writeMetadataWithName(writer, baseName, "info", metadata);
278282
for (InfoSnapshot.InfoDataPointSnapshot data : snapshot.getDataPoints()) {
279-
writeNameAndLabels(
280-
writer, getMetadataName(metadata, scheme), "_info", data.getLabels(), scheme);
283+
writeNameAndLabels(writer, infoName, null, data.getLabels(), scheme);
281284
writer.write("1");
282285
writeScrapeTimestampAndExemplar(writer, data, null, scheme);
283286
}
@@ -363,9 +366,14 @@ private void writeCountAndSum(
363366
private void writeCreated(
364367
Writer writer, MetricMetadata metadata, DataPointSnapshot data, EscapingScheme scheme)
365368
throws IOException {
369+
writeCreated(writer, getMetadataName(metadata, scheme), data, scheme);
370+
}
371+
372+
private void writeCreated(
373+
Writer writer, String baseName, DataPointSnapshot data, EscapingScheme scheme)
374+
throws IOException {
366375
if (createdTimestampsEnabled && data.hasCreatedTimestamp()) {
367-
writeNameAndLabels(
368-
writer, getMetadataName(metadata, scheme), "_created", data.getLabels(), scheme);
376+
writeNameAndLabels(writer, baseName, "_created", data.getLabels(), scheme);
369377
writeOpenMetricsTimestamp(writer, data.getCreatedTimestampMillis());
370378
if (data.hasScrapeTimestamp()) {
371379
writer.write(' ');
@@ -436,27 +444,53 @@ private void writeScrapeTimestampAndExemplar(
436444
writer.write('\n');
437445
}
438446

447+
/**
448+
* Returns the full exposition name for a metric. If the original name already ends with the given
449+
* suffix (e.g. "_total" for counters), uses the original name directly. Otherwise, appends the
450+
* suffix to the base name.
451+
*/
452+
private static String resolveExpositionName(
453+
MetricMetadata metadata, String suffix, EscapingScheme scheme) {
454+
String expositionBaseName = getExpositionBaseMetadataName(metadata, scheme);
455+
if (expositionBaseName.endsWith(suffix)) {
456+
return expositionBaseName;
457+
}
458+
return getMetadataName(metadata, scheme) + suffix;
459+
}
460+
439461
private void writeMetadata(
440462
Writer writer, String typeName, MetricMetadata metadata, EscapingScheme scheme)
441463
throws IOException {
464+
writeMetadataWithName(writer, getMetadataName(metadata, scheme), typeName, metadata);
465+
}
466+
467+
private void writeMetadataWithName(
468+
Writer writer, String name, String typeName, MetricMetadata metadata) throws IOException {
442469
writer.write("# TYPE ");
443-
writeName(writer, getMetadataName(metadata, scheme), NameType.Metric);
470+
writeName(writer, name, NameType.Metric);
444471
writer.write(' ');
445472
writer.write(typeName);
446473
writer.write('\n');
447474
if (metadata.getUnit() != null) {
448475
writer.write("# UNIT ");
449-
writeName(writer, getMetadataName(metadata, scheme), NameType.Metric);
476+
writeName(writer, name, NameType.Metric);
450477
writer.write(' ');
451478
writeEscapedString(writer, metadata.getUnit().toString());
452479
writer.write('\n');
453480
}
454481
if (metadata.getHelp() != null && !metadata.getHelp().isEmpty()) {
455482
writer.write("# HELP ");
456-
writeName(writer, getMetadataName(metadata, scheme), NameType.Metric);
483+
writeName(writer, name, NameType.Metric);
457484
writer.write(' ');
458485
writeEscapedString(writer, metadata.getHelp());
459486
writer.write('\n');
460487
}
461488
}
489+
490+
private static String resolveBaseName(String fullName, String suffix) {
491+
if (fullName.endsWith(suffix)) {
492+
return fullName.substring(0, fullName.length() - suffix.length());
493+
}
494+
return fullName;
495+
}
462496
}

prometheus-metrics-exposition-textformats/src/main/java/io/prometheus/metrics/expositionformats/PrometheusTextFormatWriter.java

Lines changed: 51 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import static io.prometheus.metrics.expositionformats.TextFormatUtil.writeName;
88
import static io.prometheus.metrics.expositionformats.TextFormatUtil.writePrometheusTimestamp;
99
import static io.prometheus.metrics.model.snapshots.SnapshotEscaper.escapeMetricSnapshot;
10+
import static io.prometheus.metrics.model.snapshots.SnapshotEscaper.getExpositionBaseMetadataName;
1011
import static io.prometheus.metrics.model.snapshots.SnapshotEscaper.getMetadataName;
1112
import static io.prometheus.metrics.model.snapshots.SnapshotEscaper.getSnapshotLabelName;
1213

@@ -157,14 +158,17 @@ public void writeCreated(Writer writer, MetricSnapshot snapshot, EscapingScheme
157158
throws IOException {
158159
boolean metadataWritten = false;
159160
MetricMetadata metadata = snapshot.getMetadata();
161+
String baseName = getMetadataName(metadata, scheme);
162+
if (snapshot instanceof CounterSnapshot) {
163+
baseName = resolveBaseName(resolveExpositionName(metadata, "_total", scheme), "_total");
164+
}
160165
for (DataPointSnapshot data : snapshot.getDataPoints()) {
161166
if (data.hasCreatedTimestamp()) {
162167
if (!metadataWritten) {
163-
writeMetadata(writer, "_created", "gauge", metadata, scheme);
168+
writeMetadataWithFullName(writer, baseName + "_created", "gauge", metadata);
164169
metadataWritten = true;
165170
}
166-
writeNameAndLabels(
167-
writer, getMetadataName(metadata, scheme), "_created", data.getLabels(), scheme);
171+
writeNameAndLabels(writer, baseName, "_created", data.getLabels(), scheme);
168172
writePrometheusTimestamp(writer, data.getCreatedTimestampMillis(), timestampsInMs);
169173
writeScrapeTimestampAndNewline(writer, data);
170174
}
@@ -175,10 +179,10 @@ private void writeCounter(Writer writer, CounterSnapshot snapshot, EscapingSchem
175179
throws IOException {
176180
if (!snapshot.getDataPoints().isEmpty()) {
177181
MetricMetadata metadata = snapshot.getMetadata();
178-
writeMetadata(writer, "_total", "counter", metadata, scheme);
182+
String counterName = resolveExpositionName(metadata, "_total", scheme);
183+
writeMetadataWithFullName(writer, counterName, "counter", metadata);
179184
for (CounterSnapshot.CounterDataPointSnapshot data : snapshot.getDataPoints()) {
180-
writeNameAndLabels(
181-
writer, getMetadataName(metadata, scheme), "_total", data.getLabels(), scheme);
185+
writeNameAndLabels(writer, counterName, null, data.getLabels(), scheme);
182186
writeDouble(writer, data.getValue());
183187
writeScrapeTimestampAndNewline(writer, data);
184188
}
@@ -321,10 +325,10 @@ private void writeSummary(Writer writer, SummarySnapshot snapshot, EscapingSchem
321325
private void writeInfo(Writer writer, InfoSnapshot snapshot, EscapingScheme scheme)
322326
throws IOException {
323327
MetricMetadata metadata = snapshot.getMetadata();
324-
writeMetadata(writer, "_info", "gauge", metadata, scheme);
328+
String infoName = resolveExpositionName(metadata, "_info", scheme);
329+
writeMetadataWithFullName(writer, infoName, "gauge", metadata);
325330
for (InfoSnapshot.InfoDataPointSnapshot data : snapshot.getDataPoints()) {
326-
writeNameAndLabels(
327-
writer, getMetadataName(metadata, scheme), "_info", data.getLabels(), scheme);
331+
writeNameAndLabels(writer, infoName, null, data.getLabels(), scheme);
328332
writer.write("1");
329333
writeScrapeTimestampAndNewline(writer, data);
330334
}
@@ -433,6 +437,44 @@ private void writeMetadata(
433437
writer.write('\n');
434438
}
435439

440+
private void writeMetadataWithFullName(
441+
Writer writer, String fullName, String typeString, MetricMetadata metadata)
442+
throws IOException {
443+
if (metadata.getHelp() != null && !metadata.getHelp().isEmpty()) {
444+
writer.write("# HELP ");
445+
writeName(writer, fullName, NameType.Metric);
446+
writer.write(' ');
447+
writeEscapedHelp(writer, metadata.getHelp());
448+
writer.write('\n');
449+
}
450+
writer.write("# TYPE ");
451+
writeName(writer, fullName, NameType.Metric);
452+
writer.write(' ');
453+
writer.write(typeString);
454+
writer.write('\n');
455+
}
456+
457+
/**
458+
* Returns the full exposition name for a metric. If the original name already ends with the given
459+
* suffix (e.g. "_total" for counters), uses the original name directly. Otherwise, appends the
460+
* suffix to the base name.
461+
*/
462+
private static String resolveExpositionName(
463+
MetricMetadata metadata, String suffix, EscapingScheme scheme) {
464+
String expositionBaseName = getExpositionBaseMetadataName(metadata, scheme);
465+
if (expositionBaseName.endsWith(suffix)) {
466+
return expositionBaseName;
467+
}
468+
return getMetadataName(metadata, scheme) + suffix;
469+
}
470+
471+
private static String resolveBaseName(String fullName, String suffix) {
472+
if (fullName.endsWith(suffix)) {
473+
return fullName.substring(0, fullName.length() - suffix.length());
474+
}
475+
return fullName;
476+
}
477+
436478
private void writeEscapedHelp(Writer writer, String s) throws IOException {
437479
for (int i = 0; i < s.length(); i++) {
438480
char c = s.charAt(i);

prometheus-metrics-model/src/main/java/io/prometheus/metrics/model/registry/Collector.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@ default MetricSnapshot collect(PrometheusScrapeRequest scrapeRequest) {
3434
@Nullable
3535
default MetricSnapshot collect(Predicate<String> includedNames) {
3636
MetricSnapshot result = collect();
37-
if (includedNames.test(result.getMetadata().getPrometheusName())) {
37+
if (includedNames.test(result.getMetadata().getPrometheusName())
38+
|| includedNames.test(result.getMetadata().getExpositionBasePrometheusName())) {
3839
return result;
3940
} else {
4041
return null;
@@ -51,7 +52,8 @@ default MetricSnapshot collect(Predicate<String> includedNames) {
5152
default MetricSnapshot collect(
5253
Predicate<String> includedNames, PrometheusScrapeRequest scrapeRequest) {
5354
MetricSnapshot result = collect(scrapeRequest);
54-
if (includedNames.test(result.getMetadata().getPrometheusName())) {
55+
if (includedNames.test(result.getMetadata().getPrometheusName())
56+
|| includedNames.test(result.getMetadata().getExpositionBasePrometheusName())) {
5557
return result;
5658
} else {
5759
return null;

0 commit comments

Comments
 (0)