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}