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