001package io.prometheus.metrics.core.metrics;
002
003import io.prometheus.metrics.config.MetricsProperties;
004import io.prometheus.metrics.config.PrometheusProperties;
005import io.prometheus.metrics.model.snapshots.Labels;
006import io.prometheus.metrics.model.snapshots.StateSetSnapshot;
007import io.prometheus.metrics.core.datapoints.StateSetDataPoint;
008
009import java.util.ArrayList;
010import java.util.Collections;
011import java.util.List;
012import java.util.stream.Stream;
013
014import static io.prometheus.metrics.model.snapshots.PrometheusNaming.prometheusName;
015
016/**
017 * StateSet metric. Example:
018 * <pre>{@code
019 * public enum Feature {
020 *
021 *     FEATURE_1("feature1"),
022 *     FEATURE_2("feature2");
023 *
024 *     private final String name;
025 *
026 *     Feature(String name) {
027 *         this.name = name;
028 *     }
029 *
030 *     // Override
031 *     public String toString() {
032 *         return name;
033 *     }
034 * }
035 *
036 * public static void main(String[] args) {
037 *
038 *     StateSet stateSet = StateSet.builder()
039 *             .name("feature_flags")
040 *             .help("Feature flags")
041 *             .labelNames("env")
042 *             .states(Feature.class)
043 *             .register();
044 *
045 *     stateSet.labelValues("dev").setFalse(FEATURE_1);
046 *     stateSet.labelValues("dev").setTrue(FEATURE_2);
047 * }
048 * }</pre>
049 * The example above shows how to use a StateSet with an enum.
050 * You don't have to use enum, you can use regular strings as well.
051 */
052public class StateSet extends StatefulMetric<StateSetDataPoint, StateSet.DataPoint> implements StateSetDataPoint {
053
054    private final boolean exemplarsEnabled;
055    private final String[] names;
056
057    private StateSet(Builder builder, PrometheusProperties prometheusProperties) {
058        super(builder);
059        MetricsProperties[] properties = getMetricProperties(builder, prometheusProperties);
060        exemplarsEnabled = getConfigProperty(properties, MetricsProperties::getExemplarsEnabled);
061        this.names = builder.names; // builder.names is already a validated copy
062        for (String name : names) {
063            if (this.getMetadata().getPrometheusName().equals(prometheusName(name))) {
064                throw new IllegalArgumentException("Label name " + name + " is illegal (can't use the metric name as label name in state set metrics)");
065            }
066        }
067    }
068
069    /**
070     * {@inheritDoc}
071     */
072    @Override
073    public StateSetSnapshot collect() {
074        return (StateSetSnapshot) super.collect();
075    }
076
077    /**
078     * {@inheritDoc}
079     */
080    @Override
081    public void setTrue(String state) {
082        getNoLabels().setTrue(state);
083    }
084
085    /**
086     * {@inheritDoc}
087     */
088    @Override
089    public void setFalse(String state) {
090        getNoLabels().setFalse(state);
091    }
092
093    @Override
094    protected StateSetSnapshot collect(List<Labels> labels, List<DataPoint> metricDataList) {
095        List<StateSetSnapshot.StateSetDataPointSnapshot> data = new ArrayList<>(labels.size());
096        for (int i = 0; i < labels.size(); i++) {
097            data.add(new StateSetSnapshot.StateSetDataPointSnapshot(names, metricDataList.get(i).values, labels.get(i)));
098        }
099        return new StateSetSnapshot(getMetadata(), data);
100    }
101
102    @Override
103    protected DataPoint newDataPoint() {
104        return new DataPoint();
105    }
106
107    @Override
108    protected boolean isExemplarsEnabled() {
109        return exemplarsEnabled;
110    }
111
112    class DataPoint implements StateSetDataPoint {
113
114        private final boolean[] values = new boolean[names.length];
115
116        private DataPoint() {
117        }
118
119        /**
120         * {@inheritDoc}
121         */
122        @Override
123        public void setTrue(String state) {
124            set(state, true);
125        }
126
127        /**
128         * {@inheritDoc}
129         */
130        @Override
131        public void setFalse(String state) {
132            set(state, false);
133        }
134
135        private void set(String name, boolean value) {
136            for (int i = 0; i < names.length; i++) {
137                if (names[i].equals(name)) {
138                    values[i] = value;
139                    return;
140                }
141            }
142            throw new IllegalArgumentException(name + ": unknown state");
143        }
144    }
145
146    public static Builder builder() {
147        return new Builder(PrometheusProperties.get());
148    }
149
150    public static Builder builder(PrometheusProperties config) {
151        return new Builder(config);
152    }
153
154    public static class Builder extends StatefulMetric.Builder<Builder, StateSet> {
155
156        private String[] names;
157
158        private Builder(PrometheusProperties config) {
159            super(Collections.emptyList(), config);
160        }
161
162        /**
163         * Declare the states that should be represented by this StateSet.
164         */
165        public Builder states(Class<? extends Enum<?>> enumClass) {
166            return states(Stream.of(enumClass.getEnumConstants()).map(Enum::toString).toArray(String[]::new));
167        }
168
169        /**
170         * Declare the states that should be represented by this StateSet.
171         */
172        public Builder states(String... stateNames) {
173            if (stateNames.length == 0) {
174                throw new IllegalArgumentException("states cannot be empty");
175            }
176            this.names = Stream.of(stateNames)
177                    .distinct()
178                    .sorted()
179                    .toArray(String[]::new);
180            return this;
181        }
182
183        @Override
184        public StateSet build() {
185            if (names == null) {
186                throw new IllegalStateException("State names are required when building a StateSet.");
187            }
188            return new StateSet(this, properties);
189        }
190
191        @Override
192        protected Builder self() {
193            return this;
194        }
195    }
196}