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