001package io.prometheus.metrics.model.snapshots;
002
003import static io.prometheus.metrics.model.snapshots.PrometheusNaming.prometheusName;
004import static java.util.Collections.unmodifiableList;
005import static java.util.Comparator.comparing;
006
007import io.prometheus.metrics.annotations.StableApi;
008import java.util.ArrayList;
009import java.util.Arrays;
010import java.util.Collection;
011import java.util.HashSet;
012import java.util.Iterator;
013import java.util.List;
014import java.util.Set;
015import java.util.stream.Stream;
016
017/** Immutable list of metric snapshots. */
018@StableApi
019public class MetricSnapshots implements Iterable<MetricSnapshot> {
020
021  private final List<MetricSnapshot> snapshots;
022
023  /** See {@link #MetricSnapshots(Collection)} */
024  public MetricSnapshots(MetricSnapshot... snapshots) {
025    this(Arrays.asList(snapshots));
026  }
027
028  /**
029   * To create MetricSnapshots, you can either call the constructor directly or use {@link
030   * #builder()}.
031   *
032   * @param snapshots the constructor creates a sorted copy of snapshots.
033   * @throws IllegalArgumentException if snapshots contain conflicting metric types (same name but
034   *     different metric types like Counter vs Gauge), or if two HistogramSnapshots share a name
035   *     but differ in gauge histogram vs classic histogram.
036   */
037  public MetricSnapshots(Collection<MetricSnapshot> snapshots) {
038    List<MetricSnapshot> list = new ArrayList<>(snapshots);
039    // Sort by prometheus name so snapshots that share a prometheus name (i.e. the same exposed
040    // family) are adjacent. TextFormatUtil.mergeDuplicates depends on this ordering to detect
041    // duplicates in a single linear pass; if this sort key changes, update that fast path too.
042    list.sort(comparing(s -> s.getMetadata().getPrometheusName()));
043
044    // Validate no conflicting metric types
045    for (int i = 0; i < list.size() - 1; i++) {
046      String name1 = list.get(i).getMetadata().getPrometheusName();
047      String name2 = list.get(i + 1).getMetadata().getPrometheusName();
048
049      if (name1.equals(name2)) {
050        MetricSnapshot s1 = list.get(i);
051        MetricSnapshot s2 = list.get(i + 1);
052        Class<?> type1 = s1.getClass();
053        Class<?> type2 = s2.getClass();
054
055        if (!type1.equals(type2)) {
056          throw new IllegalArgumentException(
057              name1
058                  + ": conflicting metric types: "
059                  + type1.getSimpleName()
060                  + " and "
061                  + type2.getSimpleName());
062        }
063
064        // HistogramSnapshot: gauge histogram vs classic histogram are semantically different
065        if (s1 instanceof HistogramSnapshot) {
066          HistogramSnapshot h1 = (HistogramSnapshot) s1;
067          HistogramSnapshot h2 = (HistogramSnapshot) s2;
068          if (h1.isGaugeHistogram() != h2.isGaugeHistogram()) {
069            throw new IllegalArgumentException(
070                name1 + ": conflicting histogram types: gauge histogram and classic histogram");
071          }
072        }
073      }
074    }
075
076    this.snapshots = unmodifiableList(list);
077  }
078
079  public static MetricSnapshots of(MetricSnapshot... snapshots) {
080    return new MetricSnapshots(snapshots);
081  }
082
083  @Override
084  public Iterator<MetricSnapshot> iterator() {
085    return snapshots.iterator();
086  }
087
088  public int size() {
089    return snapshots.size();
090  }
091
092  public MetricSnapshot get(int i) {
093    return snapshots.get(i);
094  }
095
096  public Stream<MetricSnapshot> stream() {
097    return snapshots.stream();
098  }
099
100  public static Builder builder() {
101    return new Builder();
102  }
103
104  public static class Builder {
105
106    private final List<MetricSnapshot> snapshots = new ArrayList<>();
107    private final Set<String> prometheusNames = new HashSet<>();
108
109    private Builder() {}
110
111    public boolean containsMetricName(String name) {
112      if (name == null) {
113        return false;
114      }
115      String prometheusName = prometheusName(name);
116      return prometheusNames.contains(prometheusName);
117    }
118
119    /** Add a metric snapshot. Call multiple times to add multiple metric snapshots. */
120    public Builder metricSnapshot(MetricSnapshot snapshot) {
121      snapshots.add(snapshot);
122      prometheusNames.add(snapshot.getMetadata().getPrometheusName());
123      return this;
124    }
125
126    public MetricSnapshots build() {
127      return new MetricSnapshots(snapshots);
128    }
129  }
130}