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 io.prometheus.metrics.annotations.StableApi;
007import java.util.ArrayList;
008import java.util.Arrays;
009import java.util.Collections;
010import java.util.Iterator;
011import java.util.List;
012import java.util.stream.Stream;
013import javax.annotation.Nullable;
014
015/** Immutable set of name/value pairs, sorted by name. */
016@StableApi
017public final class Labels implements Comparable<Labels>, Iterable<Label> {
018
019  public static final Labels EMPTY;
020
021  static {
022    String[] names = new String[] {};
023    String[] values = new String[] {};
024    EMPTY = new Labels(names, names, values);
025  }
026
027  // prometheusNames is the same as names, but dots are replaced with underscores.
028  // Labels is sorted by prometheusNames.
029  // If names[i] does not contain a dot, prometheusNames[i] references the same String as names[i]
030  // so that we don't have unnecessary duplicates of strings.
031  // If none of the names contains a dot, then prometheusNames references the same array as names
032  // so that we don't have unnecessary duplicate arrays.
033  private final String[] prometheusNames;
034  private final String[] names;
035  private final String[] values;
036
037  private Labels(String[] names, String[] prometheusNames, String[] values) {
038    this.names = names;
039    this.prometheusNames = prometheusNames;
040    this.values = values;
041  }
042
043  @SuppressWarnings("ReferenceEquality")
044  public boolean isEmpty() {
045    return this == EMPTY || this.equals(EMPTY);
046  }
047
048  /**
049   * Create a new Labels instance. You can either create Labels with one of the static {@code
050   * Labels.of(...)} methods, or you can use the {@link Labels#builder()}.
051   *
052   * @param keyValuePairs as in {@code {name1, value1, name2, value2}}. Length must be even. {@link
053   *     PrometheusNaming#isValidLabelName(String)} must be true for each name. Use {@link
054   *     PrometheusNaming#sanitizeLabelName(String)} to convert arbitrary strings to valid label
055   *     names. Label names must be unique (no duplicate label names).
056   */
057  public static Labels of(String... keyValuePairs) {
058    if (keyValuePairs.length % 2 != 0) {
059      throw new IllegalArgumentException("Key/value pairs must have an even length");
060    }
061    if (keyValuePairs.length == 0) {
062      return EMPTY;
063    }
064    String[] names = new String[keyValuePairs.length / 2];
065    String[] values = new String[keyValuePairs.length / 2];
066    for (int i = 0; 2 * i < keyValuePairs.length; i++) {
067      names[i] = keyValuePairs[2 * i];
068      values[i] = keyValuePairs[2 * i + 1];
069    }
070    String[] prometheusNames = makePrometheusNames(names);
071    sortAndValidate(names, prometheusNames, values);
072    return new Labels(names, prometheusNames, values);
073  }
074
075  // package private for testing
076  /**
077   * Create a new Labels instance. You can either create Labels with one of the static {@code
078   * Labels.of(...)} methods, or you can use the {@link Labels#builder()}.
079   *
080   * @param names label names. {@link PrometheusNaming#isValidLabelName(String)} must be true for
081   *     each name. Use {@link PrometheusNaming#sanitizeLabelName(String)} to convert arbitrary
082   *     strings to valid label names. Label names must be unique (no duplicate label names).
083   * @param values label values. {@code names.size()} must be equal to {@code values.size()}.
084   */
085  public static Labels of(List<String> names, List<String> values) {
086    if (names.size() != values.size()) {
087      throw new IllegalArgumentException("Names and values must have the same size.");
088    }
089    if (names.isEmpty()) {
090      return EMPTY;
091    }
092    String[] namesCopy = names.toArray(new String[0]);
093    String[] valuesCopy = values.toArray(new String[0]);
094    String[] prometheusNames = makePrometheusNames(namesCopy);
095    sortAndValidate(namesCopy, prometheusNames, valuesCopy);
096    return new Labels(namesCopy, prometheusNames, valuesCopy);
097  }
098
099  /**
100   * Create a new Labels instance. You can either create Labels with one of the static {@code
101   * Labels.of(...)} methods, or you can use the {@link Labels#builder()}.
102   *
103   * @param names label names. {@link PrometheusNaming#isValidLabelName(String)} must be true for
104   *     each name. Use {@link PrometheusNaming#sanitizeLabelName(String)} to convert arbitrary
105   *     strings to valid label names. Label names must be unique (no duplicate label names).
106   * @param values label values. {@code names.length} must be equal to {@code values.length}.
107   */
108  public static Labels of(String[] names, String[] values) {
109    if (names.length != values.length) {
110      throw new IllegalArgumentException("Names and values must have the same length.");
111    }
112    if (names.length == 0) {
113      return EMPTY;
114    }
115    String[] namesCopy = Arrays.copyOf(names, names.length);
116    String[] valuesCopy = Arrays.copyOf(values, values.length);
117    String[] prometheusNames = makePrometheusNames(namesCopy);
118    sortAndValidate(namesCopy, prometheusNames, valuesCopy);
119    return new Labels(namesCopy, prometheusNames, valuesCopy);
120  }
121
122  static String[] makePrometheusNames(String[] names) {
123    String[] prometheusNames = names;
124    for (int i = 0; i < names.length; i++) {
125      String name = names[i];
126      if (!PrometheusNaming.isValidLegacyLabelName(name)) {
127        if (sameObject(prometheusNames, names)) {
128          prometheusNames = Arrays.copyOf(names, names.length);
129        }
130        prometheusNames[i] = PrometheusNaming.prometheusName(name);
131      }
132    }
133    return prometheusNames;
134  }
135
136  /**
137   * Test if these labels contain a specific label name.
138   *
139   * <p>Dots are treated as underscores, so {@code contains("my.label")} and {@code
140   * contains("my_label")} are the same.
141   */
142  public boolean contains(String labelName) {
143    return get(labelName) != null;
144  }
145
146  /**
147   * Get the label value for a given label name.
148   *
149   * <p>Returns {@code null} if the {@code labelName} is not found.
150   *
151   * <p>Dots are treated as underscores, so {@code get("my.label")} and {@code get("my_label")} are
152   * the same.
153   */
154  @Nullable
155  public String get(String labelName) {
156    labelName = prometheusName(labelName);
157    for (int i = 0; i < prometheusNames.length; i++) {
158      if (prometheusNames[i].equals(labelName)) {
159        return values[i];
160      }
161    }
162    return null;
163  }
164
165  private static void sortAndValidate(String[] names, String[] prometheusNames, String[] values) {
166    sort(names, prometheusNames, values);
167    validateNames(names, prometheusNames);
168  }
169
170  private static void validateNames(String[] names, String[] prometheusNames) {
171    for (int i = 0; i < names.length; i++) {
172      if (!isValidLabelName(names[i])) {
173        throw new IllegalArgumentException("'" + names[i] + "' is an illegal label name");
174      }
175      // The arrays are sorted, so duplicates are next to each other
176      if (i > 0 && prometheusNames[i - 1].equals(prometheusNames[i])) {
177        throw new IllegalArgumentException(names[i] + ": duplicate label name");
178      }
179    }
180  }
181
182  /**
183   * Sorts all three parallel arrays in place using introspective quicksort.
184   *
185   * <p>Algorithm: 3-way quicksort with insertion sort for tiny partitions and heapsort fallback at
186   * the recursion depth limit. Parallel arrays are swapped in lockstep.
187   *
188   * <p>Complexity: O(n log n) average and worst case.
189   */
190  private static void sort(String[] names, String[] prometheusNames, String[] values) {
191    StringArraySorter.sort(names, prometheusNames, values);
192  }
193
194  @Override
195  public Iterator<Label> iterator() {
196    return asList().iterator();
197  }
198
199  public Stream<Label> stream() {
200    return asList().stream();
201  }
202
203  public int size() {
204    return names.length;
205  }
206
207  public String getName(int i) {
208    return names[i];
209  }
210
211  /**
212   * Like {@link #getName(int)}, but dots are replaced with underscores.
213   *
214   * <p>This is used by Prometheus exposition formats.
215   */
216  public String getPrometheusName(int i) {
217    return prometheusNames[i];
218  }
219
220  public String getValue(int i) {
221    return values[i];
222  }
223
224  /**
225   * Create a new Labels instance containing the labels of this and the labels of other. This and
226   * other must not contain the same label name.
227   */
228  public Labels merge(Labels other) {
229    if (this.isEmpty()) {
230      return other;
231    }
232    if (other.isEmpty()) {
233      return this;
234    }
235    String[] names = new String[this.names.length + other.names.length];
236    String[] prometheusNames = names;
237    if (!sameObject(this.names, this.prometheusNames)
238        || !sameObject(other.names, other.prometheusNames)) {
239      prometheusNames = new String[names.length];
240    }
241    String[] values = new String[names.length];
242    int thisPos = 0;
243    int otherPos = 0;
244    while (thisPos + otherPos < names.length) {
245      if (thisPos >= this.names.length) {
246        names[thisPos + otherPos] = other.names[otherPos];
247        values[thisPos + otherPos] = other.values[otherPos];
248        if (!sameObject(prometheusNames, names)) {
249          prometheusNames[thisPos + otherPos] = other.prometheusNames[otherPos];
250        }
251        otherPos++;
252      } else if (otherPos >= other.names.length) {
253        names[thisPos + otherPos] = this.names[thisPos];
254        values[thisPos + otherPos] = this.values[thisPos];
255        if (!sameObject(prometheusNames, names)) {
256          prometheusNames[thisPos + otherPos] = this.prometheusNames[thisPos];
257        }
258        thisPos++;
259      } else if (this.prometheusNames[thisPos].compareTo(other.prometheusNames[otherPos]) < 0) {
260        names[thisPos + otherPos] = this.names[thisPos];
261        values[thisPos + otherPos] = this.values[thisPos];
262        if (!sameObject(prometheusNames, names)) {
263          prometheusNames[thisPos + otherPos] = this.prometheusNames[thisPos];
264        }
265        thisPos++;
266      } else if (this.prometheusNames[thisPos].compareTo(other.prometheusNames[otherPos]) > 0) {
267        names[thisPos + otherPos] = other.names[otherPos];
268        values[thisPos + otherPos] = other.values[otherPos];
269        if (!sameObject(prometheusNames, names)) {
270          prometheusNames[thisPos + otherPos] = other.prometheusNames[otherPos];
271        }
272        otherPos++;
273      } else {
274        throw new IllegalArgumentException("Duplicate label name: '" + this.names[thisPos] + "'.");
275      }
276    }
277    return new Labels(names, prometheusNames, values);
278  }
279
280  /**
281   * Create a new Labels instance containing the labels of this and the labels passed as names and
282   * values. The new label names must not already be contained in this Labels instance.
283   */
284  public Labels merge(String[] names, String[] values) {
285    if (this.equals(EMPTY)) {
286      return Labels.of(names, values);
287    }
288    String[] mergedNames = new String[this.names.length + names.length];
289    String[] mergedValues = new String[this.values.length + values.length];
290    System.arraycopy(this.names, 0, mergedNames, 0, this.names.length);
291    System.arraycopy(this.values, 0, mergedValues, 0, this.values.length);
292    System.arraycopy(names, 0, mergedNames, this.names.length, names.length);
293    System.arraycopy(values, 0, mergedValues, this.values.length, values.length);
294    String[] prometheusNames = makePrometheusNames(mergedNames);
295    sortAndValidate(mergedNames, prometheusNames, mergedValues);
296    return new Labels(mergedNames, prometheusNames, mergedValues);
297  }
298
299  /**
300   * Create a new Labels instance containing the labels of this and the label passed as name and
301   * value. The label name must not already be contained in this Labels instance.
302   */
303  public Labels add(String name, String value) {
304    return merge(Labels.of(name, value));
305  }
306
307  public boolean hasSameNames(Labels other) {
308    return Arrays.equals(prometheusNames, other.prometheusNames);
309  }
310
311  public boolean hasSameValues(Labels other) {
312    return Arrays.equals(values, other.values);
313  }
314
315  @Override
316  public int compareTo(Labels other) {
317    int result = compare(prometheusNames, other.prometheusNames);
318    if (result != 0) {
319      return result;
320    }
321    return compare(values, other.values);
322  }
323
324  // Looks like Java doesn't have a compareTo() method for arrays.
325  @SuppressWarnings("ReferenceEquality")
326  private static boolean sameObject(Object left, Object right) {
327    return left == right;
328  }
329
330  private int compare(String[] array1, String[] array2) {
331    int result;
332    for (int i = 0; i < array1.length; i++) {
333      if (array2.length <= i) {
334        return 1;
335      }
336      result = array1[i].compareTo(array2[i]);
337      if (result != 0) {
338        return result;
339      }
340    }
341    if (array2.length > array1.length) {
342      return -1;
343    }
344    return 0;
345  }
346
347  private List<Label> asList() {
348    List<Label> result = new ArrayList<>(names.length);
349    for (int i = 0; i < names.length; i++) {
350      result.add(new Label(names[i], values[i]));
351    }
352    return Collections.unmodifiableList(result);
353  }
354
355  /**
356   * This must not be used in Prometheus exposition formats because names may contain dots.
357   *
358   * <p>However, for debugging it's better to show the original names rather than the Prometheus
359   * names.
360   */
361  @Override
362  public String toString() {
363    StringBuilder b = new StringBuilder();
364    b.append("{");
365    for (int i = 0; i < names.length; i++) {
366      if (i > 0) {
367        b.append(",");
368      }
369      b.append(names[i]);
370      b.append("=\"");
371      appendEscapedLabelValue(b, values[i]);
372      b.append("\"");
373    }
374    b.append("}");
375    return b.toString();
376  }
377
378  private void appendEscapedLabelValue(StringBuilder b, String value) {
379    for (int i = 0; i < value.length(); i++) {
380      char c = value.charAt(i);
381      switch (c) {
382        case '\\':
383          b.append("\\\\");
384          break;
385        case '\"':
386          b.append("\\\"");
387          break;
388        case '\n':
389          b.append("\\n");
390          break;
391        default:
392          b.append(c);
393      }
394    }
395  }
396
397  @Override
398  public boolean equals(Object o) {
399    if (this == o) {
400      return true;
401    }
402    if (o == null || getClass() != o.getClass()) {
403      return false;
404    }
405    Labels labels = (Labels) o;
406    return labels.hasSameNames(this) && labels.hasSameValues(this);
407  }
408
409  @Override
410  public int hashCode() {
411    int result = Arrays.hashCode(prometheusNames);
412    result = 31 * result + Arrays.hashCode(values);
413    return result;
414  }
415
416  public static Builder builder() {
417    return new Builder();
418  }
419
420  public static class Builder {
421    private final List<String> names = new ArrayList<>();
422    private final List<String> values = new ArrayList<>();
423
424    private Builder() {}
425
426    /** Add a label. Call multiple times to add multiple labels. */
427    public Builder label(String name, String value) {
428      names.add(name);
429      values.add(value);
430      return this;
431    }
432
433    public Labels build() {
434      return Labels.of(names, values);
435    }
436  }
437
438  /**
439   * In-place introsort for label arrays, keyed by {@code prometheusNames}.
440   *
441   * <p>Uses 3-way quicksort partitioning for large ranges, insertion sort for tiny ranges, and a
442   * heapsort fallback at the recursion-depth limit to guarantee O(n log n) worst-case complexity.
443   */
444  private static final class StringArraySorter {
445
446    private static final int INSERTION_SORT_THRESHOLD = 24;
447
448    private static void sort(String[] names, String[] prometheusNames, String[] values) {
449      int right = names.length - 1;
450      if (right <= 0) {
451        return;
452      }
453      introSort(names, prometheusNames, values, 0, right, depthLimit(names.length));
454    }
455
456    private static void introSort(
457        String[] names,
458        String[] prometheusNames,
459        String[] values,
460        int left,
461        int right,
462        int depthLimit) {
463      while (left < right) {
464        if (right - left + 1 <= INSERTION_SORT_THRESHOLD) {
465          insertionSort(names, prometheusNames, values, left, right);
466          return;
467        }
468        if (depthLimit == 0) {
469          heapSort(names, prometheusNames, values, left, right);
470          return;
471        }
472        depthLimit--;
473
474        int mid = left + ((right - left) >>> 1);
475        int pivotIndex = medianOf3(prometheusNames, left, mid, right);
476        String pivot = prometheusNames[pivotIndex];
477
478        int lt = left;
479        int i = left;
480        int gt = right;
481        while (i <= gt) {
482          int cmp = compare(prometheusNames[i], pivot);
483          if (cmp < 0) {
484            swap(i, lt, names, prometheusNames, values);
485            i++;
486            lt++;
487          } else if (cmp > 0) {
488            swap(i, gt, names, prometheusNames, values);
489            gt--;
490          } else {
491            i++;
492          }
493        }
494
495        if (lt - left < right - gt) {
496          introSort(names, prometheusNames, values, left, lt - 1, depthLimit);
497          left = gt + 1;
498        } else {
499          introSort(names, prometheusNames, values, gt + 1, right, depthLimit);
500          right = lt - 1;
501        }
502      }
503    }
504
505    private static void insertionSort(
506        String[] names, String[] prometheusNames, String[] values, int left, int right) {
507      for (int i = left + 1; i <= right; i++) {
508        String name = names[i];
509        String prometheusName = prometheusNames[i];
510        final String value = values[i];
511        int j = i - 1;
512        while (j >= left && compare(prometheusNames[j], prometheusName) > 0) {
513          names[j + 1] = names[j];
514          if (!sameObject(prometheusNames, names)) {
515            prometheusNames[j + 1] = prometheusNames[j];
516          }
517          values[j + 1] = values[j];
518          j--;
519        }
520        names[j + 1] = name;
521        if (!sameObject(prometheusNames, names)) {
522          prometheusNames[j + 1] = prometheusName;
523        }
524        values[j + 1] = value;
525      }
526    }
527
528    private static void heapSort(
529        String[] names, String[] prometheusNames, String[] values, int left, int right) {
530      int size = right - left + 1;
531      for (int i = (size >>> 1) - 1; i >= 0; i--) {
532        siftDown(names, prometheusNames, values, left, i, size);
533      }
534      for (int end = size - 1; end > 0; end--) {
535        swap(left, left + end, names, prometheusNames, values);
536        siftDown(names, prometheusNames, values, left, 0, end);
537      }
538    }
539
540    private static void siftDown(
541        String[] names, String[] prometheusNames, String[] values, int base, int root, int size) {
542      while (true) {
543        int child = (root << 1) + 1;
544        if (child >= size) {
545          return;
546        }
547        int rightChild = child + 1;
548        if (rightChild < size
549            && compare(prometheusNames[base + child], prometheusNames[base + rightChild]) < 0) {
550          child = rightChild;
551        }
552        if (compare(prometheusNames[base + root], prometheusNames[base + child]) >= 0) {
553          return;
554        }
555        swap(base + root, base + child, names, prometheusNames, values);
556        root = child;
557      }
558    }
559
560    private static int depthLimit(int length) {
561      int result = 0;
562      while (length > 1) {
563        result++;
564        length >>>= 1;
565      }
566      return result << 1;
567    }
568
569    private static int medianOf3(String[] values, int i, int j, int k) {
570      if (compare(values[i], values[j]) > 0) {
571        int tmp = i;
572        i = j;
573        j = tmp;
574      }
575      if (compare(values[j], values[k]) > 0) {
576        int tmp = j;
577        j = k;
578        k = tmp;
579      }
580      if (compare(values[i], values[j]) > 0) {
581        int tmp = i;
582        i = j;
583        j = tmp;
584      }
585      return j;
586    }
587
588    private static int compare(String left, String right) {
589      return left.compareTo(right);
590    }
591
592    private static void swap(
593        int i, int j, String[] names, String[] prometheusNames, String[] values) {
594      if (i == j) {
595        return;
596      }
597      String tmp = names[i];
598      names[i] = names[j];
599      names[j] = tmp;
600      tmp = values[i];
601      values[i] = values[j];
602      values[j] = tmp;
603      if (!sameObject(prometheusNames, names)) {
604        tmp = prometheusNames[i];
605        prometheusNames[i] = prometheusNames[j];
606        prometheusNames[j] = tmp;
607      }
608    }
609  }
610}