55import javax .annotation .Nullable ;
66
77/** Immutable container for metric metadata: name, help, unit. */
8- @ StableApi
98public 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}
0 commit comments