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}