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}