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