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}