001package io.prometheus.metrics.model.registry; 002 003import static java.util.Collections.emptySet; 004 005import io.prometheus.metrics.model.snapshots.MetricMetadata; 006import io.prometheus.metrics.model.snapshots.MetricSnapshot; 007import io.prometheus.metrics.model.snapshots.MetricSnapshots; 008import io.prometheus.metrics.model.snapshots.Unit; 009import java.util.ArrayList; 010import java.util.Collections; 011import java.util.HashSet; 012import java.util.List; 013import java.util.Objects; 014import java.util.Set; 015import java.util.concurrent.ConcurrentHashMap; 016import java.util.function.Predicate; 017import javax.annotation.Nullable; 018 019public class PrometheusRegistry { 020 021 public static final PrometheusRegistry defaultRegistry = new PrometheusRegistry(); 022 023 private final Set<Collector> collectors = ConcurrentHashMap.newKeySet(); 024 private final Set<MultiCollector> multiCollectors = ConcurrentHashMap.newKeySet(); 025 private final ConcurrentHashMap<String, RegistrationInfo> registered = new ConcurrentHashMap<>(); 026 private final ConcurrentHashMap<Collector, CollectorRegistration> collectorMetadata = 027 new ConcurrentHashMap<>(); 028 private final ConcurrentHashMap<MultiCollector, List<MultiCollectorRegistration>> 029 multiCollectorMetadata = new ConcurrentHashMap<>(); 030 031 /** Stores the registration details for a Collector at registration time. */ 032 private static class CollectorRegistration { 033 final String prometheusName; 034 final Set<String> labelNames; 035 036 CollectorRegistration(String prometheusName, @Nullable Set<String> labelNames) { 037 this.prometheusName = prometheusName; 038 this.labelNames = immutableLabelNames(labelNames); 039 } 040 } 041 042 /** 043 * Stores the registration details for a single metric within a MultiCollector. A MultiCollector 044 * can produce multiple metrics, so we need one of these per metric name. 045 */ 046 private static class MultiCollectorRegistration { 047 final String prometheusName; 048 final Set<String> labelNames; 049 050 MultiCollectorRegistration(String prometheusName, @Nullable Set<String> labelNames) { 051 this.prometheusName = prometheusName; 052 this.labelNames = immutableLabelNames(labelNames); 053 } 054 } 055 056 /** 057 * Tracks registration information for each metric name to enable validation of type consistency, 058 * label schema uniqueness, and help/unit consistency. 059 */ 060 private static class RegistrationInfo { 061 private final MetricType type; 062 private final Set<Set<String>> labelSchemas; 063 @Nullable private String help; 064 @Nullable private Unit unit; 065 066 private RegistrationInfo( 067 MetricType type, 068 Set<Set<String>> labelSchemas, 069 @Nullable String help, 070 @Nullable Unit unit) { 071 this.type = type; 072 this.labelSchemas = labelSchemas; 073 this.help = help; 074 this.unit = unit; 075 } 076 077 static RegistrationInfo of( 078 MetricType type, 079 @Nullable Set<String> labelNames, 080 @Nullable String help, 081 @Nullable Unit unit) { 082 Set<Set<String>> labelSchemas = ConcurrentHashMap.newKeySet(); 083 Set<String> normalized = 084 (labelNames == null || labelNames.isEmpty()) ? emptySet() : labelNames; 085 labelSchemas.add(normalized); 086 return new RegistrationInfo(type, labelSchemas, help, unit); 087 } 088 089 /** 090 * Validates that the given help and unit are exactly equal to this registration. Throws if 091 * values differ, including when one is null and the other is non-null. This ensures consistent 092 * metadata across all collectors sharing the same metric name. 093 */ 094 void validateMetadata(@Nullable String newHelp, @Nullable Unit newUnit) { 095 if (!Objects.equals(help, newHelp)) { 096 throw new IllegalArgumentException( 097 "Conflicting help strings. Existing: \"" + help + "\", new: \"" + newHelp + "\""); 098 } 099 if (!Objects.equals(unit, newUnit)) { 100 throw new IllegalArgumentException( 101 "Conflicting unit. Existing: " + unit + ", new: " + newUnit); 102 } 103 } 104 105 /** 106 * Adds a label schema to this registration. 107 * 108 * @param labelNames the label names to add (null or empty sets are normalized to empty set) 109 * @return true if the schema was added (new), false if it already existed 110 */ 111 boolean addLabelSet(@Nullable Set<String> labelNames) { 112 Set<String> normalized = 113 (labelNames == null || labelNames.isEmpty()) ? emptySet() : labelNames; 114 return labelSchemas.add(normalized); 115 } 116 117 /** 118 * Removes a label schema from this registration. 119 * 120 * @param labelNames the label names to remove (null or empty sets are normalized to empty set) 121 */ 122 void removeLabelSet(@Nullable Set<String> labelNames) { 123 Set<String> normalized = 124 (labelNames == null || labelNames.isEmpty()) ? emptySet() : labelNames; 125 labelSchemas.remove(normalized); 126 } 127 128 /** Returns true if all label schemas have been unregistered. */ 129 boolean isEmpty() { 130 return labelSchemas.isEmpty(); 131 } 132 133 MetricType getType() { 134 return type; 135 } 136 } 137 138 /** 139 * Returns an immutable set of label names for storage. Defends against mutation of the set 140 * returned by {@code Collector.getLabelNames()} after registration, which would break duplicate 141 * detection and unregistration. 142 */ 143 private static Set<String> immutableLabelNames(@Nullable Set<String> labelNames) { 144 if (labelNames == null || labelNames.isEmpty()) { 145 return emptySet(); 146 } 147 return Collections.unmodifiableSet(new HashSet<>(labelNames)); 148 } 149 150 /** 151 * Validates the registration of a metric with the given parameters. Ensures type consistency, 152 * label schema uniqueness, and help/unit consistency. 153 */ 154 private void validateRegistration( 155 String prometheusName, 156 MetricType metricType, 157 Set<String> normalizedLabels, 158 @Nullable String help, 159 @Nullable Unit unit) { 160 final MetricType type = metricType; 161 final Set<String> names = normalizedLabels; 162 final String helpForValidation = help; 163 final Unit unitForValidation = unit; 164 registered.compute( 165 prometheusName, 166 (n, existingInfo) -> { 167 if (existingInfo == null) { 168 return RegistrationInfo.of(type, names, helpForValidation, unitForValidation); 169 } else { 170 if (existingInfo.getType() != type) { 171 throw new IllegalArgumentException( 172 prometheusName 173 + ": Conflicting metric types. Existing: " 174 + existingInfo.getType() 175 + ", new: " 176 + type); 177 } 178 // Check label set first; only mutate help/unit after validation passes. 179 if (!existingInfo.addLabelSet(names)) { 180 throw new IllegalArgumentException( 181 prometheusName + ": duplicate metric name with identical label schema " + names); 182 } 183 // Roll back label schema if metadata validation fails 184 try { 185 existingInfo.validateMetadata(helpForValidation, unitForValidation); 186 } catch (IllegalArgumentException e) { 187 existingInfo.removeLabelSet(names); 188 throw e; 189 } 190 return existingInfo; 191 } 192 }); 193 } 194 195 public void register(Collector collector) { 196 if (!collectors.add(collector)) { 197 throw new IllegalArgumentException("Collector instance is already registered"); 198 } 199 try { 200 String prometheusName = collector.getPrometheusName(); 201 MetricType metricType = collector.getMetricType(); 202 Set<String> normalizedLabels = immutableLabelNames(collector.getLabelNames()); 203 MetricMetadata metadata = collector.getMetadata(); 204 String help = metadata != null ? metadata.getHelp() : null; 205 Unit unit = metadata != null ? metadata.getUnit() : null; 206 207 // Only perform validation if collector provides sufficient metadata. 208 // Collectors that don't implement getPrometheusName()/getMetricType() will skip validation. 209 if (prometheusName != null && metricType != null) { 210 validateRegistration(prometheusName, metricType, normalizedLabels, help, unit); 211 collectorMetadata.put( 212 collector, new CollectorRegistration(prometheusName, normalizedLabels)); 213 } 214 // Catch RuntimeException broadly because collector methods (getPrometheusName, getMetricType, 215 // etc.) are user-implemented and could throw any RuntimeException. Ensures cleanup on 216 // failure. 217 } catch (RuntimeException e) { 218 collectors.remove(collector); 219 CollectorRegistration reg = collectorMetadata.remove(collector); 220 if (reg != null && reg.prometheusName != null) { 221 unregisterLabelSchema(reg.prometheusName, reg.labelNames); 222 } 223 throw e; 224 } 225 } 226 227 public void register(MultiCollector collector) { 228 if (!multiCollectors.add(collector)) { 229 throw new IllegalArgumentException("MultiCollector instance is already registered"); 230 } 231 List<String> prometheusNamesList = collector.getPrometheusNames(); 232 List<MultiCollectorRegistration> registrations = new ArrayList<>(); 233 234 try { 235 for (String prometheusName : prometheusNamesList) { 236 MetricType metricType = collector.getMetricType(prometheusName); 237 Set<String> normalizedLabels = immutableLabelNames(collector.getLabelNames(prometheusName)); 238 MetricMetadata metadata = collector.getMetadata(prometheusName); 239 String help = metadata != null ? metadata.getHelp() : null; 240 Unit unit = metadata != null ? metadata.getUnit() : null; 241 242 if (metricType != null) { 243 validateRegistration(prometheusName, metricType, normalizedLabels, help, unit); 244 registrations.add(new MultiCollectorRegistration(prometheusName, normalizedLabels)); 245 } 246 } 247 248 multiCollectorMetadata.put(collector, registrations); 249 // Catch RuntimeException broadly because collector methods (getPrometheusNames, 250 // getMetricType, etc.) are user-implemented and could throw any RuntimeException. 251 // Ensures cleanup on failure. 252 } catch (RuntimeException e) { 253 multiCollectors.remove(collector); 254 for (MultiCollectorRegistration registration : registrations) { 255 unregisterLabelSchema(registration.prometheusName, registration.labelNames); 256 } 257 throw e; 258 } 259 } 260 261 public void unregister(Collector collector) { 262 collectors.remove(collector); 263 264 CollectorRegistration registration = collectorMetadata.remove(collector); 265 if (registration != null && registration.prometheusName != null) { 266 unregisterLabelSchema(registration.prometheusName, registration.labelNames); 267 } 268 } 269 270 public void unregister(MultiCollector collector) { 271 multiCollectors.remove(collector); 272 273 List<MultiCollectorRegistration> registrations = multiCollectorMetadata.remove(collector); 274 if (registrations != null) { 275 for (MultiCollectorRegistration registration : registrations) { 276 unregisterLabelSchema(registration.prometheusName, registration.labelNames); 277 } 278 } 279 } 280 281 /** 282 * Removes the label schema for the given metric name. If no label schemas remain for that name, 283 * removes the metric name entirely from the registry. 284 */ 285 private void unregisterLabelSchema(String prometheusName, Set<String> labelNames) { 286 registered.computeIfPresent( 287 prometheusName, 288 (name, info) -> { 289 info.removeLabelSet(labelNames); 290 if (info.isEmpty()) { 291 return null; 292 } 293 return info; 294 }); 295 } 296 297 public void clear() { 298 collectors.clear(); 299 multiCollectors.clear(); 300 registered.clear(); 301 collectorMetadata.clear(); 302 multiCollectorMetadata.clear(); 303 } 304 305 public MetricSnapshots scrape() { 306 return scrape((PrometheusScrapeRequest) null); 307 } 308 309 public MetricSnapshots scrape(@Nullable PrometheusScrapeRequest scrapeRequest) { 310 List<MetricSnapshot> allSnapshots = new ArrayList<>(); 311 for (Collector collector : collectors) { 312 MetricSnapshot snapshot = 313 scrapeRequest == null ? collector.collect() : collector.collect(scrapeRequest); 314 if (snapshot != null) { 315 allSnapshots.add(snapshot); 316 } 317 } 318 for (MultiCollector collector : multiCollectors) { 319 MetricSnapshots snapshots = 320 scrapeRequest == null ? collector.collect() : collector.collect(scrapeRequest); 321 for (MetricSnapshot snapshot : snapshots) { 322 allSnapshots.add(snapshot); 323 } 324 } 325 326 MetricSnapshots.Builder result = MetricSnapshots.builder(); 327 for (MetricSnapshot snapshot : allSnapshots) { 328 result.metricSnapshot(snapshot); 329 } 330 return result.build(); 331 } 332 333 public MetricSnapshots scrape(Predicate<String> includedNames) { 334 if (includedNames == null) { 335 return scrape(); 336 } 337 return scrape(includedNames, null); 338 } 339 340 public MetricSnapshots scrape( 341 Predicate<String> includedNames, @Nullable PrometheusScrapeRequest scrapeRequest) { 342 if (includedNames == null) { 343 return scrape(scrapeRequest); 344 } 345 List<MetricSnapshot> allSnapshots = new ArrayList<>(); 346 for (Collector collector : collectors) { 347 String prometheusName = collector.getPrometheusName(); 348 // prometheusName == null means the name is unknown, and we have to scrape to learn the name. 349 // prometheusName != null means we can skip the scrape if the name is excluded. 350 if (prometheusName == null || includedNames.test(prometheusName)) { 351 MetricSnapshot snapshot = 352 scrapeRequest == null 353 ? collector.collect(includedNames) 354 : collector.collect(includedNames, scrapeRequest); 355 if (snapshot != null) { 356 allSnapshots.add(snapshot); 357 } 358 } 359 } 360 for (MultiCollector collector : multiCollectors) { 361 List<String> prometheusNames = collector.getPrometheusNames(); 362 // empty prometheusNames means the names are unknown, and we have to scrape to learn the 363 // names. 364 // non-empty prometheusNames means we can exclude the collector if all names are excluded by 365 // the filter. 366 boolean excluded = !prometheusNames.isEmpty(); 367 for (String prometheusName : prometheusNames) { 368 if (includedNames.test(prometheusName)) { 369 excluded = false; 370 break; 371 } 372 } 373 if (!excluded) { 374 MetricSnapshots snapshots = 375 scrapeRequest == null 376 ? collector.collect(includedNames) 377 : collector.collect(includedNames, scrapeRequest); 378 for (MetricSnapshot snapshot : snapshots) { 379 if (snapshot != null) { 380 allSnapshots.add(snapshot); 381 } 382 } 383 } 384 } 385 386 MetricSnapshots.Builder result = MetricSnapshots.builder(); 387 for (MetricSnapshot snapshot : allSnapshots) { 388 result.metricSnapshot(snapshot); 389 } 390 return result.build(); 391 } 392}