001package io.prometheus.metrics.model.registry;
002
003import static java.util.Collections.emptySet;
004
005import io.prometheus.metrics.model.snapshots.MetricMetadata;
006import io.prometheus.metrics.model.snapshots.MetricSnapshot;
007import io.prometheus.metrics.model.snapshots.MetricSnapshots;
008import io.prometheus.metrics.model.snapshots.Unit;
009import java.util.ArrayList;
010import java.util.Collections;
011import java.util.HashSet;
012import java.util.List;
013import java.util.Objects;
014import java.util.Set;
015import java.util.concurrent.ConcurrentHashMap;
016import java.util.function.Predicate;
017import javax.annotation.Nullable;
018
019public class PrometheusRegistry {
020
021  public static final PrometheusRegistry defaultRegistry = new PrometheusRegistry();
022
023  private final Set<Collector> collectors = ConcurrentHashMap.newKeySet();
024  private final Set<MultiCollector> multiCollectors = ConcurrentHashMap.newKeySet();
025  private final ConcurrentHashMap<String, RegistrationInfo> registered = new ConcurrentHashMap<>();
026  private final ConcurrentHashMap<Collector, CollectorRegistration> collectorMetadata =
027      new ConcurrentHashMap<>();
028  private final ConcurrentHashMap<MultiCollector, List<MultiCollectorRegistration>>
029      multiCollectorMetadata = new ConcurrentHashMap<>();
030
031  /** Stores the registration details for a Collector at registration time. */
032  private static class CollectorRegistration {
033    final String prometheusName;
034    final Set<String> labelNames;
035
036    CollectorRegistration(String prometheusName, @Nullable Set<String> labelNames) {
037      this.prometheusName = prometheusName;
038      this.labelNames = immutableLabelNames(labelNames);
039    }
040  }
041
042  /**
043   * Stores the registration details for a single metric within a MultiCollector. A MultiCollector
044   * can produce multiple metrics, so we need one of these per metric name.
045   */
046  private static class MultiCollectorRegistration {
047    final String prometheusName;
048    final Set<String> labelNames;
049
050    MultiCollectorRegistration(String prometheusName, @Nullable Set<String> labelNames) {
051      this.prometheusName = prometheusName;
052      this.labelNames = immutableLabelNames(labelNames);
053    }
054  }
055
056  /**
057   * Tracks registration information for each metric name to enable validation of type consistency,
058   * label schema uniqueness, and help/unit consistency.
059   */
060  private static class RegistrationInfo {
061    private final MetricType type;
062    private final Set<Set<String>> labelSchemas;
063    @Nullable private String help;
064    @Nullable private Unit unit;
065
066    private RegistrationInfo(
067        MetricType type,
068        Set<Set<String>> labelSchemas,
069        @Nullable String help,
070        @Nullable Unit unit) {
071      this.type = type;
072      this.labelSchemas = labelSchemas;
073      this.help = help;
074      this.unit = unit;
075    }
076
077    static RegistrationInfo of(
078        MetricType type,
079        @Nullable Set<String> labelNames,
080        @Nullable String help,
081        @Nullable Unit unit) {
082      Set<Set<String>> labelSchemas = ConcurrentHashMap.newKeySet();
083      Set<String> normalized =
084          (labelNames == null || labelNames.isEmpty()) ? emptySet() : labelNames;
085      labelSchemas.add(normalized);
086      return new RegistrationInfo(type, labelSchemas, help, unit);
087    }
088
089    /**
090     * Validates that the given help and unit are exactly equal to this registration. Throws if
091     * values differ, including when one is null and the other is non-null. This ensures consistent
092     * metadata across all collectors sharing the same metric name.
093     */
094    void validateMetadata(@Nullable String newHelp, @Nullable Unit newUnit) {
095      if (!Objects.equals(help, newHelp)) {
096        throw new IllegalArgumentException(
097            "Conflicting help strings. Existing: \"" + help + "\", new: \"" + newHelp + "\"");
098      }
099      if (!Objects.equals(unit, newUnit)) {
100        throw new IllegalArgumentException(
101            "Conflicting unit. Existing: " + unit + ", new: " + newUnit);
102      }
103    }
104
105    /**
106     * Adds a label schema to this registration.
107     *
108     * @param labelNames the label names to add (null or empty sets are normalized to empty set)
109     * @return true if the schema was added (new), false if it already existed
110     */
111    boolean addLabelSet(@Nullable Set<String> labelNames) {
112      Set<String> normalized =
113          (labelNames == null || labelNames.isEmpty()) ? emptySet() : labelNames;
114      return labelSchemas.add(normalized);
115    }
116
117    /**
118     * Removes a label schema from this registration.
119     *
120     * @param labelNames the label names to remove (null or empty sets are normalized to empty set)
121     */
122    void removeLabelSet(@Nullable Set<String> labelNames) {
123      Set<String> normalized =
124          (labelNames == null || labelNames.isEmpty()) ? emptySet() : labelNames;
125      labelSchemas.remove(normalized);
126    }
127
128    /** Returns true if all label schemas have been unregistered. */
129    boolean isEmpty() {
130      return labelSchemas.isEmpty();
131    }
132
133    MetricType getType() {
134      return type;
135    }
136  }
137
138  /**
139   * Returns an immutable set of label names for storage. Defends against mutation of the set
140   * returned by {@code Collector.getLabelNames()} after registration, which would break duplicate
141   * detection and unregistration.
142   */
143  private static Set<String> immutableLabelNames(@Nullable Set<String> labelNames) {
144    if (labelNames == null || labelNames.isEmpty()) {
145      return emptySet();
146    }
147    return Collections.unmodifiableSet(new HashSet<>(labelNames));
148  }
149
150  /**
151   * Validates the registration of a metric with the given parameters. Ensures type consistency,
152   * label schema uniqueness, and help/unit consistency.
153   */
154  private void validateRegistration(
155      String prometheusName,
156      MetricType metricType,
157      Set<String> normalizedLabels,
158      @Nullable String help,
159      @Nullable Unit unit) {
160    final MetricType type = metricType;
161    final Set<String> names = normalizedLabels;
162    final String helpForValidation = help;
163    final Unit unitForValidation = unit;
164    registered.compute(
165        prometheusName,
166        (n, existingInfo) -> {
167          if (existingInfo == null) {
168            return RegistrationInfo.of(type, names, helpForValidation, unitForValidation);
169          } else {
170            if (existingInfo.getType() != type) {
171              throw new IllegalArgumentException(
172                  prometheusName
173                      + ": Conflicting metric types. Existing: "
174                      + existingInfo.getType()
175                      + ", new: "
176                      + type);
177            }
178            // Check label set first; only mutate help/unit after validation passes.
179            if (!existingInfo.addLabelSet(names)) {
180              throw new IllegalArgumentException(
181                  prometheusName + ": duplicate metric name with identical label schema " + names);
182            }
183            // Roll back label schema if metadata validation fails
184            try {
185              existingInfo.validateMetadata(helpForValidation, unitForValidation);
186            } catch (IllegalArgumentException e) {
187              existingInfo.removeLabelSet(names);
188              throw e;
189            }
190            return existingInfo;
191          }
192        });
193  }
194
195  public void register(Collector collector) {
196    if (!collectors.add(collector)) {
197      throw new IllegalArgumentException("Collector instance is already registered");
198    }
199    try {
200      String prometheusName = collector.getPrometheusName();
201      MetricType metricType = collector.getMetricType();
202      Set<String> normalizedLabels = immutableLabelNames(collector.getLabelNames());
203      MetricMetadata metadata = collector.getMetadata();
204      String help = metadata != null ? metadata.getHelp() : null;
205      Unit unit = metadata != null ? metadata.getUnit() : null;
206
207      // Only perform validation if collector provides sufficient metadata.
208      // Collectors that don't implement getPrometheusName()/getMetricType() will skip validation.
209      if (prometheusName != null && metricType != null) {
210        validateRegistration(prometheusName, metricType, normalizedLabels, help, unit);
211        collectorMetadata.put(
212            collector, new CollectorRegistration(prometheusName, normalizedLabels));
213      }
214      // Catch RuntimeException broadly because collector methods (getPrometheusName, getMetricType,
215      // etc.) are user-implemented and could throw any RuntimeException. Ensures cleanup on
216      // failure.
217    } catch (RuntimeException e) {
218      collectors.remove(collector);
219      CollectorRegistration reg = collectorMetadata.remove(collector);
220      if (reg != null && reg.prometheusName != null) {
221        unregisterLabelSchema(reg.prometheusName, reg.labelNames);
222      }
223      throw e;
224    }
225  }
226
227  public void register(MultiCollector collector) {
228    if (!multiCollectors.add(collector)) {
229      throw new IllegalArgumentException("MultiCollector instance is already registered");
230    }
231    List<String> prometheusNamesList = collector.getPrometheusNames();
232    List<MultiCollectorRegistration> registrations = new ArrayList<>();
233
234    try {
235      for (String prometheusName : prometheusNamesList) {
236        MetricType metricType = collector.getMetricType(prometheusName);
237        Set<String> normalizedLabels = immutableLabelNames(collector.getLabelNames(prometheusName));
238        MetricMetadata metadata = collector.getMetadata(prometheusName);
239        String help = metadata != null ? metadata.getHelp() : null;
240        Unit unit = metadata != null ? metadata.getUnit() : null;
241
242        if (metricType != null) {
243          validateRegistration(prometheusName, metricType, normalizedLabels, help, unit);
244          registrations.add(new MultiCollectorRegistration(prometheusName, normalizedLabels));
245        }
246      }
247
248      multiCollectorMetadata.put(collector, registrations);
249      // Catch RuntimeException broadly because collector methods (getPrometheusNames,
250      // getMetricType, etc.) are user-implemented and could throw any RuntimeException.
251      // Ensures cleanup on failure.
252    } catch (RuntimeException e) {
253      multiCollectors.remove(collector);
254      for (MultiCollectorRegistration registration : registrations) {
255        unregisterLabelSchema(registration.prometheusName, registration.labelNames);
256      }
257      throw e;
258    }
259  }
260
261  public void unregister(Collector collector) {
262    collectors.remove(collector);
263
264    CollectorRegistration registration = collectorMetadata.remove(collector);
265    if (registration != null && registration.prometheusName != null) {
266      unregisterLabelSchema(registration.prometheusName, registration.labelNames);
267    }
268  }
269
270  public void unregister(MultiCollector collector) {
271    multiCollectors.remove(collector);
272
273    List<MultiCollectorRegistration> registrations = multiCollectorMetadata.remove(collector);
274    if (registrations != null) {
275      for (MultiCollectorRegistration registration : registrations) {
276        unregisterLabelSchema(registration.prometheusName, registration.labelNames);
277      }
278    }
279  }
280
281  /**
282   * Removes the label schema for the given metric name. If no label schemas remain for that name,
283   * removes the metric name entirely from the registry.
284   */
285  private void unregisterLabelSchema(String prometheusName, Set<String> labelNames) {
286    registered.computeIfPresent(
287        prometheusName,
288        (name, info) -> {
289          info.removeLabelSet(labelNames);
290          if (info.isEmpty()) {
291            return null;
292          }
293          return info;
294        });
295  }
296
297  public void clear() {
298    collectors.clear();
299    multiCollectors.clear();
300    registered.clear();
301    collectorMetadata.clear();
302    multiCollectorMetadata.clear();
303  }
304
305  public MetricSnapshots scrape() {
306    return scrape((PrometheusScrapeRequest) null);
307  }
308
309  public MetricSnapshots scrape(@Nullable PrometheusScrapeRequest scrapeRequest) {
310    List<MetricSnapshot> allSnapshots = new ArrayList<>();
311    for (Collector collector : collectors) {
312      MetricSnapshot snapshot =
313          scrapeRequest == null ? collector.collect() : collector.collect(scrapeRequest);
314      if (snapshot != null) {
315        allSnapshots.add(snapshot);
316      }
317    }
318    for (MultiCollector collector : multiCollectors) {
319      MetricSnapshots snapshots =
320          scrapeRequest == null ? collector.collect() : collector.collect(scrapeRequest);
321      for (MetricSnapshot snapshot : snapshots) {
322        allSnapshots.add(snapshot);
323      }
324    }
325
326    MetricSnapshots.Builder result = MetricSnapshots.builder();
327    for (MetricSnapshot snapshot : allSnapshots) {
328      result.metricSnapshot(snapshot);
329    }
330    return result.build();
331  }
332
333  public MetricSnapshots scrape(Predicate<String> includedNames) {
334    if (includedNames == null) {
335      return scrape();
336    }
337    return scrape(includedNames, null);
338  }
339
340  public MetricSnapshots scrape(
341      Predicate<String> includedNames, @Nullable PrometheusScrapeRequest scrapeRequest) {
342    if (includedNames == null) {
343      return scrape(scrapeRequest);
344    }
345    List<MetricSnapshot> allSnapshots = new ArrayList<>();
346    for (Collector collector : collectors) {
347      String prometheusName = collector.getPrometheusName();
348      // prometheusName == null means the name is unknown, and we have to scrape to learn the name.
349      // prometheusName != null means we can skip the scrape if the name is excluded.
350      if (prometheusName == null || includedNames.test(prometheusName)) {
351        MetricSnapshot snapshot =
352            scrapeRequest == null
353                ? collector.collect(includedNames)
354                : collector.collect(includedNames, scrapeRequest);
355        if (snapshot != null) {
356          allSnapshots.add(snapshot);
357        }
358      }
359    }
360    for (MultiCollector collector : multiCollectors) {
361      List<String> prometheusNames = collector.getPrometheusNames();
362      // empty prometheusNames means the names are unknown, and we have to scrape to learn the
363      // names.
364      // non-empty prometheusNames means we can exclude the collector if all names are excluded by
365      // the filter.
366      boolean excluded = !prometheusNames.isEmpty();
367      for (String prometheusName : prometheusNames) {
368        if (includedNames.test(prometheusName)) {
369          excluded = false;
370          break;
371        }
372      }
373      if (!excluded) {
374        MetricSnapshots snapshots =
375            scrapeRequest == null
376                ? collector.collect(includedNames)
377                : collector.collect(includedNames, scrapeRequest);
378        for (MetricSnapshot snapshot : snapshots) {
379          if (snapshot != null) {
380            allSnapshots.add(snapshot);
381          }
382        }
383      }
384    }
385
386    MetricSnapshots.Builder result = MetricSnapshots.builder();
387    for (MetricSnapshot snapshot : allSnapshots) {
388      result.metricSnapshot(snapshot);
389    }
390    return result.build();
391  }
392}