001package io.prometheus.metrics.model.snapshots;
002
003import static io.prometheus.metrics.model.snapshots.PrometheusNaming.isValidLabelName;
004import static io.prometheus.metrics.model.snapshots.PrometheusNaming.prometheusName;
005
006import java.util.ArrayList;
007import java.util.Arrays;
008import java.util.Collections;
009import java.util.Iterator;
010import java.util.List;
011import java.util.stream.Stream;
012
013/** Immutable set of name/value pairs, sorted by name. */
014public final class Labels implements Comparable<Labels>, Iterable<Label> {
015
016  public static final Labels EMPTY;
017
018  static {
019    String[] names = new String[] {};
020    String[] values = new String[] {};
021    EMPTY = new Labels(names, names, values);
022  }
023
024  // prometheusNames is the same as names, but dots are replaced with underscores.
025  // Labels is sorted by prometheusNames.
026  // If names[i] does not contain a dot, prometheusNames[i] references the same String as names[i]
027  // so that we don't have unnecessary duplicates of strings.
028  // If none of the names contains a dot, then prometheusNames references the same array as names
029  // so that we don't have unnecessary duplicate arrays.
030  private final String[] prometheusNames;
031  private final String[] names;
032  private final String[] values;
033
034  private Labels(String[] names, String[] prometheusNames, String[] values) {
035    this.names = names;
036    this.prometheusNames = prometheusNames;
037    this.values = values;
038  }
039
040  @SuppressWarnings("ReferenceEquality")
041  public boolean isEmpty() {
042    return this == EMPTY || this.equals(EMPTY);
043  }
044
045  /**
046   * Create a new Labels instance. You can either create Labels with one of the static {@code
047   * Labels.of(...)} methods, or you can use the {@link Labels#builder()}.
048   *
049   * @param keyValuePairs as in {@code {name1, value1, name2, value2}}. Length must be even. {@link
050   *     PrometheusNaming#isValidLabelName(String)} must be true for each name. Use {@link
051   *     PrometheusNaming#sanitizeLabelName(String)} to convert arbitrary strings to valid label
052   *     names. Label names must be unique (no duplicate label names).
053   */
054  public static Labels of(String... keyValuePairs) {
055    if (keyValuePairs.length % 2 != 0) {
056      throw new IllegalArgumentException("Key/value pairs must have an even length");
057    }
058    if (keyValuePairs.length == 0) {
059      return EMPTY;
060    }
061    String[] names = new String[keyValuePairs.length / 2];
062    String[] values = new String[keyValuePairs.length / 2];
063    for (int i = 0; 2 * i < keyValuePairs.length; i++) {
064      names[i] = keyValuePairs[2 * i];
065      values[i] = keyValuePairs[2 * i + 1];
066    }
067    String[] prometheusNames = makePrometheusNames(names);
068    sortAndValidate(names, prometheusNames, values);
069    return new Labels(names, prometheusNames, values);
070  }
071
072  // package private for testing
073  /**
074   * Create a new Labels instance. You can either create Labels with one of the static {@code
075   * Labels.of(...)} methods, or you can use the {@link Labels#builder()}.
076   *
077   * @param names label names. {@link PrometheusNaming#isValidLabelName(String)} must be true for
078   *     each name. Use {@link PrometheusNaming#sanitizeLabelName(String)} to convert arbitrary
079   *     strings to valid label names. Label names must be unique (no duplicate label names).
080   * @param values label values. {@code names.size()} must be equal to {@code values.size()}.
081   */
082  public static Labels of(List<String> names, List<String> values) {
083    if (names.size() != values.size()) {
084      throw new IllegalArgumentException("Names and values must have the same size.");
085    }
086    if (names.isEmpty()) {
087      return EMPTY;
088    }
089    String[] namesCopy = names.toArray(new String[0]);
090    String[] valuesCopy = values.toArray(new String[0]);
091    String[] prometheusNames = makePrometheusNames(namesCopy);
092    sortAndValidate(namesCopy, prometheusNames, valuesCopy);
093    return new Labels(namesCopy, prometheusNames, valuesCopy);
094  }
095
096  /**
097   * Create a new Labels instance. You can either create Labels with one of the static {@code
098   * Labels.of(...)} methods, or you can use the {@link Labels#builder()}.
099   *
100   * @param names label names. {@link PrometheusNaming#isValidLabelName(String)} must be true for
101   *     each name. Use {@link PrometheusNaming#sanitizeLabelName(String)} to convert arbitrary
102   *     strings to valid label names. Label names must be unique (no duplicate label names).
103   * @param values label values. {@code names.length} must be equal to {@code values.length}.
104   */
105  public static Labels of(String[] names, String[] values) {
106    if (names.length != values.length) {
107      throw new IllegalArgumentException("Names and values must have the same length.");
108    }
109    if (names.length == 0) {
110      return EMPTY;
111    }
112    String[] namesCopy = Arrays.copyOf(names, names.length);
113    String[] valuesCopy = Arrays.copyOf(values, values.length);
114    String[] prometheusNames = makePrometheusNames(namesCopy);
115    sortAndValidate(namesCopy, prometheusNames, valuesCopy);
116    return new Labels(namesCopy, prometheusNames, valuesCopy);
117  }
118
119  static String[] makePrometheusNames(String[] names) {
120    String[] prometheusNames = names;
121    for (int i = 0; i < names.length; i++) {
122      if (names[i].contains(".")) {
123        if (prometheusNames == names) {
124          prometheusNames = Arrays.copyOf(names, names.length);
125        }
126        prometheusNames[i] = PrometheusNaming.prometheusName(names[i]);
127      }
128    }
129    return prometheusNames;
130  }
131
132  /**
133   * Test if these labels contain a specific label name.
134   *
135   * <p>Dots are treated as underscores, so {@code contains("my.label")} and {@code
136   * contains("my_label")} are the same.
137   */
138  public boolean contains(String labelName) {
139    return get(labelName) != null;
140  }
141
142  /**
143   * Get the label value for a given label name.
144   *
145   * <p>Returns {@code null} if the {@code labelName} is not found.
146   *
147   * <p>Dots are treated as underscores, so {@code get("my.label")} and {@code get("my_label")} are
148   * the same.
149   */
150  public String get(String labelName) {
151    labelName = prometheusName(labelName);
152    for (int i = 0; i < prometheusNames.length; i++) {
153      if (prometheusNames[i].equals(labelName)) {
154        return values[i];
155      }
156    }
157    return null;
158  }
159
160  private static void sortAndValidate(String[] names, String[] prometheusNames, String[] values) {
161    sort(names, prometheusNames, values);
162    validateNames(names, prometheusNames);
163  }
164
165  private static void validateNames(String[] names, String[] prometheusNames) {
166    for (int i = 0; i < names.length; i++) {
167      if (!isValidLabelName(names[i])) {
168        throw new IllegalArgumentException("'" + names[i] + "' is an illegal label name");
169      }
170      // The arrays are sorted, so duplicates are next to each other
171      if (i > 0 && prometheusNames[i - 1].equals(prometheusNames[i])) {
172        throw new IllegalArgumentException(names[i] + ": duplicate label name");
173      }
174    }
175  }
176
177  private static void sort(String[] names, String[] prometheusNames, String[] values) {
178    // bubblesort
179    int n = prometheusNames.length;
180    for (int i = 0; i < n - 1; i++) {
181      for (int j = 0; j < n - i - 1; j++) {
182        if (prometheusNames[j].compareTo(prometheusNames[j + 1]) > 0) {
183          swap(j, j + 1, names, prometheusNames, values);
184        }
185      }
186    }
187  }
188
189  private static void swap(
190      int i, int j, String[] names, String[] prometheusNames, String[] values) {
191    String tmp = names[j];
192    names[j] = names[i];
193    names[i] = tmp;
194    tmp = values[j];
195    values[j] = values[i];
196    values[i] = tmp;
197    if (prometheusNames != names) {
198      tmp = prometheusNames[j];
199      prometheusNames[j] = prometheusNames[i];
200      prometheusNames[i] = tmp;
201    }
202  }
203
204  @Override
205  public Iterator<Label> iterator() {
206    return asList().iterator();
207  }
208
209  public Stream<Label> stream() {
210    return asList().stream();
211  }
212
213  public int size() {
214    return names.length;
215  }
216
217  public String getName(int i) {
218    return names[i];
219  }
220
221  /**
222   * Like {@link #getName(int)}, but dots are replaced with underscores.
223   *
224   * <p>This is used by Prometheus exposition formats.
225   */
226  public String getPrometheusName(int i) {
227    return prometheusNames[i];
228  }
229
230  public String getValue(int i) {
231    return values[i];
232  }
233
234  /**
235   * Create a new Labels instance containing the labels of this and the labels of other. This and
236   * other must not contain the same label name.
237   */
238  public Labels merge(Labels other) {
239    if (this.isEmpty()) {
240      return other;
241    }
242    if (other.isEmpty()) {
243      return this;
244    }
245    String[] names = new String[this.names.length + other.names.length];
246    String[] prometheusNames = names;
247    if (this.names != this.prometheusNames || other.names != other.prometheusNames) {
248      prometheusNames = new String[names.length];
249    }
250    String[] values = new String[names.length];
251    int thisPos = 0;
252    int otherPos = 0;
253    while (thisPos + otherPos < names.length) {
254      if (thisPos >= this.names.length) {
255        names[thisPos + otherPos] = other.names[otherPos];
256        values[thisPos + otherPos] = other.values[otherPos];
257        if (prometheusNames != names) {
258          prometheusNames[thisPos + otherPos] = other.prometheusNames[otherPos];
259        }
260        otherPos++;
261      } else if (otherPos >= other.names.length) {
262        names[thisPos + otherPos] = this.names[thisPos];
263        values[thisPos + otherPos] = this.values[thisPos];
264        if (prometheusNames != names) {
265          prometheusNames[thisPos + otherPos] = this.prometheusNames[thisPos];
266        }
267        thisPos++;
268      } else if (this.prometheusNames[thisPos].compareTo(other.prometheusNames[otherPos]) < 0) {
269        names[thisPos + otherPos] = this.names[thisPos];
270        values[thisPos + otherPos] = this.values[thisPos];
271        if (prometheusNames != names) {
272          prometheusNames[thisPos + otherPos] = this.prometheusNames[thisPos];
273        }
274        thisPos++;
275      } else if (this.prometheusNames[thisPos].compareTo(other.prometheusNames[otherPos]) > 0) {
276        names[thisPos + otherPos] = other.names[otherPos];
277        values[thisPos + otherPos] = other.values[otherPos];
278        if (prometheusNames != names) {
279          prometheusNames[thisPos + otherPos] = other.prometheusNames[otherPos];
280        }
281        otherPos++;
282      } else {
283        throw new IllegalArgumentException("Duplicate label name: '" + this.names[thisPos] + "'.");
284      }
285    }
286    return new Labels(names, prometheusNames, values);
287  }
288
289  /**
290   * Create a new Labels instance containing the labels of this and the labels passed as names and
291   * values. The new label names must not already be contained in this Labels instance.
292   */
293  public Labels merge(String[] names, String[] values) {
294    if (this.equals(EMPTY)) {
295      return Labels.of(names, values);
296    }
297    String[] mergedNames = new String[this.names.length + names.length];
298    String[] mergedValues = new String[this.values.length + values.length];
299    System.arraycopy(this.names, 0, mergedNames, 0, this.names.length);
300    System.arraycopy(this.values, 0, mergedValues, 0, this.values.length);
301    System.arraycopy(names, 0, mergedNames, this.names.length, names.length);
302    System.arraycopy(values, 0, mergedValues, this.values.length, values.length);
303    String[] prometheusNames = makePrometheusNames(mergedNames);
304    sortAndValidate(mergedNames, prometheusNames, mergedValues);
305    return new Labels(mergedNames, prometheusNames, mergedValues);
306  }
307
308  /**
309   * Create a new Labels instance containing the labels of this and the label passed as name and
310   * value. The label name must not already be contained in this Labels instance.
311   */
312  public Labels add(String name, String value) {
313    return merge(Labels.of(name, value));
314  }
315
316  public boolean hasSameNames(Labels other) {
317    return Arrays.equals(prometheusNames, other.prometheusNames);
318  }
319
320  public boolean hasSameValues(Labels other) {
321    return Arrays.equals(values, other.values);
322  }
323
324  @Override
325  public int compareTo(Labels other) {
326    int result = compare(prometheusNames, other.prometheusNames);
327    if (result != 0) {
328      return result;
329    }
330    return compare(values, other.values);
331  }
332
333  // Looks like Java doesn't have a compareTo() method for arrays.
334  private int compare(String[] array1, String[] array2) {
335    int result;
336    for (int i = 0; i < array1.length; i++) {
337      if (array2.length <= i) {
338        return 1;
339      }
340      result = array1[i].compareTo(array2[i]);
341      if (result != 0) {
342        return result;
343      }
344    }
345    if (array2.length > array1.length) {
346      return -1;
347    }
348    return 0;
349  }
350
351  private List<Label> asList() {
352    List<Label> result = new ArrayList<>(names.length);
353    for (int i = 0; i < names.length; i++) {
354      result.add(new Label(names[i], values[i]));
355    }
356    return Collections.unmodifiableList(result);
357  }
358
359  /**
360   * This must not be used in Prometheus exposition formats because names may contain dots.
361   *
362   * <p>However, for debugging it's better to show the original names rather than the Prometheus
363   * names.
364   */
365  @Override
366  public String toString() {
367    StringBuilder b = new StringBuilder();
368    b.append("{");
369    for (int i = 0; i < names.length; i++) {
370      if (i > 0) {
371        b.append(",");
372      }
373      b.append(names[i]);
374      b.append("=\"");
375      appendEscapedLabelValue(b, values[i]);
376      b.append("\"");
377    }
378    b.append("}");
379    return b.toString();
380  }
381
382  private void appendEscapedLabelValue(StringBuilder b, String value) {
383    for (int i = 0; i < value.length(); i++) {
384      char c = value.charAt(i);
385      switch (c) {
386        case '\\':
387          b.append("\\\\");
388          break;
389        case '\"':
390          b.append("\\\"");
391          break;
392        case '\n':
393          b.append("\\n");
394          break;
395        default:
396          b.append(c);
397      }
398    }
399  }
400
401  @Override
402  public boolean equals(Object o) {
403    if (this == o) {
404      return true;
405    }
406    if (o == null || getClass() != o.getClass()) {
407      return false;
408    }
409    Labels labels = (Labels) o;
410    return labels.hasSameNames(this) && labels.hasSameValues(this);
411  }
412
413  @Override
414  public int hashCode() {
415    int result = Arrays.hashCode(prometheusNames);
416    result = 31 * result + Arrays.hashCode(values);
417    return result;
418  }
419
420  public static Builder builder() {
421    return new Builder();
422  }
423
424  public static class Builder {
425    private final List<String> names = new ArrayList<>();
426    private final List<String> values = new ArrayList<>();
427
428    private Builder() {}
429
430    /** Add a label. Call multiple times to add multiple labels. */
431    public Builder label(String name, String value) {
432      names.add(name);
433      values.add(value);
434      return this;
435    }
436
437    public Labels build() {
438      return Labels.of(names, values);
439    }
440  }
441}