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 (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 (this.names != this.prometheusNames || 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 (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 (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 (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 (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  private int compare(String[] array1, String[] array2) {
325    int result;
326    for (int i = 0; i < array1.length; i++) {
327      if (array2.length <= i) {
328        return 1;
329      }
330      result = array1[i].compareTo(array2[i]);
331      if (result != 0) {
332        return result;
333      }
334    }
335    if (array2.length > array1.length) {
336      return -1;
337    }
338    return 0;
339  }
340
341  private List<Label> asList() {
342    List<Label> result = new ArrayList<>(names.length);
343    for (int i = 0; i < names.length; i++) {
344      result.add(new Label(names[i], values[i]));
345    }
346    return Collections.unmodifiableList(result);
347  }
348
349  /**
350   * This must not be used in Prometheus exposition formats because names may contain dots.
351   *
352   * <p>However, for debugging it's better to show the original names rather than the Prometheus
353   * names.
354   */
355  @Override
356  public String toString() {
357    StringBuilder b = new StringBuilder();
358    b.append("{");
359    for (int i = 0; i < names.length; i++) {
360      if (i > 0) {
361        b.append(",");
362      }
363      b.append(names[i]);
364      b.append("=\"");
365      appendEscapedLabelValue(b, values[i]);
366      b.append("\"");
367    }
368    b.append("}");
369    return b.toString();
370  }
371
372  private void appendEscapedLabelValue(StringBuilder b, String value) {
373    for (int i = 0; i < value.length(); i++) {
374      char c = value.charAt(i);
375      switch (c) {
376        case '\\':
377          b.append("\\\\");
378          break;
379        case '\"':
380          b.append("\\\"");
381          break;
382        case '\n':
383          b.append("\\n");
384          break;
385        default:
386          b.append(c);
387      }
388    }
389  }
390
391  @Override
392  public boolean equals(Object o) {
393    if (this == o) {
394      return true;
395    }
396    if (o == null || getClass() != o.getClass()) {
397      return false;
398    }
399    Labels labels = (Labels) o;
400    return labels.hasSameNames(this) && labels.hasSameValues(this);
401  }
402
403  @Override
404  public int hashCode() {
405    int result = Arrays.hashCode(prometheusNames);
406    result = 31 * result + Arrays.hashCode(values);
407    return result;
408  }
409
410  public static Builder builder() {
411    return new Builder();
412  }
413
414  public static class Builder {
415    private final List<String> names = new ArrayList<>();
416    private final List<String> values = new ArrayList<>();
417
418    private Builder() {}
419
420    /** Add a label. Call multiple times to add multiple labels. */
421    public Builder label(String name, String value) {
422      names.add(name);
423      values.add(value);
424      return this;
425    }
426
427    public Labels build() {
428      return Labels.of(names, values);
429    }
430  }
431
432  /**
433   * In-place introsort for label arrays, keyed by {@code prometheusNames}.
434   *
435   * <p>Uses 3-way quicksort partitioning for large ranges, insertion sort for tiny ranges, and a
436   * heapsort fallback at the recursion-depth limit to guarantee O(n log n) worst-case complexity.
437   */
438  private static final class StringArraySorter {
439
440    private static final int INSERTION_SORT_THRESHOLD = 24;
441
442    private static void sort(String[] names, String[] prometheusNames, String[] values) {
443      int right = names.length - 1;
444      if (right <= 0) {
445        return;
446      }
447      introSort(names, prometheusNames, values, 0, right, depthLimit(names.length));
448    }
449
450    private static void introSort(
451        String[] names,
452        String[] prometheusNames,
453        String[] values,
454        int left,
455        int right,
456        int depthLimit) {
457      while (left < right) {
458        if (right - left + 1 <= INSERTION_SORT_THRESHOLD) {
459          insertionSort(names, prometheusNames, values, left, right);
460          return;
461        }
462        if (depthLimit == 0) {
463          heapSort(names, prometheusNames, values, left, right);
464          return;
465        }
466        depthLimit--;
467
468        int mid = left + ((right - left) >>> 1);
469        int pivotIndex = medianOf3(prometheusNames, left, mid, right);
470        String pivot = prometheusNames[pivotIndex];
471
472        int lt = left;
473        int i = left;
474        int gt = right;
475        while (i <= gt) {
476          int cmp = compare(prometheusNames[i], pivot);
477          if (cmp < 0) {
478            swap(i, lt, names, prometheusNames, values);
479            i++;
480            lt++;
481          } else if (cmp > 0) {
482            swap(i, gt, names, prometheusNames, values);
483            gt--;
484          } else {
485            i++;
486          }
487        }
488
489        if (lt - left < right - gt) {
490          introSort(names, prometheusNames, values, left, lt - 1, depthLimit);
491          left = gt + 1;
492        } else {
493          introSort(names, prometheusNames, values, gt + 1, right, depthLimit);
494          right = lt - 1;
495        }
496      }
497    }
498
499    private static void insertionSort(
500        String[] names, String[] prometheusNames, String[] values, int left, int right) {
501      for (int i = left + 1; i <= right; i++) {
502        String name = names[i];
503        String prometheusName = prometheusNames[i];
504        final String value = values[i];
505        int j = i - 1;
506        while (j >= left && compare(prometheusNames[j], prometheusName) > 0) {
507          names[j + 1] = names[j];
508          if (prometheusNames != names) {
509            prometheusNames[j + 1] = prometheusNames[j];
510          }
511          values[j + 1] = values[j];
512          j--;
513        }
514        names[j + 1] = name;
515        if (prometheusNames != names) {
516          prometheusNames[j + 1] = prometheusName;
517        }
518        values[j + 1] = value;
519      }
520    }
521
522    private static void heapSort(
523        String[] names, String[] prometheusNames, String[] values, int left, int right) {
524      int size = right - left + 1;
525      for (int i = (size >>> 1) - 1; i >= 0; i--) {
526        siftDown(names, prometheusNames, values, left, i, size);
527      }
528      for (int end = size - 1; end > 0; end--) {
529        swap(left, left + end, names, prometheusNames, values);
530        siftDown(names, prometheusNames, values, left, 0, end);
531      }
532    }
533
534    private static void siftDown(
535        String[] names, String[] prometheusNames, String[] values, int base, int root, int size) {
536      while (true) {
537        int child = (root << 1) + 1;
538        if (child >= size) {
539          return;
540        }
541        int rightChild = child + 1;
542        if (rightChild < size
543            && compare(prometheusNames[base + child], prometheusNames[base + rightChild]) < 0) {
544          child = rightChild;
545        }
546        if (compare(prometheusNames[base + root], prometheusNames[base + child]) >= 0) {
547          return;
548        }
549        swap(base + root, base + child, names, prometheusNames, values);
550        root = child;
551      }
552    }
553
554    private static int depthLimit(int length) {
555      int result = 0;
556      while (length > 1) {
557        result++;
558        length >>>= 1;
559      }
560      return result << 1;
561    }
562
563    private static int medianOf3(String[] values, int i, int j, int k) {
564      if (compare(values[i], values[j]) > 0) {
565        int tmp = i;
566        i = j;
567        j = tmp;
568      }
569      if (compare(values[j], values[k]) > 0) {
570        int tmp = j;
571        j = k;
572        k = tmp;
573      }
574      if (compare(values[i], values[j]) > 0) {
575        int tmp = i;
576        i = j;
577        j = tmp;
578      }
579      return j;
580    }
581
582    private static int compare(String left, String right) {
583      return left.compareTo(right);
584    }
585
586    private static void swap(
587        int i, int j, String[] names, String[] prometheusNames, String[] values) {
588      if (i == j) {
589        return;
590      }
591      String tmp = names[i];
592      names[i] = names[j];
593      names[j] = tmp;
594      tmp = values[i];
595      values[i] = values[j];
596      values[j] = tmp;
597      if (prometheusNames != names) {
598        tmp = prometheusNames[i];
599        prometheusNames[i] = prometheusNames[j];
600        prometheusNames[j] = tmp;
601      }
602    }
603  }
604}