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