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}