001package io.prometheus.metrics.model.registry; 002 003import static java.util.Collections.emptySet; 004 005import io.prometheus.metrics.annotations.StableApi; 006import io.prometheus.metrics.model.snapshots.MetricFamilyDescriptor; 007import io.prometheus.metrics.model.snapshots.MetricMetadata; 008import io.prometheus.metrics.model.snapshots.MetricSnapshot; 009import io.prometheus.metrics.model.snapshots.MetricSnapshots; 010import io.prometheus.metrics.model.snapshots.Unit; 011import java.util.ArrayList; 012import java.util.Collections; 013import java.util.HashSet; 014import java.util.List; 015import java.util.Objects; 016import java.util.Set; 017import java.util.concurrent.ConcurrentHashMap; 018import java.util.function.Predicate; 019import javax.annotation.Nullable; 020 021@StableApi 022public class PrometheusRegistry { 023 024 public static final PrometheusRegistry defaultRegistry = new PrometheusRegistry(); 025 026 private final Set<Collector> collectors = ConcurrentHashMap.newKeySet(); 027 private final Set<MultiCollector> multiCollectors = ConcurrentHashMap.newKeySet(); 028 private final ConcurrentHashMap<String, RegistrationInfo> registered = new ConcurrentHashMap<>(); 029 private final ConcurrentHashMap<Collector, CollectorRegistration> collectorMetadata = 030 new ConcurrentHashMap<>(); 031 private final ConcurrentHashMap<MultiCollector, List<MultiCollectorRegistration>> 032 multiCollectorMetadata = new ConcurrentHashMap<>(); 033 034 /** 035 * Maps exposition-level names (e.g. "events_total", "events_created") to the owning 036 * prometheusName (e.g. "events"). Used to detect cross-type collisions at registration time. 037 */ 038 private final ConcurrentHashMap<String, String> expositionNameOwners = new ConcurrentHashMap<>(); 039 040 /** Stores the registration details for a Collector at registration time. */ 041 private static class CollectorRegistration { 042 final String prometheusName; 043 @Nullable final String expositionBasePrometheusName; 044 final Set<String> labelNames; 045 046 CollectorRegistration( 047 String prometheusName, 048 @Nullable String expositionBasePrometheusName, 049 @Nullable Set<String> labelNames) { 050 this.prometheusName = prometheusName; 051 this.expositionBasePrometheusName = expositionBasePrometheusName; 052 this.labelNames = immutableLabelNames(labelNames); 053 } 054 } 055 056 /** 057 * Stores the registration details for a single metric within a MultiCollector. A MultiCollector 058 * can produce multiple metrics, so we need one of these per metric name. 059 */ 060 private static class MultiCollectorRegistration { 061 final String prometheusName; 062 final Set<String> labelNames; 063 064 MultiCollectorRegistration(String prometheusName, @Nullable Set<String> labelNames) { 065 this.prometheusName = prometheusName; 066 this.labelNames = immutableLabelNames(labelNames); 067 } 068 } 069 070 /** 071 * Tracks registration information for each metric name to enable validation of type consistency, 072 * label schema uniqueness, and help/unit consistency. 073 */ 074 private static class RegistrationInfo { 075 private final MetricType type; 076 private final Set<Set<String>> labelSchemas; 077 @Nullable private String help; 078 @Nullable private Unit unit; 079 080 private RegistrationInfo( 081 MetricType type, 082 Set<Set<String>> labelSchemas, 083 @Nullable String help, 084 @Nullable Unit unit) { 085 this.type = type; 086 this.labelSchemas = labelSchemas; 087 this.help = help; 088 this.unit = unit; 089 } 090 091 static RegistrationInfo of( 092 MetricType type, 093 @Nullable Set<String> labelNames, 094 @Nullable String help, 095 @Nullable Unit unit) { 096 Set<Set<String>> labelSchemas = ConcurrentHashMap.newKeySet(); 097 Set<String> normalized = 098 (labelNames == null || labelNames.isEmpty()) ? emptySet() : labelNames; 099 labelSchemas.add(normalized); 100 return new RegistrationInfo(type, labelSchemas, help, unit); 101 } 102 103 /** 104 * Validates that the given help and unit are exactly equal to this registration. Throws if 105 * values differ, including when one is null and the other is non-null. This ensures consistent 106 * metadata across all collectors sharing the same metric name. 107 */ 108 void validateMetadata(@Nullable String newHelp, @Nullable Unit newUnit) { 109 if (!Objects.equals(help, newHelp)) { 110 throw new IllegalArgumentException( 111 "Conflicting help strings. Existing: \"" + help + "\", new: \"" + newHelp + "\""); 112 } 113 if (!Objects.equals(unit, newUnit)) { 114 throw new IllegalArgumentException( 115 "Conflicting unit. Existing: " + unit + ", new: " + newUnit); 116 } 117 } 118 119 /** 120 * Adds a label schema to this registration. 121 * 122 * @param labelNames the label names to add (null or empty sets are normalized to empty set) 123 * @return true if the schema was added (new), false if it already existed 124 */ 125 boolean addLabelSet(@Nullable Set<String> labelNames) { 126 Set<String> normalized = 127 (labelNames == null || labelNames.isEmpty()) ? emptySet() : labelNames; 128 return labelSchemas.add(normalized); 129 } 130 131 /** 132 * Removes a label schema from this registration. 133 * 134 * @param labelNames the label names to remove (null or empty sets are normalized to empty set) 135 */ 136 void removeLabelSet(@Nullable Set<String> labelNames) { 137 Set<String> normalized = 138 (labelNames == null || labelNames.isEmpty()) ? emptySet() : labelNames; 139 labelSchemas.remove(normalized); 140 } 141 142 /** Returns true if all label schemas have been unregistered. */ 143 boolean isEmpty() { 144 return labelSchemas.isEmpty(); 145 } 146 147 MetricType getType() { 148 return type; 149 } 150 } 151 152 /** 153 * Returns an immutable set of label names for storage. Defends against mutation of the set 154 * returned by {@code Collector.getLabelNames()} after registration, which would break duplicate 155 * detection and unregistration. 156 */ 157 private static Set<String> immutableLabelNames(@Nullable Set<String> labelNames) { 158 if (labelNames == null || labelNames.isEmpty()) { 159 return emptySet(); 160 } 161 return Collections.unmodifiableSet(new HashSet<>(labelNames)); 162 } 163 164 @SuppressWarnings("deprecation") 165 @Nullable 166 private static String getDeprecatedPrometheusName(Collector collector) { 167 return collector.getPrometheusName(); 168 } 169 170 @SuppressWarnings("deprecation") 171 private static List<String> getDeprecatedPrometheusNames(MultiCollector collector) { 172 return collector.getPrometheusNames(); 173 } 174 175 /** 176 * Computes the set of exposition-level time series names that a metric with the given name and 177 * type will produce. 178 */ 179 static Set<String> computeExpositionNames(String prometheusName, MetricType type) { 180 Set<String> names = new HashSet<>(); 181 switch (type) { 182 case COUNTER: 183 names.add(prometheusName + "_total"); 184 names.add(prometheusName + "_created"); 185 break; 186 case INFO: 187 names.add(prometheusName + "_info"); 188 break; 189 case HISTOGRAM: 190 names.add(prometheusName + "_bucket"); 191 names.add(prometheusName + "_count"); 192 names.add(prometheusName + "_sum"); 193 names.add(prometheusName + "_created"); 194 break; 195 case SUMMARY: 196 names.add(prometheusName + "_count"); 197 names.add(prometheusName + "_sum"); 198 names.add(prometheusName + "_created"); 199 break; 200 case GAUGE: 201 case STATESET: 202 case UNKNOWN: 203 default: 204 names.add(prometheusName); 205 break; 206 } 207 return names; 208 } 209 210 /** 211 * Validates the registration of a metric with the given parameters. Ensures type consistency, 212 * label schema uniqueness, help/unit consistency, and exposition name collision detection. 213 */ 214 private void validateRegistration( 215 String prometheusName, 216 MetricType metricType, 217 Set<String> normalizedLabels, 218 @Nullable String help, 219 @Nullable Unit unit) { 220 final MetricType type = metricType; 221 final Set<String> names = normalizedLabels; 222 final String helpForValidation = help; 223 final Unit unitForValidation = unit; 224 225 Set<String> expositionNames = computeExpositionNames(prometheusName, type); 226 String claimError = claimExpositionNames(expositionNames, prometheusName); 227 if (claimError != null) { 228 throw new IllegalArgumentException(claimError); 229 } 230 231 boolean success = false; 232 try { 233 registered.compute( 234 prometheusName, 235 (n, existingInfo) -> { 236 if (existingInfo == null) { 237 return RegistrationInfo.of(type, names, helpForValidation, unitForValidation); 238 } else { 239 if (existingInfo.getType() != type) { 240 throw new IllegalArgumentException( 241 prometheusName 242 + ": Conflicting metric types. Existing: " 243 + existingInfo.getType() 244 + ", new: " 245 + type); 246 } 247 // Check label set first; only mutate help/unit after validation passes. 248 if (!existingInfo.addLabelSet(names)) { 249 throw new IllegalArgumentException( 250 prometheusName 251 + ": duplicate metric name with identical label schema " 252 + names); 253 } 254 // Roll back label schema if metadata validation fails 255 try { 256 existingInfo.validateMetadata(helpForValidation, unitForValidation); 257 } catch (IllegalArgumentException e) { 258 existingInfo.removeLabelSet(names); 259 throw e; 260 } 261 return existingInfo; 262 } 263 }); 264 success = true; 265 } finally { 266 if (!success) { 267 releaseExpositionNames(expositionNames, prometheusName); 268 } 269 } 270 } 271 272 /** 273 * Atomically claims exposition names for the given metric. Returns null on success, or an error 274 * message on conflict. Rolls back any partially claimed names on failure. 275 */ 276 @Nullable 277 private String claimExpositionNames(Set<String> expositionNames, String prometheusName) { 278 for (String expositionName : expositionNames) { 279 String existingOwner = expositionNameOwners.putIfAbsent(expositionName, prometheusName); 280 if (existingOwner != null && !existingOwner.equals(prometheusName)) { 281 releaseExpositionNames(expositionNames, prometheusName); 282 return "'" 283 + prometheusName 284 + "' and '" 285 + existingOwner 286 + "' have conflicting exposition name: '" 287 + expositionName 288 + "'"; 289 } 290 } 291 return null; 292 } 293 294 private void releaseExpositionNames(Set<String> expositionNames, String owner) { 295 for (String expositionName : expositionNames) { 296 expositionNameOwners.remove(expositionName, owner); 297 } 298 } 299 300 public void register(Collector collector) { 301 if (!collectors.add(collector)) { 302 throw new IllegalArgumentException("Collector instance is already registered"); 303 } 304 try { 305 MetricFamilyDescriptor descriptor = collector.getMetricFamilyDescriptor(); 306 307 // Only perform validation if collector provides sufficient metadata. Collectors that don't 308 // implement getMetricFamilyDescriptor() will skip registration-time validation. 309 if (descriptor != null) { 310 String prometheusName = descriptor.getPrometheusName(); 311 MetricType metricType = descriptor.getType(); 312 Set<String> normalizedLabels = immutableLabelNames(descriptor.getLabelNames()); 313 MetricMetadata metadata = descriptor.getMetadata(); 314 validateRegistration( 315 prometheusName, metricType, normalizedLabels, metadata.getHelp(), metadata.getUnit()); 316 collectorMetadata.put( 317 collector, 318 new CollectorRegistration( 319 prometheusName, metadata.getExpositionBasePrometheusName(), normalizedLabels)); 320 } 321 // Catch RuntimeException broadly because collector methods are user-implemented and could 322 // throw any RuntimeException. Ensures cleanup on failure. 323 } catch (RuntimeException e) { 324 collectors.remove(collector); 325 CollectorRegistration reg = collectorMetadata.remove(collector); 326 if (reg != null && reg.prometheusName != null) { 327 unregisterLabelSchema(reg.prometheusName, reg.labelNames); 328 } 329 throw e; 330 } 331 } 332 333 public void register(MultiCollector collector) { 334 if (!multiCollectors.add(collector)) { 335 throw new IllegalArgumentException("MultiCollector instance is already registered"); 336 } 337 List<MultiCollectorRegistration> registrations = new ArrayList<>(); 338 339 try { 340 for (MetricFamilyDescriptor descriptor : collector.getMetricFamilyDescriptors()) { 341 String prometheusName = descriptor.getPrometheusName(); 342 Set<String> normalizedLabels = immutableLabelNames(descriptor.getLabelNames()); 343 MetricMetadata metadata = descriptor.getMetadata(); 344 345 validateRegistration( 346 prometheusName, 347 descriptor.getType(), 348 normalizedLabels, 349 metadata.getHelp(), 350 metadata.getUnit()); 351 registrations.add(new MultiCollectorRegistration(prometheusName, normalizedLabels)); 352 } 353 354 multiCollectorMetadata.put(collector, registrations); 355 // Catch RuntimeException broadly because collector methods are user-implemented and could 356 // throw any RuntimeException. Ensures cleanup on failure. 357 } catch (RuntimeException e) { 358 multiCollectors.remove(collector); 359 for (MultiCollectorRegistration registration : registrations) { 360 unregisterLabelSchema(registration.prometheusName, registration.labelNames); 361 } 362 throw e; 363 } 364 } 365 366 public void unregister(Collector collector) { 367 collectors.remove(collector); 368 369 CollectorRegistration registration = collectorMetadata.remove(collector); 370 if (registration != null && registration.prometheusName != null) { 371 unregisterLabelSchema(registration.prometheusName, registration.labelNames); 372 } 373 } 374 375 public void unregister(MultiCollector collector) { 376 multiCollectors.remove(collector); 377 378 List<MultiCollectorRegistration> registrations = multiCollectorMetadata.remove(collector); 379 if (registrations != null) { 380 for (MultiCollectorRegistration registration : registrations) { 381 unregisterLabelSchema(registration.prometheusName, registration.labelNames); 382 } 383 } 384 } 385 386 /** 387 * Removes the label schema for the given metric name. If no label schemas remain for that name, 388 * removes the metric name entirely from the registry, including its exposition name reservations. 389 */ 390 private void unregisterLabelSchema(String prometheusName, Set<String> labelNames) { 391 registered.computeIfPresent( 392 prometheusName, 393 (name, info) -> { 394 info.removeLabelSet(labelNames); 395 if (info.isEmpty()) { 396 // Remove exposition name reservations for this metric. 397 Set<String> expositionNames = computeExpositionNames(prometheusName, info.getType()); 398 for (String expositionName : expositionNames) { 399 expositionNameOwners.remove(expositionName, prometheusName); 400 } 401 return null; 402 } 403 return info; 404 }); 405 } 406 407 public void clear() { 408 collectors.clear(); 409 multiCollectors.clear(); 410 registered.clear(); 411 collectorMetadata.clear(); 412 multiCollectorMetadata.clear(); 413 expositionNameOwners.clear(); 414 } 415 416 public MetricSnapshots scrape() { 417 return scrape((PrometheusScrapeRequest) null); 418 } 419 420 public MetricSnapshots scrape(@Nullable PrometheusScrapeRequest scrapeRequest) { 421 List<MetricSnapshot> allSnapshots = new ArrayList<>(); 422 for (Collector collector : collectors) { 423 MetricSnapshot snapshot = 424 scrapeRequest == null ? collector.collect() : collector.collect(scrapeRequest); 425 if (snapshot != null) { 426 allSnapshots.add(snapshot); 427 } 428 } 429 for (MultiCollector collector : multiCollectors) { 430 MetricSnapshots snapshots = 431 scrapeRequest == null ? collector.collect() : collector.collect(scrapeRequest); 432 for (MetricSnapshot snapshot : snapshots) { 433 allSnapshots.add(snapshot); 434 } 435 } 436 437 MetricSnapshots.Builder result = MetricSnapshots.builder(); 438 for (MetricSnapshot snapshot : allSnapshots) { 439 result.metricSnapshot(snapshot); 440 } 441 return result.build(); 442 } 443 444 public MetricSnapshots scrape(Predicate<String> includedNames) { 445 if (includedNames == null) { 446 return scrape(); 447 } 448 return scrape(includedNames, null); 449 } 450 451 public MetricSnapshots scrape( 452 Predicate<String> includedNames, @Nullable PrometheusScrapeRequest scrapeRequest) { 453 if (includedNames == null) { 454 return scrape(scrapeRequest); 455 } 456 List<MetricSnapshot> allSnapshots = new ArrayList<>(); 457 for (Collector collector : collectors) { 458 CollectorRegistration reg = collectorMetadata.get(collector); 459 String prometheusName = 460 reg != null ? reg.prometheusName : getDeprecatedPrometheusName(collector); 461 // prometheusName == null means the name is unknown, and we have to scrape to learn the name. 462 // prometheusName != null means we can skip the scrape if the name is excluded. 463 // Also test the original name (e.g. "events_total" for a counter named "events"). 464 String expositionName = reg != null ? reg.expositionBasePrometheusName : null; 465 if (prometheusName == null 466 || includedNames.test(prometheusName) 467 || (expositionName != null && includedNames.test(expositionName))) { 468 MetricSnapshot snapshot = 469 scrapeRequest == null 470 ? collector.collect(includedNames) 471 : collector.collect(includedNames, scrapeRequest); 472 if (snapshot != null) { 473 allSnapshots.add(snapshot); 474 } 475 } 476 } 477 for (MultiCollector collector : multiCollectors) { 478 List<MultiCollectorRegistration> registrations = multiCollectorMetadata.get(collector); 479 List<String> prometheusNames = getDeprecatedPrometheusNames(collector); 480 // empty prometheusNames means the names are unknown, and we have to scrape to learn the 481 // names. 482 // non-empty prometheusNames means we can exclude the collector if all names are excluded by 483 // the filter. 484 boolean excluded = 485 (registrations != null && !registrations.isEmpty()) || !prometheusNames.isEmpty(); 486 if (registrations != null) { 487 for (MultiCollectorRegistration registration : registrations) { 488 if (includedNames.test(registration.prometheusName)) { 489 excluded = false; 490 break; 491 } 492 } 493 } 494 for (String prometheusName : prometheusNames) { 495 if (includedNames.test(prometheusName)) { 496 excluded = false; 497 break; 498 } 499 } 500 if (!excluded) { 501 MetricSnapshots snapshots = 502 scrapeRequest == null 503 ? collector.collect(includedNames) 504 : collector.collect(includedNames, scrapeRequest); 505 for (MetricSnapshot snapshot : snapshots) { 506 if (snapshot != null) { 507 allSnapshots.add(snapshot); 508 } 509 } 510 } 511 } 512 513 MetricSnapshots.Builder result = MetricSnapshots.builder(); 514 for (MetricSnapshot snapshot : allSnapshots) { 515 result.metricSnapshot(snapshot); 516 } 517 return result.build(); 518 } 519}