001package io.prometheus.metrics.model.snapshots;
002
003import io.prometheus.metrics.annotations.StableApi;
004import io.prometheus.metrics.config.EscapingScheme;
005import javax.annotation.Nullable;
006
007/** Immutable container for metric metadata: name, help, unit. */
008public final class MetricMetadata {
009
010  /**
011   * Name without suffix.
012   *
013   * <p>For example, the name for a counter "http_requests_total" is "http_requests". The name of an
014   * info called "jvm_info" is "jvm".
015   *
016   * <p>We allow dots in label names. Dots are automatically replaced with underscores in Prometheus
017   * exposition formats. However, if metrics from this library are exposed in OpenTelemetry format
018   * dots are retained.
019   *
020   * <p>See {@link #MetricMetadata(String, String, Unit)} for more info on naming conventions.
021   */
022  private final String name;
023
024  /**
025   * Same as name that all invalid char (without Unicode support) are replaced by _
026   *
027   * <p>Multiple metrics with the same prometheusName are not allowed, because they would end up in
028   * the same time series in Prometheus if {@link EscapingScheme#UNDERSCORE_ESCAPING} or {@link
029   * EscapingScheme#DOTS_ESCAPING} is used.
030   */
031  private final String prometheusName;
032
033  /**
034   * The base name for exposition, with unit suffix ensured and type suffix preserved. For example,
035   * for {@code Counter.builder().name("events_total").unit(BYTES)}, this is "events_total_bytes".
036   * Used by format writers for smart-append logic (e.g. deciding whether to append _total).
037   */
038  private final String expositionBaseName;
039
040  private final String expositionBasePrometheusName;
041
042  /**
043   * The original name as provided by the user, before any modification (no suffix stripping, no
044   * unit appending). For example, for {@code Counter.builder().name("req").unit(BYTES)}, this is
045   * "req". Used by the OTel exporter with {@code preserve_names=true}.
046   */
047  private final String originalName;
048
049  @Nullable private final String help;
050  @Nullable private final Unit unit;
051
052  /** See {@link #MetricMetadata(String, String, Unit)} */
053  @StableApi
054  public MetricMetadata(String name) {
055    this(name, null, null);
056  }
057
058  /** See {@link #MetricMetadata(String, String, Unit)} */
059  @StableApi
060  public MetricMetadata(String name, String help) {
061    this(name, help, null);
062  }
063
064  /**
065   * Constructor.
066   *
067   * @param name must not be {@code null}. {@link PrometheusNaming#isValidMetricName(String)
068   *     isValidMetricName(name)} must be {@code true}. Use {@link
069   *     PrometheusNaming#sanitizeMetricName(String)} to convert arbitrary strings into valid names.
070   * @param help optional. May be {@code null}.
071   * @param unit optional. May be {@code null}.
072   */
073  @StableApi
074  public MetricMetadata(String name, @Nullable String help, @Nullable Unit unit) {
075    this(name, name, help, unit);
076  }
077
078  /**
079   * Creates a builder for {@link MetricMetadata}.
080   *
081   * <p>Use the builder instead of the multi-arg constructors for cleaner, more readable code:
082   *
083   * <pre>{@code
084   * MetricMetadata.builder()
085   *     .name("http_requests")
086   *     .help("Total HTTP requests")
087   *     .unit(Unit.BYTES)
088   *     .counterSuffix(true)
089   *     .build();
090   * }</pre>
091   */
092  @StableApi
093  public static Builder builder() {
094    return new Builder();
095  }
096
097  /** Builder for {@link MetricMetadata}. */
098  public static final class Builder {
099    @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
183  /**
184   * Constructor with exposition base name.
185   *
186   * @param name the base name (with type suffixes stripped, e.g. "events" for a counter named
187   *     "events_total")
188   * @param expositionBaseName the name with unit suffix ensured and type suffix preserved, used by
189   *     format writers for smart-append logic
190   * @param help optional. May be {@code null}.
191   * @param unit optional. May be {@code null}.
192   * @deprecated Use {@link #builder()} instead.
193   */
194  @StableApi
195  @Deprecated
196  public MetricMetadata(
197      String name, String expositionBaseName, @Nullable String help, @Nullable Unit unit) {
198    this(name, expositionBaseName, expositionBaseName, help, unit);
199  }
200
201  /**
202   * Constructor with exposition base name and original name.
203   *
204   * @param name the base name (with type suffixes stripped, e.g. "events" for a counter named
205   *     "events_total")
206   * @param expositionBaseName the name with unit suffix ensured and type suffix preserved
207   * @param originalName the raw name as provided by the user, before any modification
208   * @param help optional. May be {@code null}.
209   * @param unit optional. May be {@code null}.
210   * @deprecated Use {@link #builder()} instead.
211   */
212  @StableApi
213  @Deprecated
214  public MetricMetadata(
215      String name,
216      String expositionBaseName,
217      String originalName,
218      @Nullable String help,
219      @Nullable Unit unit) {
220    this.name = name;
221    this.expositionBaseName = expositionBaseName;
222    this.originalName = originalName;
223    this.help = help;
224    this.unit = unit;
225    validate();
226    this.prometheusName = PrometheusNaming.prometheusName(name);
227    this.expositionBasePrometheusName = PrometheusNaming.prometheusName(expositionBaseName);
228  }
229
230  /**
231   * The name does not include the {@code _total} suffix for counter metrics or the {@code _info}
232   * suffix for Info metrics.
233   *
234   * <p>The name may contain any Unicode chars. Use {@link #getPrometheusName()} to get the name in
235   * legacy Prometheus format, i.e. with all dots and all invalid chars replaced by underscores.
236   */
237  @StableApi
238  public String getName() {
239    return name;
240  }
241
242  /**
243   * Same as {@link #getName()} but with all invalid characters and dots replaced by underscores.
244   *
245   * <p>This is used by Prometheus exposition formats.
246   */
247  @StableApi
248  public String getPrometheusName() {
249    return prometheusName;
250  }
251
252  /**
253   * The original name as provided by the user, before any modification. For example, if the user
254   * called {@code Counter.builder().name("req").unit(BYTES)}, this returns "req" while {@link
255   * #getName()} returns "req_bytes" and {@link #getExpositionBaseName()} returns "req_bytes".
256   */
257  @StableApi
258  public String getOriginalName() {
259    return originalName;
260  }
261
262  /**
263   * The base name for exposition, with unit suffix ensured and type suffix preserved. For example,
264   * if the user called {@code Counter.builder().name("events_total")}, this returns "events_total"
265   * while {@link #getName()} returns "events".
266   */
267  @StableApi
268  public String getExpositionBaseName() {
269    return expositionBaseName;
270  }
271
272  /**
273   * Same as {@link #getExpositionBaseName()} but with all invalid characters and dots replaced by
274   * underscores.
275   */
276  @StableApi
277  public String getExpositionBasePrometheusName() {
278    return expositionBasePrometheusName;
279  }
280
281  @StableApi
282  @Nullable
283  public String getHelp() {
284    return help;
285  }
286
287  @StableApi
288  public boolean hasUnit() {
289    return unit != null;
290  }
291
292  @StableApi
293  @Nullable
294  public Unit getUnit() {
295    return unit;
296  }
297
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
305  private void validate() {
306    if (name == null) {
307      throw new IllegalArgumentException("Missing required field: name is null");
308    }
309    String error = PrometheusNaming.validateMetricName(name);
310    if (error != null) {
311      throw new IllegalArgumentException(
312          "'"
313              + name
314              + "': Illegal metric name. "
315              + error
316              + " Call "
317              + PrometheusNaming.class.getSimpleName()
318              + ".sanitizeMetricName(name) to avoid this error.");
319    }
320    if (hasUnit()) {
321      if (!name.endsWith("_" + unit) && !name.endsWith("." + unit)) {
322        throw new IllegalArgumentException(
323            "'"
324                + name
325                + "': Illegal metric name. If the unit is non-null, "
326                + "the name must end with the unit: _"
327                + unit
328                + "."
329                + " Call "
330                + PrometheusNaming.class.getSimpleName()
331                + ".sanitizeMetricName(name, unit) to avoid this error.");
332      }
333    }
334  }
335
336  MetricMetadata escape(EscapingScheme escapingScheme) {
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();
344  }
345}