001package io.prometheus.metrics.core.metrics;
002
003import io.prometheus.metrics.annotations.StableApi;
004import io.prometheus.metrics.config.PrometheusProperties;
005import io.prometheus.metrics.model.registry.MetricType;
006import io.prometheus.metrics.model.snapshots.Label;
007import io.prometheus.metrics.model.snapshots.Labels;
008import io.prometheus.metrics.model.snapshots.MetricFamilyDescriptor;
009import io.prometheus.metrics.model.snapshots.MetricMetadata;
010import io.prometheus.metrics.model.snapshots.PrometheusNaming;
011import io.prometheus.metrics.model.snapshots.Unit;
012import java.util.Arrays;
013import java.util.HashSet;
014import java.util.List;
015import java.util.Set;
016import javax.annotation.Nullable;
017
018/**
019 * Almost all metrics have fixed metadata, i.e. the metric name is known when the metric is created.
020 *
021 * <p>An exception would be a metric that is a bridge to a 3rd party metric library, where the
022 * metric name has to be retrieved from the 3rd party metric library at scrape time.
023 */
024@StableApi
025public abstract class MetricWithFixedMetadata extends Metric {
026
027  protected final MetricMetadata metadata;
028  protected final String[] labelNames;
029
030  protected MetricWithFixedMetadata(Builder<?, ?> builder) {
031    super(builder);
032    String name = makeName(builder.name, builder.unit);
033    if (builder.originalName == null) {
034      throw new IllegalArgumentException("Missing required field: name is null");
035    }
036    String originalName = builder.originalName;
037    String expositionBaseName = makeExpositionBaseName(originalName, builder.unit);
038    this.metadata =
039        MetricMetadata.builder()
040            .name(name)
041            .expositionBaseName(expositionBaseName)
042            .originalName(originalName)
043            .help(builder.help)
044            .unit(builder.unit)
045            .build();
046    this.labelNames = Arrays.copyOf(builder.labelNames, builder.labelNames.length);
047  }
048
049  @Override
050  @Nullable
051  @SuppressWarnings("deprecation")
052  public MetricFamilyDescriptor getMetricFamilyDescriptor() {
053    MetricType metricType = getMetricType();
054    if (metricType == null) {
055      return null;
056    }
057    return MetricFamilyDescriptor.of(metricType, metadata, getPrometheusLabels());
058  }
059
060  /**
061   * @deprecated Use {@link #getMetricFamilyDescriptor()} instead.
062   */
063  @Override
064  @Deprecated
065  @SuppressWarnings("InlineMeSuggester")
066  public MetricMetadata getMetadata() {
067    return metadata;
068  }
069
070  private String makeName(@Nullable String name, @Nullable Unit unit) {
071    if (name == null) {
072      throw new IllegalArgumentException("Missing required field: name is null");
073    }
074    if (unit != null) {
075      if (!name.endsWith("_" + unit) && !name.endsWith("." + unit)) {
076        name += "_" + unit;
077      }
078    }
079    return name;
080  }
081
082  private String makeExpositionBaseName(@Nullable String expositionBaseName, @Nullable Unit unit) {
083    if (expositionBaseName == null) {
084      throw new IllegalArgumentException("Missing required field: name is null");
085    }
086    if (unit != null) {
087      if (!expositionBaseName.endsWith("_" + unit) && !expositionBaseName.endsWith("." + unit)) {
088        expositionBaseName += "_" + unit;
089      }
090    }
091    return expositionBaseName;
092  }
093
094  /**
095   * @deprecated Use {@link #getMetricFamilyDescriptor()} instead.
096   */
097  @Override
098  @Deprecated
099  @SuppressWarnings("InlineMeSuggester")
100  public String getPrometheusName() {
101    return metadata.getPrometheusName();
102  }
103
104  /**
105   * @deprecated Use {@link #getMetricFamilyDescriptor()} instead.
106   */
107  @Override
108  @Deprecated
109  @SuppressWarnings("InlineMeSuggester")
110  public Set<String> getLabelNames() {
111    return getPrometheusLabels();
112  }
113
114  private Set<String> getPrometheusLabels() {
115    Set<String> names = new HashSet<>();
116    for (String labelName : labelNames) {
117      names.add(PrometheusNaming.prometheusName(labelName));
118    }
119    for (Label label : constLabels) {
120      names.add(PrometheusNaming.prometheusName(label.getName()));
121    }
122    return names;
123  }
124
125  public abstract static class Builder<B extends Builder<B, M>, M extends MetricWithFixedMetadata>
126      extends Metric.Builder<B, M> {
127
128    @Nullable private String name;
129    @Nullable private String originalName;
130    @Nullable private Unit unit;
131    @Nullable private String help;
132    private String[] labelNames = new String[0];
133
134    protected Builder(List<String> illegalLabelNames, PrometheusProperties properties) {
135      super(illegalLabelNames, properties);
136    }
137
138    public B name(String name) {
139      String error = PrometheusNaming.validateMetricName(name);
140      if (error != null) {
141        throw new IllegalArgumentException("'" + name + "': Illegal metric name: " + error);
142      }
143      this.name = name;
144      this.originalName = name;
145      return self();
146    }
147
148    /**
149     * Set the metric name and original name separately. Used by Counter and Info builders which
150     * strip type suffixes from the name but preserve the original for exposition.
151     */
152    protected B nameWithOriginal(String name, String originalName) {
153      String error = PrometheusNaming.validateMetricName(name);
154      if (error != null) {
155        throw new IllegalArgumentException("'" + name + "': Illegal metric name: " + error);
156      }
157      error = PrometheusNaming.validateMetricName(originalName);
158      if (error != null) {
159        throw new IllegalArgumentException("'" + originalName + "': Illegal metric name: " + error);
160      }
161      this.name = name;
162      this.originalName = originalName;
163      return self();
164    }
165
166    public B unit(@Nullable Unit unit) {
167      this.unit = unit;
168      return self();
169    }
170
171    public B help(String help) {
172      this.help = help;
173      return self();
174    }
175
176    public B labelNames(String... labelNames) {
177      for (String labelName : labelNames) {
178        if (!PrometheusNaming.isValidLabelName(labelName)) {
179          throw new IllegalArgumentException(labelName + ": illegal label name");
180        }
181        if (illegalLabelNames.contains(labelName)) {
182          throw new IllegalArgumentException(
183              labelName + ": illegal label name for this metric type");
184        }
185        if (constLabels.contains(labelName)) {
186          throw new IllegalArgumentException(labelName + ": duplicate label name");
187        }
188      }
189      this.labelNames = labelNames;
190      return self();
191    }
192
193    @Override
194    public B constLabels(Labels constLabels) {
195      for (String labelName : labelNames) {
196        if (constLabels.contains(labelName)) { // Labels.contains() treats dots like underscores
197          throw new IllegalArgumentException(labelName + ": duplicate label name");
198        }
199      }
200      return super.constLabels(constLabels);
201    }
202
203    @Override
204    public abstract M build();
205
206    @Override
207    protected abstract B self();
208  }
209}