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. */
008@StableApi
009public final class MetricMetadata {
010
011  /**
012   * Name without suffix.
013   *
014   * <p>For example, the name for a counter "http_requests_total" is "http_requests". The name of an
015   * info called "jvm_info" is "jvm".
016   *
017   * <p>We allow dots in label names. Dots are automatically replaced with underscores in Prometheus
018   * exposition formats. However, if metrics from this library are exposed in OpenTelemetry format
019   * dots are retained.
020   *
021   * <p>See {@link #MetricMetadata(String, String, Unit)} for more info on naming conventions.
022   */
023  private final String name;
024
025  /**
026   * Same as name that all invalid char (without Unicode support) are replaced by _
027   *
028   * <p>Multiple metrics with the same prometheusName are not allowed, because they would end up in
029   * the same time series in Prometheus if {@link EscapingScheme#UNDERSCORE_ESCAPING} or {@link
030   * EscapingScheme#DOTS_ESCAPING} is used.
031   */
032  private final String prometheusName;
033
034  /**
035   * The base name for exposition, with unit suffix ensured and type suffix preserved. For example,
036   * for {@code Counter.builder().name("events_total").unit(BYTES)}, this is "events_total_bytes".
037   * Used by format writers for smart-append logic (e.g. deciding whether to append _total).
038   */
039  private final String expositionBaseName;
040
041  private final String expositionBasePrometheusName;
042
043  /**
044   * The original name as provided by the user, before any modification (no suffix stripping, no
045   * unit appending). For example, for {@code Counter.builder().name("req").unit(BYTES)}, this is
046   * "req". Used by the OTel exporter with {@code preserve_names=true}.
047   */
048  private final String originalName;
049
050  @Nullable private final String help;
051  @Nullable private final Unit unit;
052
053  /** See {@link #MetricMetadata(String, String, Unit)} */
054  public MetricMetadata(String name) {
055    this(name, null, null);
056  }
057
058  /** See {@link #MetricMetadata(String, String, Unit)} */
059  public MetricMetadata(String name, String help) {
060    this(name, help, null);
061  }
062
063  /**
064   * Constructor.
065   *
066   * @param name must not be {@code null}. {@link PrometheusNaming#isValidMetricName(String)
067   *     isValidMetricName(name)} must be {@code true}. Use {@link
068   *     PrometheusNaming#sanitizeMetricName(String)} to convert arbitrary strings into valid names.
069   * @param help optional. May be {@code null}.
070   * @param unit optional. May be {@code null}.
071   */
072  public MetricMetadata(String name, @Nullable String help, @Nullable Unit unit) {
073    this(name, name, help, unit);
074  }
075
076  /**
077   * Constructor with exposition base name.
078   *
079   * @param name the base name (with type suffixes stripped, e.g. "events" for a counter named
080   *     "events_total")
081   * @param expositionBaseName the name with unit suffix ensured and type suffix preserved, used by
082   *     format writers for smart-append logic
083   * @param help optional. May be {@code null}.
084   * @param unit optional. May be {@code null}.
085   */
086  public MetricMetadata(
087      String name, String expositionBaseName, @Nullable String help, @Nullable Unit unit) {
088    this(name, expositionBaseName, expositionBaseName, help, unit);
089  }
090
091  /**
092   * Constructor with exposition base name and original name.
093   *
094   * @param name the base name (with type suffixes stripped, e.g. "events" for a counter named
095   *     "events_total")
096   * @param expositionBaseName the name with unit suffix ensured and type suffix preserved
097   * @param originalName the raw name as provided by the user, before any modification
098   * @param help optional. May be {@code null}.
099   * @param unit optional. May be {@code null}.
100   */
101  public MetricMetadata(
102      String name,
103      String expositionBaseName,
104      String originalName,
105      @Nullable String help,
106      @Nullable Unit unit) {
107    this.name = name;
108    this.expositionBaseName = expositionBaseName;
109    this.originalName = originalName;
110    this.help = help;
111    this.unit = unit;
112    validate();
113    this.prometheusName = PrometheusNaming.prometheusName(name);
114    this.expositionBasePrometheusName = PrometheusNaming.prometheusName(expositionBaseName);
115  }
116
117  /**
118   * The name does not include the {@code _total} suffix for counter metrics or the {@code _info}
119   * suffix for Info metrics.
120   *
121   * <p>The name may contain any Unicode chars. Use {@link #getPrometheusName()} to get the name in
122   * legacy Prometheus format, i.e. with all dots and all invalid chars replaced by underscores.
123   */
124  public String getName() {
125    return name;
126  }
127
128  /**
129   * Same as {@link #getName()} but with all invalid characters and dots replaced by underscores.
130   *
131   * <p>This is used by Prometheus exposition formats.
132   */
133  public String getPrometheusName() {
134    return prometheusName;
135  }
136
137  /**
138   * The original name as provided by the user, before any modification. For example, if the user
139   * called {@code Counter.builder().name("req").unit(BYTES)}, this returns "req" while {@link
140   * #getName()} returns "req_bytes" and {@link #getExpositionBaseName()} returns "req_bytes".
141   */
142  public String getOriginalName() {
143    return originalName;
144  }
145
146  /**
147   * The base name for exposition, with unit suffix ensured and type suffix preserved. For example,
148   * if the user called {@code Counter.builder().name("events_total")}, this returns "events_total"
149   * while {@link #getName()} returns "events".
150   */
151  public String getExpositionBaseName() {
152    return expositionBaseName;
153  }
154
155  /**
156   * Same as {@link #getExpositionBaseName()} but with all invalid characters and dots replaced by
157   * underscores.
158   */
159  public String getExpositionBasePrometheusName() {
160    return expositionBasePrometheusName;
161  }
162
163  @Nullable
164  public String getHelp() {
165    return help;
166  }
167
168  public boolean hasUnit() {
169    return unit != null;
170  }
171
172  @Nullable
173  public Unit getUnit() {
174    return unit;
175  }
176
177  private void validate() {
178    if (name == null) {
179      throw new IllegalArgumentException("Missing required field: name is null");
180    }
181    String error = PrometheusNaming.validateMetricName(name);
182    if (error != null) {
183      throw new IllegalArgumentException(
184          "'"
185              + name
186              + "': Illegal metric name. "
187              + error
188              + " Call "
189              + PrometheusNaming.class.getSimpleName()
190              + ".sanitizeMetricName(name) to avoid this error.");
191    }
192    if (hasUnit()) {
193      if (!name.endsWith("_" + unit) && !name.endsWith("." + unit)) {
194        throw new IllegalArgumentException(
195            "'"
196                + name
197                + "': Illegal metric name. If the unit is non-null, "
198                + "the name must end with the unit: _"
199                + unit
200                + "."
201                + " Call "
202                + PrometheusNaming.class.getSimpleName()
203                + ".sanitizeMetricName(name, unit) to avoid this error.");
204      }
205    }
206  }
207
208  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);
215  }
216}