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/**
012 * Immutable snapshot of a StateSet metric.
013 */
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        super(metadata, data);
025        validate();
026    }
027
028    private void validate() {
029        if (getMetadata().hasUnit()) {
030            throw new IllegalArgumentException("An state set metric cannot have a unit.");
031        }
032        for (StateSetDataPointSnapshot entry : getDataPoints()) {
033            if (entry.getLabels().contains(getMetadata().getPrometheusName())) {
034                throw new IllegalArgumentException("Label name " + getMetadata().getPrometheusName() + " is reserved.");
035            }
036        }
037    }
038
039    @Override
040    public List<StateSetDataPointSnapshot> getDataPoints() {
041        return (List<StateSetDataPointSnapshot>) dataPoints;
042    }
043
044
045    public static class StateSetDataPointSnapshot extends DataPointSnapshot 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 directly or use the
051         * Builder with {@link StateSetDataPointSnapshot#builder()}.
052         *
053         * @param names  state names. Must have at least 1 entry.
054         *               The constructor will create a copy of the array.
055         * @param values state values. Must have the same length as {@code names}.
056         *               The constructor will 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.
065         * This is only useful in rare cases as the scrape timestamp is usually set by the Prometheus server
066         * during scraping. Exceptions include mirroring metrics with given timestamps from other metric sources.
067         */
068        public StateSetDataPointSnapshot(String[] names, boolean[] values, Labels labels, long scrapeTimestampMillis) {
069            super(labels, 0L, scrapeTimestampMillis);
070            if (names.length == 0) {
071                throw new IllegalArgumentException("StateSet must have at least one state.");
072            }
073            if (names.length != values.length) {
074                throw new IllegalArgumentException("names[] and values[] must have the same length");
075            }
076            String[] namesCopy = Arrays.copyOf(names, names.length);
077            boolean[] valuesCopy = Arrays.copyOf(values, names.length);
078            sort(namesCopy, valuesCopy);
079            this.names = namesCopy;
080            this.values = valuesCopy;
081            validate();
082        }
083
084        public int size() {
085            return names.length;
086        }
087
088        public String getName(int i) {
089            return names[i];
090        }
091
092        public boolean isTrue(int i) {
093            return values[i];
094        }
095
096        private void validate() {
097            for (int i = 0; i < names.length; i++) {
098                if (names[i].length() == 0) {
099                    throw new IllegalArgumentException("Empty string as state name");
100                }
101                if (i > 0 && names[i - 1].equals(names[i])) {
102                    throw new IllegalArgumentException(names[i] + " duplicate state name");
103                }
104            }
105        }
106
107        private List<State> asList() {
108            List<State> result = new ArrayList<>(size());
109            for (int i = 0; i < names.length; i++) {
110                result.add(new State(names[i], values[i]));
111            }
112            return Collections.unmodifiableList(result);
113        }
114
115        @Override
116        public Iterator<State> iterator() {
117            return asList().iterator();
118        }
119
120        public Stream<State> stream() {
121            return asList().stream();
122        }
123
124        private static void sort(String[] names, boolean[] values) {
125            // Bubblesort
126            int n = names.length;
127            for (int i = 0; i < n - 1; i++) {
128                for (int j = 0; j < n - i - 1; j++) {
129                    if (names[j].compareTo(names[j + 1]) > 0) {
130                        swap(j, j + 1, names, values);
131                    }
132                }
133            }
134        }
135
136        private static void swap(int i, int j, String[] names, boolean[] values) {
137            String tmpName = names[j];
138            names[j] = names[i];
139            names[i] = tmpName;
140            boolean tmpValue = values[j];
141            values[j] = values[i];
142            values[i] = tmpValue;
143        }
144
145        public static Builder builder() {
146            return new Builder();
147        }
148
149        public static class Builder extends DataPointSnapshot.Builder<Builder> {
150
151            private final ArrayList<String> names = new ArrayList<>();
152            private final ArrayList<Boolean> values = new ArrayList<>();
153
154            private Builder() {}
155
156            /**
157             * Add a state. Call multple times to add multiple states.
158             */
159            public Builder state(String name, boolean value) {
160                names.add(name);
161                values.add(value);
162                return this;
163            }
164
165            @Override
166            protected Builder self() {
167                return this;
168            }
169
170            public StateSetDataPointSnapshot build() {
171                boolean[] valuesArray = new boolean[values.size()];
172                for (int i = 0; i < values.size(); i++) {
173                    valuesArray[i] = values.get(i);
174                }
175                return new StateSetDataPointSnapshot(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
209        /**
210         * Add a data point. Call multiple times to add multiple data points.
211         */
212        public Builder dataPoint(StateSetDataPointSnapshot dataPoint) {
213            dataPoints.add(dataPoint);
214            return this;
215        }
216
217        @Override
218        public Builder unit(Unit unit) {
219            throw new IllegalArgumentException("StateSet metric cannot have a unit.");
220        }
221
222        @Override
223        public StateSetSnapshot build() {
224            return new StateSetSnapshot(buildMetadata(), dataPoints);
225        }
226
227        @Override
228        protected Builder self() {
229            return this;
230        }
231    }
232}