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