001package io.prometheus.metrics.model.snapshots;
002
003import java.util.ArrayList;
004import java.util.Arrays;
005import java.util.Collection;
006import java.util.Collections;
007import java.util.Iterator;
008import java.util.List;
009import java.util.stream.Stream;
010
011/** Immutable snapshot of a StateSet metric. */
012public final class StateSetSnapshot extends MetricSnapshot {
013
014  /**
015   * To create a new {@link StateSetSnapshot}, you can either call the constructor directly or use
016   * the builder with {@link StateSetSnapshot#builder()}.
017   *
018   * @param metadata See {@link MetricMetadata} for more naming conventions.
019   * @param data the constructor will create a sorted copy of the collection.
020   */
021  public StateSetSnapshot(MetricMetadata metadata, Collection<StateSetDataPointSnapshot> data) {
022    super(metadata, data);
023    validate();
024  }
025
026  private void validate() {
027    if (getMetadata().hasUnit()) {
028      throw new IllegalArgumentException("An state set metric cannot have a unit.");
029    }
030    for (StateSetDataPointSnapshot entry : getDataPoints()) {
031      if (entry.getLabels().contains(getMetadata().getPrometheusName())) {
032        throw new IllegalArgumentException(
033            "Label name " + getMetadata().getPrometheusName() + " is reserved.");
034      }
035    }
036  }
037
038  @SuppressWarnings("unchecked")
039  @Override
040  public List<StateSetDataPointSnapshot> getDataPoints() {
041    return (List<StateSetDataPointSnapshot>) dataPoints;
042  }
043
044  public static class StateSetDataPointSnapshot extends DataPointSnapshot
045      implements Iterable<State> {
046    private final String[] names;
047    private final boolean[] values;
048
049    /**
050     * To create a new {@link StateSetDataPointSnapshot}, you can either call the constructor
051     * directly or use the Builder with {@link StateSetDataPointSnapshot#builder()}.
052     *
053     * @param names state names. Must have at least 1 entry. The constructor will create a copy of
054     *     the array.
055     * @param values state values. Must have the same length as {@code names}. The constructor will
056     *     create a copy of the array.
057     * @param labels must not be null. Use {@link Labels#EMPTY} if there are no labels.
058     */
059    public StateSetDataPointSnapshot(String[] names, boolean[] values, Labels labels) {
060      this(names, values, labels, 0L);
061    }
062
063    /**
064     * Constructor with an additional scrape timestamp. This is only useful in rare cases as the
065     * scrape timestamp is usually set by the Prometheus server during scraping. Exceptions include
066     * mirroring metrics with given timestamps from other metric sources.
067     */
068    public StateSetDataPointSnapshot(
069        String[] names, boolean[] values, Labels labels, long scrapeTimestampMillis) {
070      super(labels, 0L, scrapeTimestampMillis);
071      if (names.length == 0) {
072        throw new IllegalArgumentException("StateSet must have at least one state.");
073      }
074      if (names.length != values.length) {
075        throw new IllegalArgumentException("names[] and values[] must have the same length");
076      }
077      String[] namesCopy = Arrays.copyOf(names, names.length);
078      boolean[] valuesCopy = Arrays.copyOf(values, names.length);
079      sort(namesCopy, valuesCopy);
080      this.names = namesCopy;
081      this.values = valuesCopy;
082      validate();
083    }
084
085    public int size() {
086      return names.length;
087    }
088
089    public String getName(int i) {
090      return names[i];
091    }
092
093    public boolean isTrue(int i) {
094      return values[i];
095    }
096
097    private void validate() {
098      for (int i = 0; i < names.length; i++) {
099        if (names[i].isEmpty()) {
100          throw new IllegalArgumentException("Empty string as state name");
101        }
102        if (i > 0 && names[i - 1].equals(names[i])) {
103          throw new IllegalArgumentException(names[i] + " duplicate state name");
104        }
105      }
106    }
107
108    private List<State> asList() {
109      List<State> result = new ArrayList<>(size());
110      for (int i = 0; i < names.length; i++) {
111        result.add(new State(names[i], values[i]));
112      }
113      return Collections.unmodifiableList(result);
114    }
115
116    @Override
117    public Iterator<State> iterator() {
118      return asList().iterator();
119    }
120
121    public Stream<State> stream() {
122      return asList().stream();
123    }
124
125    private static void sort(String[] names, boolean[] values) {
126      // Bubblesort
127      int n = names.length;
128      for (int i = 0; i < n - 1; i++) {
129        for (int j = 0; j < n - i - 1; j++) {
130          if (names[j].compareTo(names[j + 1]) > 0) {
131            swap(j, j + 1, names, values);
132          }
133        }
134      }
135    }
136
137    private static void swap(int i, int j, String[] names, boolean[] values) {
138      String tmpName = names[j];
139      names[j] = names[i];
140      names[i] = tmpName;
141      boolean tmpValue = values[j];
142      values[j] = values[i];
143      values[i] = tmpValue;
144    }
145
146    public static Builder builder() {
147      return new Builder();
148    }
149
150    public static class Builder extends DataPointSnapshot.Builder<Builder> {
151
152      private final List<String> names = new ArrayList<>();
153      private final List<Boolean> values = new ArrayList<>();
154
155      private Builder() {}
156
157      /** Add a state. Call multiple times to add multiple states. */
158      public Builder state(String name, boolean value) {
159        names.add(name);
160        values.add(value);
161        return this;
162      }
163
164      @Override
165      protected Builder self() {
166        return this;
167      }
168
169      public StateSetDataPointSnapshot build() {
170        boolean[] valuesArray = new boolean[values.size()];
171        for (int i = 0; i < values.size(); i++) {
172          valuesArray[i] = values.get(i);
173        }
174        return new StateSetDataPointSnapshot(
175            names.toArray(new String[] {}), valuesArray, labels, scrapeTimestampMillis);
176      }
177    }
178  }
179
180  public static class State {
181    private final String name;
182    private final boolean value;
183
184    private State(String name, boolean value) {
185      this.name = name;
186      this.value = value;
187    }
188
189    public String getName() {
190      return name;
191    }
192
193    public boolean isTrue() {
194      return value;
195    }
196  }
197
198  public static Builder builder() {
199    return new Builder();
200  }
201
202  public static class Builder extends MetricSnapshot.Builder<Builder> {
203
204    private final List<StateSetDataPointSnapshot> dataPoints = new ArrayList<>();
205
206    private Builder() {}
207
208    /** Add a data point. Call multiple times to add multiple data points. */
209    public Builder dataPoint(StateSetDataPointSnapshot dataPoint) {
210      dataPoints.add(dataPoint);
211      return this;
212    }
213
214    @Override
215    public Builder unit(Unit unit) {
216      throw new IllegalArgumentException("StateSet metric cannot have a unit.");
217    }
218
219    @Override
220    public StateSetSnapshot build() {
221      return new StateSetSnapshot(buildMetadata(), dataPoints);
222    }
223
224    @Override
225    protected Builder self() {
226      return this;
227    }
228  }
229}