Skip to content

Commit adeef32

Browse files
authored
feat: add MetricMetadata.Builder, deprecate wide constructors (#2202)
## Summary - Adds `MetricMetadata.builder()` with `name`, `help`, `unit`, `counterSuffix` fields - Builder appends unit to the base name when absent, and appends `_total` to `expositionBaseName` when `counterSuffix=true` - Deprecates the 4-arg and 5-arg constructors; internal callers (`MetricMetadataSupport`, `MetricMetadata.escape`) suppress the warning - Updates `docs/apidiffs/current_vs_latest/prometheus-metrics-model.txt` ## Motivation The OTel exporter ([opentelemetry/opentelemetry-java#8346](open-telemetry/opentelemetry-java#8346)) needs to express per-strategy counter intent without pre-computing `expositionBaseName` manually. The builder encapsulates that logic and provides a cleaner public API for any downstream adapter that constructs `MetricMetadata` directly. ## Test plan - [ ] `MetricMetadataTest` — 8 new builder tests covering: no unit, unit absent/present, counter suffix, counter + unit, UTF-8 name, non-counter, name-required validation - [ ] Existing 4-arg/5-arg constructor tests annotated with `@SuppressWarnings("deprecation")` --------- Signed-off-by: Gregor Zeitlinger <gregor.zeitlinger@grafana.com>
1 parent 565d168 commit adeef32

5 files changed

Lines changed: 250 additions & 15 deletions

File tree

docs/apidiffs/current_vs_latest/prometheus-metrics-model.txt

Lines changed: 16 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,13 @@ protected MetricWithFixedMetadata(Builder<?, ?> builder) {
3636
String originalName = builder.originalName;
3737
String expositionBaseName = makeExpositionBaseName(originalName, builder.unit);
3838
this.metadata =
39-
new MetricMetadata(name, expositionBaseName, originalName, builder.help, builder.unit);
39+
MetricMetadata.builder()
40+
.name(name)
41+
.expositionBaseName(expositionBaseName)
42+
.originalName(originalName)
43+
.help(builder.help)
44+
.unit(builder.unit)
45+
.build();
4046
this.labelNames = Arrays.copyOf(builder.labelNames, builder.labelNames.length);
4147
}
4248

prometheus-metrics-model/src/main/java/io/prometheus/metrics/model/snapshots/MetricMetadata.java

Lines changed: 136 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
import javax.annotation.Nullable;
66

77
/** Immutable container for metric metadata: name, help, unit. */
8-
@StableApi
98
public final class MetricMetadata {
109

1110
/**
@@ -51,11 +50,13 @@ public final class MetricMetadata {
5150
@Nullable private final Unit unit;
5251

5352
/** See {@link #MetricMetadata(String, String, Unit)} */
53+
@StableApi
5454
public MetricMetadata(String name) {
5555
this(name, null, null);
5656
}
5757

5858
/** See {@link #MetricMetadata(String, String, Unit)} */
59+
@StableApi
5960
public MetricMetadata(String name, String help) {
6061
this(name, help, null);
6162
}
@@ -69,10 +70,116 @@ public MetricMetadata(String name, String help) {
6970
* @param help optional. May be {@code null}.
7071
* @param unit optional. May be {@code null}.
7172
*/
73+
@StableApi
7274
public MetricMetadata(String name, @Nullable String help, @Nullable Unit unit) {
7375
this(name, name, help, unit);
7476
}
7577

78+
/**
79+
* Creates a builder for {@link MetricMetadata}.
80+
*
81+
* <p>Use the builder instead of the multi-arg constructors for cleaner, more readable code:
82+
*
83+
* <pre>{@code
84+
* MetricMetadata.builder()
85+
* .name("http_requests")
86+
* .help("Total HTTP requests")
87+
* .unit(Unit.BYTES)
88+
* .counterSuffix(true)
89+
* .build();
90+
* }</pre>
91+
*/
92+
@StableApi
93+
public static Builder builder() {
94+
return new Builder();
95+
}
96+
97+
/** Builder for {@link MetricMetadata}. */
98+
public static final class Builder {
99+
@Nullable private String name;
100+
@Nullable private String expositionBaseName;
101+
@Nullable private String originalName;
102+
@Nullable private String help;
103+
@Nullable private Unit unit;
104+
private boolean counterSuffix;
105+
106+
private Builder() {}
107+
108+
/** Required. The base metric name (without type suffix like {@code _total}). */
109+
@StableApi
110+
public Builder name(String name) {
111+
this.name = name;
112+
if (originalName == null) {
113+
this.originalName = name;
114+
}
115+
return this;
116+
}
117+
118+
/**
119+
* Internal use only. Not part of the stable API.
120+
*
121+
* <p>Allows internal callers to preserve a separate exposition base name.
122+
*/
123+
public Builder expositionBaseName(String expositionBaseName) {
124+
this.expositionBaseName = expositionBaseName;
125+
return this;
126+
}
127+
128+
/**
129+
* Internal use only. Not part of the stable API.
130+
*
131+
* <p>Allows internal callers to preserve the raw name before normalization.
132+
*/
133+
public Builder originalName(String originalName) {
134+
this.originalName = originalName;
135+
return this;
136+
}
137+
138+
/** Optional. Human-readable description of the metric. */
139+
@StableApi
140+
public Builder help(@Nullable String help) {
141+
this.help = help;
142+
return this;
143+
}
144+
145+
/** Optional. The unit of measurement. Appended to the name if not already present. */
146+
@StableApi
147+
public Builder unit(@Nullable Unit unit) {
148+
this.unit = unit;
149+
return this;
150+
}
151+
152+
/**
153+
* Optional. When {@code true}, the writer appends {@code _total} to the exposition name. Use
154+
* this for counter metrics, especially UTF-8 names where the writer cannot infer it from the
155+
* snapshot type alone.
156+
*/
157+
@StableApi
158+
public Builder counterSuffix(boolean counterSuffix) {
159+
this.counterSuffix = counterSuffix;
160+
return this;
161+
}
162+
163+
/** Builds the {@link MetricMetadata}. Throws if {@code name} was not set. */
164+
@StableApi
165+
public MetricMetadata build() {
166+
if (name == null) {
167+
throw new IllegalArgumentException("name is required");
168+
}
169+
String baseName = appendUnitIfMissing(name, unit);
170+
String originalName = this.originalName == null ? name : this.originalName;
171+
String expositionBaseName =
172+
appendUnitIfMissing(
173+
this.expositionBaseName == null ? baseName : this.expositionBaseName, unit);
174+
if (counterSuffix
175+
&& !expositionBaseName.endsWith("_total")
176+
&& !expositionBaseName.endsWith(".total")) {
177+
expositionBaseName = expositionBaseName + "_total";
178+
}
179+
return new MetricMetadata(baseName, expositionBaseName, originalName, help, unit);
180+
}
181+
}
182+
76183
/**
77184
* Constructor with exposition base name.
78185
*
@@ -82,7 +189,10 @@ public MetricMetadata(String name, @Nullable String help, @Nullable Unit unit) {
82189
* format writers for smart-append logic
83190
* @param help optional. May be {@code null}.
84191
* @param unit optional. May be {@code null}.
192+
* @deprecated Use {@link #builder()} instead.
85193
*/
194+
@StableApi
195+
@Deprecated
86196
public MetricMetadata(
87197
String name, String expositionBaseName, @Nullable String help, @Nullable Unit unit) {
88198
this(name, expositionBaseName, expositionBaseName, help, unit);
@@ -97,7 +207,10 @@ public MetricMetadata(
97207
* @param originalName the raw name as provided by the user, before any modification
98208
* @param help optional. May be {@code null}.
99209
* @param unit optional. May be {@code null}.
210+
* @deprecated Use {@link #builder()} instead.
100211
*/
212+
@StableApi
213+
@Deprecated
101214
public MetricMetadata(
102215
String name,
103216
String expositionBaseName,
@@ -121,6 +234,7 @@ public MetricMetadata(
121234
* <p>The name may contain any Unicode chars. Use {@link #getPrometheusName()} to get the name in
122235
* legacy Prometheus format, i.e. with all dots and all invalid chars replaced by underscores.
123236
*/
237+
@StableApi
124238
public String getName() {
125239
return name;
126240
}
@@ -130,6 +244,7 @@ public String getName() {
130244
*
131245
* <p>This is used by Prometheus exposition formats.
132246
*/
247+
@StableApi
133248
public String getPrometheusName() {
134249
return prometheusName;
135250
}
@@ -139,6 +254,7 @@ public String getPrometheusName() {
139254
* called {@code Counter.builder().name("req").unit(BYTES)}, this returns "req" while {@link
140255
* #getName()} returns "req_bytes" and {@link #getExpositionBaseName()} returns "req_bytes".
141256
*/
257+
@StableApi
142258
public String getOriginalName() {
143259
return originalName;
144260
}
@@ -148,6 +264,7 @@ public String getOriginalName() {
148264
* if the user called {@code Counter.builder().name("events_total")}, this returns "events_total"
149265
* while {@link #getName()} returns "events".
150266
*/
267+
@StableApi
151268
public String getExpositionBaseName() {
152269
return expositionBaseName;
153270
}
@@ -156,24 +273,35 @@ public String getExpositionBaseName() {
156273
* Same as {@link #getExpositionBaseName()} but with all invalid characters and dots replaced by
157274
* underscores.
158275
*/
276+
@StableApi
159277
public String getExpositionBasePrometheusName() {
160278
return expositionBasePrometheusName;
161279
}
162280

281+
@StableApi
163282
@Nullable
164283
public String getHelp() {
165284
return help;
166285
}
167286

287+
@StableApi
168288
public boolean hasUnit() {
169289
return unit != null;
170290
}
171291

292+
@StableApi
172293
@Nullable
173294
public Unit getUnit() {
174295
return unit;
175296
}
176297

298+
private static String appendUnitIfMissing(String name, @Nullable Unit unit) {
299+
if (unit != null && !name.endsWith("_" + unit) && !name.endsWith("." + unit)) {
300+
return name + "_" + unit;
301+
}
302+
return name;
303+
}
304+
177305
private void validate() {
178306
if (name == null) {
179307
throw new IllegalArgumentException("Missing required field: name is null");
@@ -206,11 +334,12 @@ private void validate() {
206334
}
207335

208336
MetricMetadata escape(EscapingScheme escapingScheme) {
209-
return new MetricMetadata(
210-
PrometheusNaming.escapeName(name, escapingScheme),
211-
PrometheusNaming.escapeName(expositionBaseName, escapingScheme),
212-
PrometheusNaming.escapeName(originalName, escapingScheme),
213-
help,
214-
unit);
337+
return MetricMetadata.builder()
338+
.name(PrometheusNaming.escapeName(name, escapingScheme))
339+
.expositionBaseName(PrometheusNaming.escapeName(expositionBaseName, escapingScheme))
340+
.originalName(PrometheusNaming.escapeName(originalName, escapingScheme))
341+
.help(help)
342+
.unit(unit)
343+
.build();
215344
}
216345
}

prometheus-metrics-model/src/main/java/io/prometheus/metrics/model/snapshots/MetricMetadataSupport.java

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,13 @@ private static MetricMetadata typedMetadata(
2525
String suffix,
2626
String dotSuffix) {
2727
String baseName = stripSuffix(originalName, suffix, dotSuffix);
28-
return new MetricMetadata(
29-
appendUnitIfMissing(baseName, unit),
30-
appendUnitIfMissing(originalName, unit),
31-
originalName,
32-
help,
33-
unit);
28+
return MetricMetadata.builder()
29+
.name(appendUnitIfMissing(baseName, unit))
30+
.expositionBaseName(appendUnitIfMissing(originalName, unit))
31+
.originalName(originalName)
32+
.help(help)
33+
.unit(unit)
34+
.build();
3435
}
3536

3637
private static String appendUnitIfMissing(String name, @Nullable Unit unit) {

0 commit comments

Comments
 (0)