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