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