diff --git a/docs/content/getting-started/registry.md b/docs/content/getting-started/registry.md index 7f561ecef..c7a38e457 100644 --- a/docs/content/getting-started/registry.md +++ b/docs/content/getting-started/registry.md @@ -96,6 +96,27 @@ and full label-schema validation and duplicate detection still apply. A collecto non-null type but leaves `getLabelNames()` as `null` is still validated, with its labels treated as empty. +## Filtering Metrics + +You can set a registry-level metric name filter that applies to all scrape operations. +Only metrics whose names match the filter predicate will be included in scrape results: + +```java +PrometheusRegistry.defaultRegistry.setMetricFilter(name -> !name.startsWith("debug_")); +``` + +The registry filter is AND-combined with any scrape-time `includedNames` predicate passed to +`scrape(Predicate)`. For example, if the registry filter allows `counter_*` and the scrape-time +filter allows `counter_a`, only `counter_a` will be included. + +To remove the filter, set it to `null`: + +```java +PrometheusRegistry.defaultRegistry.setMetricFilter(null); +``` + +Note that `clear()` does not reset the metric filter -- it only removes registered collectors. + ## Unregistering a Metric There is no automatic expiry of unused metrics (yet), once a metric is registered it will remain diff --git a/mise.toml b/mise.toml index 8ea1d673e..d27a0e469 100644 --- a/mise.toml +++ b/mise.toml @@ -37,7 +37,8 @@ env.PROTO_GENERATION = "true" [tasks.test] description = "run unit tests, ignoring formatting and linters" -run = "./mvnw test -Dspotless.check.skip=true -Dcoverage.skip=true -Dcheckstyle.skip=true -Dwarnings=-nowarn" +usage = 'arg "[args]" var=#true help="Extra Maven arguments (e.g. -pl prometheus-metrics-model)"' +run = "./mvnw test -Dspotless.check.skip=true -Dcoverage.skip=true -Dcheckstyle.skip=true -Dwarnings=-nowarn ${usage_args:-}" [tasks.test-all] description = "run all tests" diff --git a/prometheus-metrics-model/src/main/java/io/prometheus/metrics/model/registry/PrometheusRegistry.java b/prometheus-metrics-model/src/main/java/io/prometheus/metrics/model/registry/PrometheusRegistry.java index f66824972..625523a8b 100644 --- a/prometheus-metrics-model/src/main/java/io/prometheus/metrics/model/registry/PrometheusRegistry.java +++ b/prometheus-metrics-model/src/main/java/io/prometheus/metrics/model/registry/PrometheusRegistry.java @@ -27,6 +27,7 @@ public class PrometheusRegistry { new ConcurrentHashMap<>(); private final ConcurrentHashMap> multiCollectorMetadata = new ConcurrentHashMap<>(); + private @Nullable Predicate metricFilter; /** Stores the registration details for a Collector at registration time. */ private static class CollectorRegistration { @@ -302,11 +303,44 @@ public void clear() { multiCollectorMetadata.clear(); } + /** + * Sets a registry-level metric name filter. When set, only metrics whose names match the + * predicate will be included in scrape results. This filter is AND-combined with any scrape-time + * {@code includedNames} predicate. + * + * @param metricFilter the filter predicate, or {@code null} to remove the filter + */ + public void setMetricFilter(@Nullable Predicate metricFilter) { + this.metricFilter = metricFilter; + } + + /** Returns the current registry-level metric name filter, or {@code null} if none is set. */ + @Nullable + public Predicate getMetricFilter() { + return metricFilter; + } + + @Nullable + private Predicate effectiveFilter(@Nullable Predicate includedNames) { + Predicate registryFilter = this.metricFilter; + if (registryFilter != null && includedNames != null) { + return registryFilter.and(includedNames); + } else if (registryFilter != null) { + return registryFilter; + } else { + return includedNames; + } + } + public MetricSnapshots scrape() { return scrape((PrometheusScrapeRequest) null); } public MetricSnapshots scrape(@Nullable PrometheusScrapeRequest scrapeRequest) { + Predicate filter = effectiveFilter(null); + if (filter != null) { + return scrape(filter, scrapeRequest); + } List allSnapshots = new ArrayList<>(); for (Collector collector : collectors) { MetricSnapshot snapshot = @@ -331,15 +365,17 @@ public MetricSnapshots scrape(@Nullable PrometheusScrapeRequest scrapeRequest) { } public MetricSnapshots scrape(Predicate includedNames) { - if (includedNames == null) { - return scrape(); + Predicate filter = effectiveFilter(includedNames); + if (filter == null) { + return scrape((PrometheusScrapeRequest) null); } - return scrape(includedNames, null); + return scrape(filter, null); } public MetricSnapshots scrape( Predicate includedNames, @Nullable PrometheusScrapeRequest scrapeRequest) { - if (includedNames == null) { + Predicate filter = effectiveFilter(includedNames); + if (filter == null) { return scrape(scrapeRequest); } List allSnapshots = new ArrayList<>(); @@ -347,11 +383,11 @@ public MetricSnapshots scrape( String prometheusName = collector.getPrometheusName(); // prometheusName == null means the name is unknown, and we have to scrape to learn the name. // prometheusName != null means we can skip the scrape if the name is excluded. - if (prometheusName == null || includedNames.test(prometheusName)) { + if (prometheusName == null || filter.test(prometheusName)) { MetricSnapshot snapshot = scrapeRequest == null - ? collector.collect(includedNames) - : collector.collect(includedNames, scrapeRequest); + ? collector.collect(filter) + : collector.collect(filter, scrapeRequest); if (snapshot != null) { allSnapshots.add(snapshot); } @@ -365,7 +401,7 @@ public MetricSnapshots scrape( // the filter. boolean excluded = !prometheusNames.isEmpty(); for (String prometheusName : prometheusNames) { - if (includedNames.test(prometheusName)) { + if (filter.test(prometheusName)) { excluded = false; break; } @@ -373,8 +409,8 @@ public MetricSnapshots scrape( if (!excluded) { MetricSnapshots snapshots = scrapeRequest == null - ? collector.collect(includedNames) - : collector.collect(includedNames, scrapeRequest); + ? collector.collect(filter) + : collector.collect(filter, scrapeRequest); for (MetricSnapshot snapshot : snapshots) { if (snapshot != null) { allSnapshots.add(snapshot); diff --git a/prometheus-metrics-model/src/test/java/io/prometheus/metrics/model/registry/PrometheusRegistryTest.java b/prometheus-metrics-model/src/test/java/io/prometheus/metrics/model/registry/PrometheusRegistryTest.java index 90a04934e..1c31f8ee4 100644 --- a/prometheus-metrics-model/src/test/java/io/prometheus/metrics/model/registry/PrometheusRegistryTest.java +++ b/prometheus-metrics-model/src/test/java/io/prometheus/metrics/model/registry/PrometheusRegistryTest.java @@ -315,7 +315,7 @@ public Set getLabelNames() { .hasMessageContaining("duplicate metric name with identical label schema"); // Only the first collector should be in the registry; counter2 was removed on rollback. - assertThat(registry.scrape().size()).isEqualTo(1); + assertThat(registry.scrape().size()).isOne(); } @Test @@ -999,12 +999,12 @@ public Set getLabelNames() { // Unregister first collector - name should still be registered registry.unregister(counter1); MetricSnapshots snapshots = registry.scrape(); - assertThat(snapshots.size()).isEqualTo(1); + assertThat(snapshots.size()).isOne(); // Unregister second collector - name should be removed registry.unregister(counter2); snapshots = registry.scrape(); - assertThat(snapshots.size()).isEqualTo(0); + assertThat(snapshots.size()).isZero(); // Should be able to register again with same name assertThatCode(() -> registry.register(counter1)).doesNotThrowAnyException(); @@ -1038,7 +1038,7 @@ public MetricType getMetricType(String prometheusName) { assertThat(registry.scrape().size()).isEqualTo(2); registry.unregister(multi); - assertThat(registry.scrape().size()).isEqualTo(0); + assertThat(registry.scrape().size()).isZero(); // Should be able to register collectors with same names again Collector counter = @@ -1062,6 +1062,98 @@ public MetricType getMetricType() { assertThatCode(() -> registry.register(counter)).doesNotThrowAnyException(); } + @Test + void metricFilter_filtersOnScrape() { + PrometheusRegistry registry = new PrometheusRegistry(); + registry.register(counterA1); + registry.register(counterB); + registry.register(gaugeA); + + registry.setMetricFilter(name -> name.startsWith("counter")); + + MetricSnapshots snapshots = registry.scrape(); + assertThat(snapshots.size()).isEqualTo(2); + } + + @Test + void metricFilter_combinedWithScrapeTimeFilter() { + PrometheusRegistry registry = new PrometheusRegistry(); + registry.register(counterA1); + registry.register(counterB); + registry.register(gaugeA); + + registry.setMetricFilter(name -> name.startsWith("counter")); + MetricSnapshots snapshots = registry.scrape(name -> name.equals("counter_a")); + assertThat(snapshots.size()).isOne(); + } + + @Test + void metricFilter_nullClearsFilter() { + PrometheusRegistry registry = new PrometheusRegistry(); + registry.register(counterA1); + registry.register(counterB); + registry.register(gaugeA); + + registry.setMetricFilter(name -> name.startsWith("counter")); + assertThat(registry.scrape().size()).isEqualTo(2); + + registry.setMetricFilter(null); + assertThat(registry.scrape().size()).isEqualTo(3); + } + + @Test + void metricFilter_appliedToScrapeWithScrapeRequest() { + PrometheusRegistry registry = new PrometheusRegistry(); + registry.register(counterA1); + registry.register(counterB); + registry.register(gaugeA); + + registry.setMetricFilter(name -> name.startsWith("counter")); + + MetricSnapshots snapshots = registry.scrape((PrometheusScrapeRequest) null); + assertThat(snapshots.size()).isEqualTo(2); + } + + @Test + void metricFilter_appliedToMultiCollector() { + PrometheusRegistry registry = new PrometheusRegistry(); + registry.register(counterA1); + registry.register(multiCollector); + + registry.setMetricFilter(name -> name.equals("counter_a")); + + MetricSnapshots snapshots = registry.scrape(); + assertThat(snapshots.size()).isOne(); + } + + @Test + void metricFilter_noNameCollector_alwaysScraped() { + PrometheusRegistry registry = new PrometheusRegistry(); + registry.register(noName); + registry.register(counterA1); + + // Filter matches both "counter_a" and "no_name_gauge" (the snapshot name of noName). + // noName has null getPrometheusName(), so the registry always calls collect(filter) on it. + // The collector's own collect(Predicate) then tests the snapshot name against the filter. + registry.setMetricFilter(name -> name.equals("counter_a") || name.equals("no_name_gauge")); + + MetricSnapshots snapshots = registry.scrape(); + assertThat(snapshots.size()).isEqualTo(2); + } + + @Test + void metricFilter_excludesAll() { + PrometheusRegistry registry = new PrometheusRegistry(); + registry.register(counterA1); + registry.register(counterB); + registry.register(gaugeA); + + registry.setMetricFilter(name -> false); + + MetricSnapshots snapshots = registry.scrape(); + assertThat(snapshots.size()).isZero(); + } + @Test void unregister_legacyCollector_noErrors() { PrometheusRegistry registry = new PrometheusRegistry(); @@ -1081,10 +1173,10 @@ public String getPrometheusName() { }; registry.register(legacy); - assertThat(registry.scrape().size()).isEqualTo(1); + assertThat(registry.scrape().size()).isOne(); // Unregister should work without errors even for legacy collectors assertThatCode(() -> registry.unregister(legacy)).doesNotThrowAnyException(); - assertThat(registry.scrape().size()).isEqualTo(0); + assertThat(registry.scrape().size()).isZero(); } }