Skip to main content

prometheus_filtered/
lib.rs

1// Copyright (c) 2026 IOTA Stiftung
2// SPDX-License-Identifier: Apache-2.0
3
4//! Drop-in replacement for the `prometheus` crate with optional per-metric
5//! filtering.
6//!
7//! Replace `use prometheus::*` with `use prometheus_filtered::*` to control
8//! which metrics are exposed. The active filter combines the node config's
9//! directives with the `METRICS_FILTER` environment variable's; where both
10//! match the same metric, the env var wins.
11//!
12//! Filter syntax: comma-separated `pattern=LEVEL` directives, last-match
13//! wins, where `LEVEL` is one of `off`, `warn`, `info`, `debug`, `trace`.
14//! A bare `LEVEL` token (no `pattern=`) sets the global default. A pattern
15//! matches if it is a prefix of the metric name OR is a component/prefix of the
16//! calling module path (e.g. `traffic_controller` matches
17//! `iota_core::traffic_controller::metrics`).
18//!
19//! Examples:
20//! - `METRICS_FILTER=off,authority=warn`
21//! - `METRICS_FILTER=authority=off`
22//!
23//! The directives act as **exposure**
24//! thresholds deciding which metrics [`Registry::gather`] includes in its
25//! output (`off` exposes none of the matched metrics). Metrics matched by no
26//! directive are exposed unconditionally, so with no filter configured the
27//! crate behaves exactly like plain `prometheus`; use a bare `LEVEL` directive
28//! to set a stricter global default.
29
30use std::{
31    collections::HashMap,
32    sync::{Arc, OnceLock, RwLock},
33};
34
35/// Re-exported under a hidden alias so `$crate::prometheus::xxx!` works
36/// inside `#[macro_export]` macros without requiring callers to depend
37/// directly on the `prometheus` crate.
38#[doc(hidden)]
39pub use prometheus;
40// Re-export prometheus primitives that require no wrapping.
41pub use prometheus::{
42    DEFAULT_BUCKETS, Encoder, Error, HistogramOpts, Opts, PROTOBUF_FORMAT, ProtobufEncoder, Result,
43    TextEncoder, exponential_buckets, gather, histogram_opts, linear_buckets, opts, proto,
44};
45use tracing::warn;
46
47// ---------------------------------------------------------------------------
48// core sub-module
49// ---------------------------------------------------------------------------
50
51/// Mirrors `prometheus::core` and provides `GenericGauge`/`GenericCounter`
52/// wrappers compatible with prometheus's own generic types.
53///
54/// `crate::IntGauge`, `crate::Gauge`, `crate::IntCounter`, and
55/// `crate::Counter` are type aliases for concrete instantiations of these
56/// types, so `Option<IntGauge>` and `Option<GenericGauge<AtomicI64>>` are
57/// the same type.
58pub mod core {
59    use std::mem::ManuallyDrop;
60
61    pub use prometheus::core::{
62        Atomic, AtomicF64, AtomicI64, AtomicU64, Collector, Desc, Describer, Metric,
63        MetricVecBuilder, Number,
64    };
65
66    macro_rules! impl_generic_metric_traits {
67        ($T:ident) => {
68            impl<P: Atomic> Clone for $T<P> {
69                fn clone(&self) -> Self {
70                    Self(self.0.clone())
71                }
72            }
73
74            impl<P: Atomic> std::fmt::Debug for $T<P> {
75                fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
76                    write!(f, "{}", stringify!($T))?;
77                    if self.0.is_none() {
78                        write!(f, "(disabled)")?;
79                    }
80                    Ok(())
81                }
82            }
83
84            impl<P: Atomic> prometheus::core::Collector for $T<P> {
85                fn desc(&self) -> Vec<&Desc> {
86                    self.0
87                        .as_ref()
88                        .map(|inner| inner.desc())
89                        .unwrap_or_default()
90                }
91
92                fn collect(&self) -> Vec<prometheus::proto::MetricFamily> {
93                    self.0
94                        .as_ref()
95                        .map(|inner| inner.collect())
96                        .unwrap_or_default()
97                }
98            }
99        };
100    }
101
102    macro_rules! impl_metric_traits {
103        ($T:ident) => {
104            impl Clone for $T {
105                fn clone(&self) -> Self {
106                    Self(self.0.clone())
107                }
108            }
109
110            impl std::fmt::Debug for $T {
111                fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
112                    write!(f, "{}", stringify!($T))?;
113                    if self.0.is_none() {
114                        write!(f, "(disabled)")?;
115                    }
116                    Ok(())
117                }
118            }
119
120            impl prometheus::core::Collector for $T {
121                fn desc(&self) -> Vec<&Desc> {
122                    self.0
123                        .as_ref()
124                        .map(|inner| inner.desc())
125                        .unwrap_or_default()
126                }
127
128                fn collect(&self) -> Vec<prometheus::proto::MetricFamily> {
129                    self.0
130                        .as_ref()
131                        .map(|inner| inner.collect())
132                        .unwrap_or_default()
133                }
134            }
135        };
136    }
137
138    macro_rules! impl_generic_metric_vec {
139        ($T:ident, $M:ident) => {
140            impl<P: Atomic> $T<P> {
141                pub fn new_some(inner: prometheus::core::$T<P>) -> Self {
142                    Self(Some(inner))
143                }
144
145                pub fn new_none() -> Self {
146                    Self(None)
147                }
148
149                #[inline]
150                pub fn with_label_values<V>(&self, vals: &[V]) -> $M<P>
151                where
152                    V: AsRef<str> + std::fmt::Debug,
153                {
154                    $M::<P>(self.0.as_ref().map(|inner| inner.with_label_values(vals)))
155                }
156
157                #[inline]
158                pub fn remove_label_values<V>(&self, vals: &[V]) -> super::Result<()>
159                where
160                    V: AsRef<str> + std::fmt::Debug,
161                {
162                    self.0
163                        .as_ref()
164                        .map(|inner| inner.remove_label_values(vals))
165                        .unwrap_or(Ok(()))
166                }
167
168                #[inline]
169                pub fn get_metric_with<V, S: std::hash::BuildHasher>(
170                    &self,
171                    labels: &std::collections::HashMap<&str, V, S>,
172                ) -> super::Result<$M<P>>
173                where
174                    V: AsRef<str> + std::fmt::Debug,
175                {
176                    self.0
177                        .as_ref()
178                        .map(|inner| inner.get_metric_with(labels).map($M::<P>::new_some))
179                        .unwrap_or(Ok($M::<P>::new_none()))
180                }
181
182                #[inline]
183                pub fn get_metric_with_label_values<V>(&self, vals: &[V]) -> super::Result<$M<P>>
184                where
185                    V: AsRef<str> + std::fmt::Debug,
186                {
187                    self.0
188                        .as_ref()
189                        .map(|inner| {
190                            inner
191                                .get_metric_with_label_values(vals)
192                                .map($M::<P>::new_some)
193                        })
194                        .unwrap_or(Ok($M::<P>::new_none()))
195                }
196
197                #[inline]
198                pub fn reset(&self) {
199                    if let Some(v) = &self.0 {
200                        v.reset();
201                    }
202                }
203            }
204        };
205    }
206
207    pub struct GenericCounter<P: Atomic>(Option<prometheus::core::GenericCounter<P>>);
208
209    impl<P: Atomic> GenericCounter<P> {
210        pub fn new_some(inner: prometheus::core::GenericCounter<P>) -> Self {
211            Self(Some(inner))
212        }
213
214        pub fn new_none() -> Self {
215            Self(None)
216        }
217
218        pub fn new(name: &str, help: &str) -> prometheus::Result<Self> {
219            prometheus::core::GenericCounter::new(name, help).map(Self::new_some)
220        }
221
222        pub fn with_opts(opts: super::Opts) -> super::Result<Self> {
223            prometheus::core::GenericCounter::with_opts(opts).map(Self::new_some)
224        }
225
226        #[inline]
227        pub fn get(&self) -> P::T {
228            self.0
229                .as_ref()
230                .map(|inner| inner.get())
231                .unwrap_or(<P::T>::from_i64(0))
232        }
233
234        #[inline]
235        pub fn inc(&self) {
236            if let Some(inner) = &self.0 {
237                inner.inc();
238            }
239        }
240
241        #[inline]
242        pub fn inc_by(&self, v: <P as Atomic>::T) {
243            if let Some(inner) = &self.0 {
244                inner.inc_by(v);
245            }
246        }
247
248        #[inline]
249        pub fn reset(&self) {
250            if let Some(inner) = &self.0 {
251                inner.reset();
252            }
253        }
254    }
255
256    impl_generic_metric_traits!(GenericCounter);
257
258    pub struct GenericGauge<P: Atomic>(Option<prometheus::core::GenericGauge<P>>);
259
260    impl<P: Atomic> GenericGauge<P> {
261        pub fn new_some(inner: prometheus::core::GenericGauge<P>) -> Self {
262            Self(Some(inner))
263        }
264
265        pub fn new_none() -> Self {
266            Self(None)
267        }
268
269        pub fn new(name: &str, help: &str) -> super::Result<Self> {
270            prometheus::core::GenericGauge::new(name, help).map(Self::new_some)
271        }
272
273        pub fn with_opts(opts: super::Opts) -> super::Result<Self> {
274            prometheus::core::GenericGauge::with_opts(opts).map(Self::new_some)
275        }
276
277        #[inline]
278        pub fn get(&self) -> P::T {
279            self.0
280                .as_ref()
281                .map(|inner| inner.get())
282                .unwrap_or(<P::T>::from_i64(0))
283        }
284
285        #[inline]
286        pub fn set(&self, v: P::T) {
287            if let Some(inner) = &self.0 {
288                inner.set(v);
289            }
290        }
291
292        #[inline]
293        pub fn inc(&self) {
294            if let Some(inner) = &self.0 {
295                inner.inc();
296            }
297        }
298
299        #[inline]
300        pub fn dec(&self) {
301            if let Some(inner) = &self.0 {
302                inner.dec();
303            }
304        }
305
306        #[inline]
307        pub fn add(&self, v: P::T) {
308            if let Some(inner) = &self.0 {
309                inner.add(v);
310            }
311        }
312
313        #[inline]
314        pub fn sub(&self, v: P::T) {
315            if let Some(inner) = &self.0 {
316                inner.sub(v);
317            }
318        }
319    }
320
321    impl_generic_metric_traits!(GenericGauge);
322
323    pub struct GenericCounterVec<P: Atomic>(Option<prometheus::core::GenericCounterVec<P>>);
324
325    impl_generic_metric_traits!(GenericCounterVec);
326    impl_generic_metric_vec!(GenericCounterVec, GenericCounter);
327
328    pub struct GenericGaugeVec<P: Atomic>(Option<prometheus::core::GenericGaugeVec<P>>);
329
330    impl_generic_metric_traits!(GenericGaugeVec);
331    impl_generic_metric_vec!(GenericGaugeVec, GenericGauge);
332
333    pub struct Histogram(Option<prometheus::Histogram>);
334
335    impl_metric_traits!(Histogram);
336
337    impl Histogram {
338        pub fn new_some(inner: prometheus::Histogram) -> Self {
339            Self(Some(inner))
340        }
341
342        pub fn new_none() -> Self {
343            Self(None)
344        }
345
346        pub fn with_opts(opts: prometheus::HistogramOpts) -> prometheus::Result<Self> {
347            prometheus::Histogram::with_opts(opts).map(|h| Self(Some(h)))
348        }
349
350        #[inline]
351        pub fn observe(&self, v: f64) {
352            if let Some(h) = &self.0 {
353                h.observe(v);
354            }
355        }
356
357        #[inline]
358        pub fn start_timer(&self) -> HistogramTimer {
359            HistogramTimer(self.0.as_ref().map(|h| h.start_timer()))
360        }
361
362        #[inline]
363        pub fn get_sample_count(&self) -> u64 {
364            self.0.as_ref().map_or(0, |h| h.get_sample_count())
365        }
366
367        #[inline]
368        pub fn get_sample_sum(&self) -> f64 {
369            self.0.as_ref().map_or(0.0, |h| h.get_sample_sum())
370        }
371    }
372
373    pub struct HistogramTimer(Option<prometheus::HistogramTimer>);
374
375    impl std::fmt::Debug for HistogramTimer {
376        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
377            write!(f, "HistogramTimer")?;
378            if self.0.is_none() {
379                write!(f, "(disabled)")?;
380            }
381            Ok(())
382        }
383    }
384
385    impl Drop for HistogramTimer {
386        fn drop(&mut self) {
387            // Dropping the inner prometheus::HistogramTimer records the observation.
388            drop(self.0.take());
389        }
390    }
391
392    impl HistogramTimer {
393        /// Records the elapsed time and returns it; prevents the `Drop` impl
394        /// from recording a second time.
395        #[inline]
396        pub fn stop_and_record(self) -> f64 {
397            // ManuallyDrop prevents our Drop impl from running, so the inner timer
398            // can be consumed by its own stop_and_record without double-recording.
399            let mut wrapper = ManuallyDrop::new(self);
400            wrapper
401                .0
402                .take()
403                .map(|t| t.stop_and_record())
404                .unwrap_or_default()
405        }
406
407        /// Records the duration; provided for compatibility with older
408        /// prometheus APIs.
409        #[inline]
410        pub fn observe_duration(self) {
411            let _ = self.stop_and_record();
412        }
413
414        /// Discards the timer without recording; returns the elapsed seconds.
415        #[inline]
416        pub fn stop_and_discard(self) -> f64 {
417            let mut wrapper = ManuallyDrop::new(self);
418            wrapper
419                .0
420                .take()
421                .map(|t| t.stop_and_discard())
422                .unwrap_or_default()
423        }
424    }
425
426    pub struct HistogramVec(Option<prometheus::HistogramVec>);
427
428    impl_metric_traits!(HistogramVec);
429
430    impl HistogramVec {
431        pub fn new_some(inner: prometheus::HistogramVec) -> Self {
432            Self(Some(inner))
433        }
434
435        pub fn new_none() -> Self {
436            Self(None)
437        }
438
439        #[inline]
440        pub fn with_label_values(&self, vals: &[&str]) -> Histogram {
441            Histogram(self.0.as_ref().map(|v| v.with_label_values(vals)))
442        }
443
444        #[inline]
445        pub fn remove_label_values(&self, vals: &[&str]) -> prometheus::Result<()> {
446            match &self.0 {
447                Some(v) => v.remove_label_values(vals),
448                None => Ok(()),
449            }
450        }
451    }
452}
453
454pub type Counter = core::GenericCounter<prometheus::core::AtomicF64>;
455pub type IntCounter = core::GenericCounter<prometheus::core::AtomicU64>;
456pub type Gauge = core::GenericGauge<prometheus::core::AtomicF64>;
457pub type IntGauge = core::GenericGauge<prometheus::core::AtomicI64>;
458
459pub type CounterVec = core::GenericCounterVec<prometheus::core::AtomicF64>;
460pub type IntCounterVec = core::GenericCounterVec<prometheus::core::AtomicU64>;
461pub type GaugeVec = core::GenericGaugeVec<prometheus::core::AtomicF64>;
462pub type IntGaugeVec = core::GenericGaugeVec<prometheus::core::AtomicI64>;
463
464pub use core::{Histogram, HistogramTimer, HistogramVec};
465
466// ---------------------------------------------------------------------------
467// Filter
468// ---------------------------------------------------------------------------
469
470/// Verbosity level for a metric.
471#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
472#[serde(rename_all = "lowercase")]
473pub enum MetricLevel {
474    /// As a filter threshold: expose none of the matched metrics. Not
475    /// meaningful as a per-metric level — tag metrics `Warn`..`Trace`.
476    Off,
477    Warn,
478    Info,
479    // The default for an untagged metric.
480    #[default]
481    Debug,
482    Trace,
483}
484
485impl MetricLevel {
486    pub(crate) const fn verbosity(self) -> u8 {
487        match self {
488            Self::Off => 0,
489            Self::Warn => 1,
490            Self::Info => 2,
491            Self::Debug => 3,
492            Self::Trace => 4,
493        }
494    }
495}
496
497/// Default threshold when no directive matches a metric: expose it. Filtering
498/// is opt-in, so an unfiltered registry behaves like plain `prometheus`.
499const DEFAULT_THRESHOLD: u8 = MetricLevel::Trace.verbosity();
500
501#[derive(Clone)]
502struct FilterDirective {
503    /// Empty string means global catch-all.
504    pattern: String,
505    /// Metrics matched by this directive are exposed iff their verbosity is
506    /// `<= threshold`. `off=0`, `warn=1`, `info=2`, `debug=3`, `trace=4`.
507    threshold: u8,
508}
509
510/// Parses and evaluates `METRICS_FILTER`-style directives.
511///
512/// Filter string: comma-separated `pattern=LEVEL` directives, last-match
513/// wins, a metric is exposed when its own level is at or below the threshold.
514#[derive(Default)]
515pub struct Filter {
516    directives: Vec<FilterDirective>,
517}
518
519/// Parses one `pattern=LEVEL` directive. `None` for an empty segment or an
520/// invalid level (dropped with a warning).
521fn parse_directive(part: &str) -> Option<FilterDirective> {
522    let part = part.trim();
523    if part.is_empty() {
524        return None;
525    }
526    let (pattern, value) = if let Some(eq) = part.rfind('=') {
527        (part[..eq].trim().to_owned(), part[eq + 1..].trim())
528    } else {
529        (String::new(), part)
530    };
531    let threshold = match value {
532        "off" => 0,
533        "warn" => 1,
534        "info" => 2,
535        "debug" => 3,
536        "trace" => 4,
537        other => {
538            warn!(
539                "dropping prometheus filter directive {part:?}: invalid level {other:?}, \
540                 expected one of off/warn/info/debug/trace"
541            );
542            return None;
543        }
544    };
545    Some(FilterDirective { pattern, threshold })
546}
547
548/// Evaluates `directives` for a metric, returning the last matching
549/// directive's threshold, or [`DEFAULT_THRESHOLD`] when none matches.
550///
551/// Matching order (last wins):
552/// 1. Empty pattern — global default.
553/// 2. `name.starts_with(pattern)` — metric name prefix.
554/// 3. `module.starts_with(pattern)` — module path prefix.
555/// 4. `module` contains `"::{pattern}"` — exact module component.
556fn threshold_for(directives: &[FilterDirective], name: &str, module: &str) -> u8 {
557    let mut threshold = DEFAULT_THRESHOLD;
558    for dir in directives {
559        if dir.pattern.is_empty()
560            || name.starts_with(dir.pattern.as_str())
561            || module.starts_with(dir.pattern.as_str())
562            || module.contains(&format!("::{}", dir.pattern))
563        {
564            threshold = dir.threshold;
565        }
566    }
567    threshold
568}
569
570impl Filter {
571    /// Parses a directive string, ignoring the `METRICS_FILTER` env var; use
572    /// [`Filter::resolve`] to honour it.
573    pub fn parse(s: &str) -> Self {
574        let directives = s.split(',').filter_map(parse_directive).collect();
575        Self { directives }
576    }
577
578    /// Returns `true` if a registered metric named `name` in `module` at
579    /// verbosity `level` should be exposed when gathering.
580    #[inline]
581    pub fn is_exposed(&self, name: &str, module: &str, level: MetricLevel) -> bool {
582        threshold_for(&self.directives, name, module) >= level.verbosity()
583    }
584
585    /// Resolves the metrics filter from `fallback` (the node config)
586    /// and the `METRICS_FILTER` env variables. If the same key exists in both,
587    /// the env var takes precedence.
588    pub fn resolve(fallback: Option<&str>) -> Self {
589        let env = std::env::var("METRICS_FILTER").ok();
590        match (fallback, env.as_deref()) {
591            (Some(f), Some(e)) => Self::parse(&format!("{f},{e}")),
592            (Some(f), None) => Self::parse(f),
593            (None, Some(e)) => Self::parse(e),
594            (None, None) => Self::default(),
595        }
596    }
597}
598
599// ---------------------------------------------------------------------------
600// Registry
601// ---------------------------------------------------------------------------
602
603/// Wraps `prometheus::Registry` with an embedded `Filter` so that
604/// `register_*_with_registry!` macros can decide at construction time whether
605/// a metric should be active.
606///
607/// Metrics registered through the wrapper macros are recorded with their
608/// module path and level, so [`Registry::gather`] can apply the filter's
609/// exposure directives to them.
610#[derive(Clone)]
611pub struct Registry {
612    inner: prometheus::Registry,
613    filter: Arc<Filter>,
614    /// Name prefix passed to [`Registry::new_custom`]; gathered family names
615    /// include it.
616    prefix: Option<String>,
617    /// Gathered family name → (module path, level) for metrics registered via
618    /// the wrapper macros; consulted by [`Registry::gather`].
619    registered: Arc<RwLock<HashMap<String, (String, MetricLevel)>>>,
620}
621
622impl Registry {
623    /// Creates a registry whose filter is resolved from the `METRICS_FILTER`
624    /// env var (permissive when unset).
625    pub fn new() -> Self {
626        Self {
627            inner: prometheus::Registry::new(),
628            filter: Arc::new(Filter::resolve(None)),
629            prefix: None,
630            registered: Arc::new(RwLock::new(HashMap::new())),
631        }
632    }
633
634    /// Creates a custom-prefixed registry.
635    pub fn new_custom(
636        prefix: Option<String>,
637        labels: Option<std::collections::HashMap<String, String>>,
638        filter: Option<Arc<Filter>>,
639    ) -> prometheus::Result<Self> {
640        Ok(Self {
641            inner: prometheus::Registry::new_custom(prefix.clone(), labels)?,
642            filter: filter.unwrap_or_else(|| Arc::new(Filter::resolve(None))),
643            prefix,
644            registered: Arc::new(RwLock::new(HashMap::new())),
645        })
646    }
647
648    /// Returns the registry's filter, so related registries can be built to
649    /// share it via [`Registry::new_custom`].
650    #[inline]
651    pub fn filter(&self) -> Arc<Filter> {
652        self.filter.clone()
653    }
654
655    /// Used by the wrapper macros: records a registering metric's module path
656    /// and level, so [`Registry::gather`] can apply the filter's exposure
657    /// directives to it.
658    #[inline]
659    pub fn record(&self, name: &str, module: &str, level: MetricLevel) {
660        let exposed_name = match &self.prefix {
661            Some(prefix) => format!("{prefix}_{name}"),
662            None => name.to_owned(),
663        };
664        self.registered
665            .write()
666            .unwrap()
667            .insert(exposed_name, (module.to_owned(), level));
668    }
669
670    /// Returns the underlying `prometheus::Registry` for use inside wrapper
671    /// macros.
672    #[inline]
673    pub fn inner(&self) -> &prometheus::Registry {
674        &self.inner
675    }
676
677    pub fn register(&self, c: Box<dyn prometheus::core::Collector>) -> prometheus::Result<()> {
678        self.inner.register(c)
679    }
680
681    pub fn unregister(&self, c: Box<dyn prometheus::core::Collector>) -> prometheus::Result<()> {
682        self.inner.unregister(c)
683    }
684
685    /// Gathers the registry's metric families, dropping those disabled by the
686    /// filter's exposure directives. Families not registered through the
687    /// wrapper macros (e.g. direct collectors) always pass through.
688    pub fn gather(&self) -> Vec<prometheus::proto::MetricFamily> {
689        let registered = self.registered.read().unwrap();
690        self.inner
691            .gather()
692            .into_iter()
693            .filter(|family| {
694                registered.get(family.name()).is_none_or(|(module, level)| {
695                    self.filter.is_exposed(family.name(), module, *level)
696                })
697            })
698            .collect()
699    }
700}
701
702impl Default for Registry {
703    fn default() -> Self {
704        Self::new()
705    }
706}
707
708impl std::fmt::Debug for Registry {
709    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
710        f.debug_struct("Registry").finish_non_exhaustive()
711    }
712}
713
714/// Returns the process-wide `Filter` of the [`default_registry`], resolved
715/// once from `METRICS_FILTER` (permissive when unset).
716fn default_filter() -> &'static Arc<Filter> {
717    static INSTANCE: OnceLock<Arc<Filter>> = OnceLock::new();
718    INSTANCE.get_or_init(|| Arc::new(Filter::resolve(None)))
719}
720
721/// Returns a reference to the global default `Registry`, wrapping the
722/// underlying `prometheus::default_registry()`. Metrics registered here
723/// appear in the standard prometheus default gather output.
724pub fn default_registry() -> &'static Registry {
725    use std::sync::OnceLock;
726    static INSTANCE: OnceLock<Registry> = OnceLock::new();
727    INSTANCE.get_or_init(|| Registry {
728        inner: prometheus::default_registry().clone(),
729        filter: default_filter().clone(),
730        prefix: None,
731        registered: Arc::new(RwLock::new(HashMap::new())),
732    })
733}
734
735// ---------------------------------------------------------------------------
736// Wrapper macros
737// ---------------------------------------------------------------------------
738//
739// Each macro captures `module_path!()` at the call site so the filter can
740// match by subsystem in addition to metric name.
741//
742// The `$registry` must be a `prometheus_filtered::Registry`. On success the
743// macro always returns `Ok(WrappedType(Some(...)))` or `Ok(WrappedType(None))`
744// — never `Err` from the filtering logic itself.
745//
746// `$crate::prometheus::` is used for inner prometheus macro calls so that
747// callers don't need a direct `prometheus` crate dependency.
748//
749// `let _n = $name; let name: &str = &*_n;` handles both `&str` literals and
750// `format!(...)` String expressions uniformly.
751
752/// register_int_counter_with_registry!(name, help, registry)
753#[macro_export]
754macro_rules! register_int_counter_with_registry {
755    ($name:expr, $help:expr, $registry:expr $(,)?) => {
756        $crate::register_int_counter_with_registry!(
757            $name, $help, $registry; $crate::MetricLevel::Debug
758        )
759    };
760    ($name:expr, $help:expr, $registry:expr ; $level:expr $(,)?) => {{
761        let _n = $name;
762        let name: &str = &*_n;
763        let module: &str = module_path!();
764        ($registry).record(name, module, $level);
765        $crate::prometheus::register_int_counter_with_registry!(
766            name,
767            $help,
768            ($registry).inner()
769        )
770        .map($crate::core::GenericCounter::new_some)
771    }};
772}
773
774/// register_int_counter_vec_with_registry!(name, help, labels, registry)
775#[macro_export]
776macro_rules! register_int_counter_vec_with_registry {
777    ($name:expr, $help:expr, $labels:expr, $registry:expr $(,)?) => {
778        $crate::register_int_counter_vec_with_registry!(
779            $name, $help, $labels, $registry; $crate::MetricLevel::Debug
780        )
781    };
782    ($name:expr, $help:expr, $labels:expr, $registry:expr ; $level:expr $(,)?) => {{
783        let _n = $name;
784        let name: &str = &*_n;
785        let module: &str = module_path!();
786        ($registry).record(name, module, $level);
787        $crate::prometheus::register_int_counter_vec_with_registry!(
788            name,
789            $help,
790            $labels,
791            ($registry).inner()
792        )
793        .map($crate::IntCounterVec::new_some)
794    }};
795}
796
797/// register_int_gauge_with_registry!(name, help, registry)
798#[macro_export]
799macro_rules! register_int_gauge_with_registry {
800    ($name:expr, $help:expr, $registry:expr $(,)?) => {
801        $crate::register_int_gauge_with_registry!(
802            $name, $help, $registry; $crate::MetricLevel::Debug
803        )
804    };
805    ($name:expr, $help:expr, $registry:expr ; $level:expr $(,)?) => {{
806        let _n = $name;
807        let name: &str = &*_n;
808        let module: &str = module_path!();
809        ($registry).record(name, module, $level);
810        $crate::prometheus::register_int_gauge_with_registry!(name, $help, ($registry).inner())
811            .map($crate::core::GenericGauge::new_some)
812    }};
813}
814
815/// register_int_gauge_vec_with_registry!(name, help, labels, registry)
816#[macro_export]
817macro_rules! register_int_gauge_vec_with_registry {
818    ($name:expr, $help:expr, $labels:expr, $registry:expr $(,)?) => {
819        $crate::register_int_gauge_vec_with_registry!(
820            $name, $help, $labels, $registry; $crate::MetricLevel::Debug
821        )
822    };
823    ($name:expr, $help:expr, $labels:expr, $registry:expr ; $level:expr $(,)?) => {{
824        let _n = $name;
825        let name: &str = &*_n;
826        let module: &str = module_path!();
827        ($registry).record(name, module, $level);
828        $crate::prometheus::register_int_gauge_vec_with_registry!(
829            name,
830            $help,
831            $labels,
832            ($registry).inner()
833        )
834        .map($crate::IntGaugeVec::new_some)
835    }};
836}
837
838/// register_histogram_with_registry!(name, help, registry)
839/// register_histogram_with_registry!(name, help, buckets, registry)
840#[macro_export]
841macro_rules! register_histogram_with_registry {
842    ($name:expr, $help:expr, $registry:expr $(,)?) => {
843        $crate::register_histogram_with_registry!($name, $help, $registry; $crate::MetricLevel::Debug)
844    };
845    ($name:expr, $help:expr, $buckets:expr, $registry:expr $(,)?) => {
846        $crate::register_histogram_with_registry!(
847            $name, $help, $buckets, $registry; $crate::MetricLevel::Debug
848        )
849    };
850    ($name:expr, $help:expr, $registry:expr ; $level:expr $(,)?) => {{
851        let _n = $name;
852        let name: &str = &*_n;
853        let module: &str = module_path!();
854        ($registry).record(name, module, $level);
855        $crate::prometheus::register_histogram_with_registry!(name, $help, ($registry).inner())
856            .map($crate::Histogram::new_some)
857    }};
858    ($name:expr, $help:expr, $buckets:expr, $registry:expr ; $level:expr $(,)?) => {{
859        let _n = $name;
860        let name: &str = &*_n;
861        let module: &str = module_path!();
862        ($registry).record(name, module, $level);
863        $crate::prometheus::register_histogram_with_registry!(
864            name,
865            $help,
866            $buckets,
867            ($registry).inner()
868        )
869        .map($crate::Histogram::new_some)
870    }};
871}
872
873/// register_histogram_vec_with_registry!(name, help, labels, registry)
874/// register_histogram_vec_with_registry!(name, help, labels, buckets, registry)
875#[macro_export]
876macro_rules! register_histogram_vec_with_registry {
877    ($name:expr, $help:expr, $labels:expr, $registry:expr $(,)?) => {
878        $crate::register_histogram_vec_with_registry!(
879            $name, $help, $labels, $registry; $crate::MetricLevel::Debug
880        )
881    };
882    ($name:expr, $help:expr, $labels:expr, $buckets:expr, $registry:expr $(,)?) => {
883        $crate::register_histogram_vec_with_registry!(
884            $name, $help, $labels, $buckets, $registry; $crate::MetricLevel::Debug
885        )
886    };
887    ($name:expr, $help:expr, $labels:expr, $registry:expr ; $level:expr $(,)?) => {{
888        let _n = $name;
889        let name: &str = &*_n;
890        let module: &str = module_path!();
891        ($registry).record(name, module, $level);
892        $crate::prometheus::register_histogram_vec_with_registry!(
893            name,
894            $help,
895            $labels,
896            ($registry).inner()
897        )
898        .map($crate::HistogramVec::new_some)
899    }};
900    ($name:expr, $help:expr, $labels:expr, $buckets:expr, $registry:expr ; $level:expr $(,)?) => {{
901        let _n = $name;
902        let name: &str = &*_n;
903        let module: &str = module_path!();
904        ($registry).record(name, module, $level);
905        $crate::prometheus::register_histogram_vec_with_registry!(
906            name,
907            $help,
908            $labels,
909            $buckets,
910            ($registry).inner()
911        )
912        .map($crate::HistogramVec::new_some)
913    }};
914}
915
916/// register_gauge_vec_with_registry!(name, help, labels, registry)
917#[macro_export]
918macro_rules! register_gauge_vec_with_registry {
919    ($name:expr, $help:expr, $labels:expr, $registry:expr $(,)?) => {
920        $crate::register_gauge_vec_with_registry!(
921            $name, $help, $labels, $registry; $crate::MetricLevel::Debug
922        )
923    };
924    ($name:expr, $help:expr, $labels:expr, $registry:expr ; $level:expr $(,)?) => {{
925        let _n = $name;
926        let name: &str = &*_n;
927        let module: &str = module_path!();
928        ($registry).record(name, module, $level);
929        $crate::prometheus::register_gauge_vec_with_registry!(
930            name,
931            $help,
932            $labels,
933            ($registry).inner()
934        )
935        .map($crate::core::GenericGaugeVec::new_some)
936    }};
937}
938
939/// register_gauge_with_registry!(name, help, registry)
940#[macro_export]
941macro_rules! register_gauge_with_registry {
942    ($name:expr, $help:expr, $registry:expr $(,)?) => {
943        $crate::register_gauge_with_registry!($name, $help, $registry; $crate::MetricLevel::Debug)
944    };
945    ($name:expr, $help:expr, $registry:expr ; $level:expr $(,)?) => {{
946        let _n = $name;
947        let name: &str = &*_n;
948        let module: &str = module_path!();
949        ($registry).record(name, module, $level);
950        $crate::prometheus::register_gauge_with_registry!(name, $help, ($registry).inner())
951            .map($crate::core::GenericGauge::new_some)
952    }};
953}
954
955/// register_counter!(name, help) - global prometheus registry, filtered.
956#[macro_export]
957macro_rules! register_counter {
958    ($name:expr, $help:expr $(,)?) => {
959        $crate::register_counter!($name, $help; $crate::MetricLevel::Debug)
960    };
961    ($name:expr, $help:expr ; $level:expr $(,)?) => {{
962        let _n = $name;
963        let name: &str = &*_n;
964        let module: &str = module_path!();
965        $crate::default_registry().record(name, module, $level);
966        $crate::prometheus::register_counter!(name, $help)
967            .map($crate::core::GenericCounter::new_some)
968    }};
969}
970
971/// register_counter_vec_with_registry!(name, help, labels, registry)
972#[macro_export]
973macro_rules! register_counter_vec_with_registry {
974    ($name:expr, $help:expr, $labels:expr, $registry:expr $(,)?) => {{
975        let _n = $name;
976        let name: &str = &*_n;
977        let module: &str = module_path!();
978        ($registry).record(name, module, $crate::MetricLevel::Debug);
979        $crate::prometheus::register_counter_vec_with_registry!(
980            name,
981            $help,
982            $labels,
983            ($registry).inner()
984        )
985        .map($crate::core::GenericCounterVec::new_some)
986    }};
987}
988
989/// register_counter_vec!(name, help, labels) - global registry, filtered.
990#[macro_export]
991macro_rules! register_counter_vec {
992    ($name:expr, $help:expr, $labels:expr $(,)?) => {
993        $crate::register_counter_vec!($name, $help, $labels; $crate::MetricLevel::Debug)
994    };
995    ($name:expr, $help:expr, $labels:expr ; $level:expr $(,)?) => {{
996        let _n = $name;
997        let name: &str = &*_n;
998        let module: &str = module_path!();
999        $crate::default_registry().record(name, module, $level);
1000        $crate::prometheus::register_counter_vec!(name, $help, $labels)
1001            .map($crate::core::GenericCounterVec::new_some)
1002    }};
1003}
1004
1005/// register_histogram_vec!(opts, labels) or (name, help, labels) or (name,
1006/// help, labels, buckets) — global prometheus registry, filtered.
1007#[macro_export]
1008macro_rules! register_histogram_vec {
1009    ($opts:expr, $labels:expr $(,)?) => {
1010        $crate::register_histogram_vec!($opts, $labels; $crate::MetricLevel::Debug)
1011    };
1012    ($name:expr, $help:expr, $labels:expr $(,)?) => {
1013        $crate::register_histogram_vec!($name, $help, $labels; $crate::MetricLevel::Debug)
1014    };
1015    ($name:expr, $help:expr, $labels:expr, $buckets:expr $(,)?) => {
1016        $crate::register_histogram_vec!(
1017            $name, $help, $labels, $buckets; $crate::MetricLevel::Debug
1018        )
1019    };
1020    ($opts:expr, $labels:expr ; $level:expr $(,)?) => {{
1021        let opts = $opts;
1022        let name: &str = &opts.common_opts.name;
1023        let module: &str = module_path!();
1024        $crate::default_registry().record(name, module, $level);
1025        $crate::prometheus::register_histogram_vec!(opts, $labels)
1026            .map($crate::HistogramVec::new_some)
1027    }};
1028    ($name:expr, $help:expr, $labels:expr ; $level:expr $(,)?) => {{
1029        let _n = $name;
1030        let name: &str = &*_n;
1031        let module: &str = module_path!();
1032        $crate::default_registry().record(name, module, $level);
1033        $crate::prometheus::register_histogram_vec!(name, $help, $labels)
1034            .map($crate::HistogramVec::new_some)
1035    }};
1036    ($name:expr, $help:expr, $labels:expr, $buckets:expr ; $level:expr $(,)?) => {{
1037        let _n = $name;
1038        let name: &str = &*_n;
1039        let module: &str = module_path!();
1040        $crate::default_registry().record(name, module, $level);
1041        $crate::prometheus::register_histogram_vec!(name, $help, $labels, $buckets)
1042            .map($crate::HistogramVec::new_some)
1043    }};
1044}
1045
1046#[cfg(test)]
1047mod tests {
1048    use super::MetricLevel::Debug;
1049
1050    #[test]
1051    fn filter_matches_metric_or_module_name_prefix() {
1052        // An `off` directive hides exactly the metrics its pattern matches;
1053        // unmatched metrics stay exposed (the permissive default).
1054        let filter = super::Filter::parse("authority=off");
1055        assert!(filter.is_exposed("some_authority", "iota_core::checkpoints", Debug));
1056        assert!(!filter.is_exposed("authority", "iota_core::checkpoints", Debug));
1057        assert!(!filter.is_exposed("authority_aggregator", "iota_core::checkpoints", Debug));
1058        assert!(filter.is_exposed("certs_total", "iota_core::some_authority", Debug));
1059        assert!(!filter.is_exposed("certs_total", "iota_core::authority", Debug));
1060        assert!(!filter.is_exposed("certs_total", "iota_core::authority_aggregator", Debug));
1061
1062        // the last matching prefix shadows the previous ones
1063        let filter = super::Filter::parse("authority=off,authority_aggregator=trace");
1064        assert!(!filter.is_exposed("authority", "iota_core::checkpoints", Debug));
1065        assert!(filter.is_exposed("authority_aggregator", "iota_core::checkpoints", Debug));
1066        assert!(!filter.is_exposed("certs_total", "iota_core::authority", Debug));
1067        assert!(filter.is_exposed("certs_total", "iota_core::authority_aggregator", Debug));
1068
1069        // filter can be set off by default
1070        let filter = super::Filter::parse("off,authority_aggregator=trace");
1071        assert!(!filter.is_exposed("some_authority", "iota_core::checkpoints", Debug));
1072        assert!(!filter.is_exposed("authority", "iota_core::checkpoints", Debug));
1073        assert!(filter.is_exposed("authority_aggregator", "iota_core::checkpoints", Debug));
1074        assert!(!filter.is_exposed("certs_total", "iota_core::some_authority", Debug));
1075        assert!(!filter.is_exposed("certs_total", "iota_core::authority", Debug));
1076        assert!(filter.is_exposed("certs_total", "iota_core::authority_aggregator", Debug));
1077
1078        // the full prefix must be matched
1079        let filter = super::Filter::parse("authority_aggregator=off");
1080        assert!(filter.is_exposed("authority", "iota_core::checkpoints", Debug));
1081        assert!(!filter.is_exposed("authority_aggregator", "iota_core::checkpoints", Debug));
1082        assert!(filter.is_exposed("certs_total", "iota_core::authority", Debug));
1083        assert!(!filter.is_exposed("certs_total", "iota_core::authority_aggregator", Debug));
1084    }
1085
1086    #[test]
1087    fn unmatched_metrics_are_exposed() {
1088        use super::MetricLevel::{Info, Trace, Warn};
1089        // Filtering is opt-in: with no matching directive every metric is
1090        // exposed, matching plain `prometheus` behaviour.
1091        for filter in [
1092            super::Filter::parse(""),
1093            super::Filter::default(),
1094            // empty segments are ignored rather than treated as directives.
1095            super::Filter::parse(",,"),
1096        ] {
1097            assert!(filter.is_exposed("anything", "any::module", Warn));
1098            assert!(filter.is_exposed("anything", "any::module", Info));
1099            assert!(filter.is_exposed("anything", "any::module", Debug));
1100            assert!(filter.is_exposed("anything", "any::module", Trace));
1101        }
1102    }
1103
1104    #[test]
1105    fn rejects_boolean_and_numeric_aliases() {
1106        use super::MetricLevel::Trace;
1107        // Only the RUST_LOG-style level names are accepted; the former
1108        // `on`/`true`/`1` and `false`/`0` aliases are now invalid, so they are
1109        // dropped and the directive falls back to the permissive default.
1110        for alias in ["on", "true", "1", "false", "0"] {
1111            let filter = super::Filter::parse(&format!("authority={alias}"));
1112            assert!(
1113                filter.is_exposed("authority", "m", Trace),
1114                "{alias} should be dropped as invalid, leaving the default"
1115            );
1116        }
1117        // `off` still disables.
1118        assert!(
1119            !super::Filter::parse("authority=off").is_exposed("authority", "m", Debug),
1120            "off should disable"
1121        );
1122    }
1123
1124    #[test]
1125    fn invalid_directives_are_dropped() {
1126        use super::MetricLevel::Trace;
1127        // an unrecognised value leaves the directive out, falling back to the
1128        // permissive default.
1129        assert!(super::Filter::parse("authority=maybe").is_exposed("authority", "m", Trace));
1130        // a bare token without `=LEVEL` is parsed as a global value and, being
1131        // invalid, dropped — it does NOT enable/disable the `authority` subsystem.
1132        assert!(super::Filter::parse("authority").is_exposed("authority", "m", Trace));
1133        // a valid directive alongside an invalid one still takes effect.
1134        let filter = super::Filter::parse("authority=off,bogus=nope");
1135        assert!(!filter.is_exposed("authority", "m", Debug));
1136    }
1137
1138    #[test]
1139    fn matches_module_path_prefix() {
1140        // a pattern that is a prefix of the full module path (not only a `::`
1141        // component) matches.
1142        let filter = super::Filter::parse("iota_core=off");
1143        assert!(!filter.is_exposed("certs_total", "iota_core::authority", Debug));
1144        assert!(filter.is_exposed("certs_total", "starfish::core", Debug));
1145    }
1146
1147    #[test]
1148    fn global_trace_default() {
1149        // last-match-wins applies to bare global directives too.
1150        assert!(super::Filter::parse("off,trace").is_exposed("authority", "m", Debug));
1151        // an explicit permissive default with a targeted `off` override.
1152        let filter = super::Filter::parse("trace,authority=off");
1153        assert!(filter.is_exposed("certs_total", "m", Debug));
1154        assert!(!filter.is_exposed("authority", "m", Debug));
1155    }
1156
1157    #[test]
1158    fn whitespace_is_trimmed() {
1159        let filter = super::Filter::parse("  authority = off ,  authority_aggregator = trace  ");
1160        assert!(!filter.is_exposed("authority", "m", Debug));
1161        assert!(filter.is_exposed("authority_aggregator", "m", Debug));
1162    }
1163
1164    #[test]
1165    fn resolve_applies_fallback() {
1166        use super::{Arc, Filter, MetricLevel, Registry};
1167
1168        // The env var's directives are merged after the fallback's, so the
1169        // assertions below only hold when it is unset.
1170        if std::env::var_os("METRICS_FILTER").is_some() {
1171            return;
1172        }
1173
1174        // No env, no fallback -> everything is exposed.
1175        assert!(Filter::resolve(None).is_exposed("anything", "m", MetricLevel::Trace));
1176        assert!(Filter::resolve(None).is_exposed("anything", "m", Debug));
1177
1178        // No env -> the fallback directives apply.
1179        let filter = Arc::new(Filter::resolve(Some("off,authority=trace")));
1180        assert!(filter.is_exposed("authority", "m", MetricLevel::Debug));
1181        assert!(!filter.is_exposed("consensus", "m", MetricLevel::Debug));
1182
1183        // Registries built to share the filter see the same decisions.
1184        let registry = Registry::new_custom(None, None, Some(filter.clone())).unwrap();
1185        let shared = Registry::new_custom(None, None, Some(filter)).unwrap();
1186        assert!(std::sync::Arc::ptr_eq(&registry.filter(), &shared.filter()));
1187    }
1188
1189    #[test]
1190    fn level_thresholds() {
1191        use super::MetricLevel::{Debug, Info, Trace, Warn};
1192        // `warn` threshold exposes only warn metrics.
1193        let f = super::Filter::parse("authority=warn");
1194        assert!(f.is_exposed("x", "iota_core::authority", Warn));
1195        assert!(!f.is_exposed("x", "iota_core::authority", Info));
1196        assert!(!f.is_exposed("x", "iota_core::authority", Debug));
1197        // `info` threshold exposes warn+info, hides debug.
1198        let f = super::Filter::parse("authority=info");
1199        assert!(f.is_exposed("x", "iota_core::authority", Warn));
1200        assert!(f.is_exposed("x", "iota_core::authority", Info));
1201        assert!(!f.is_exposed("x", "iota_core::authority", Debug));
1202        // `debug` exposes everything untagged and below, but not trace.
1203        let f = super::Filter::parse("authority=debug");
1204        assert!(f.is_exposed("x", "iota_core::authority", Debug));
1205        assert!(!f.is_exposed("x", "iota_core::authority", Trace));
1206        // `trace` exposes everything.
1207        let f = super::Filter::parse("authority=trace");
1208        assert!(f.is_exposed("x", "iota_core::authority", Trace));
1209        // `off` exposes nothing.
1210        assert!(!super::Filter::parse("authority=off").is_exposed(
1211            "x",
1212            "iota_core::authority",
1213            Warn
1214        ));
1215        // No directive -> exposed at every level.
1216        assert!(super::Filter::parse("").is_exposed("x", "m", Info));
1217        assert!(super::Filter::parse("").is_exposed("x", "m", Trace));
1218    }
1219}
1220
1221#[cfg(test)]
1222mod gather_filter_tests {
1223    use super::{Filter, MetricLevel, Registry};
1224
1225    fn registry(filter: &str) -> Registry {
1226        Registry::new_custom(None, None, Some(std::sync::Arc::new(Filter::parse(filter)))).unwrap()
1227    }
1228
1229    fn gathered_names(registry: &Registry) -> Vec<String> {
1230        let mut names: Vec<_> = registry
1231            .gather()
1232            .iter()
1233            .map(|f| f.name().to_owned())
1234            .collect();
1235        names.sort();
1236        names
1237    }
1238
1239    #[test]
1240    fn gather_applies_level_thresholds_by_module() {
1241        // Metrics register in this module (`prometheus_filtered::gather_filter_tests`).
1242        let reg = registry("gather_filter_tests=warn");
1243        crate::register_int_gauge_with_registry!("g_warn", "h", &reg; MetricLevel::Warn).unwrap();
1244        let g_debug = crate::register_int_gauge_with_registry!("g_debug", "h", &reg).unwrap();
1245        g_debug.set(7);
1246
1247        // Only the warn-tagged metric is exposed; the debug one is registered
1248        // and keeps collecting.
1249        assert_eq!(gathered_names(&reg), ["g_warn"]);
1250        assert_eq!(g_debug.get(), 7);
1251    }
1252
1253    #[test]
1254    fn off_directive_hides_but_still_registers() {
1255        let reg = registry("g_hidden=off");
1256        let g = crate::register_int_gauge_with_registry!("g_hidden", "h", &reg).unwrap();
1257        // Registered (a disabled wrapper would print "(disabled)") and
1258        // collecting, but absent from gather output.
1259        assert_eq!(format!("{g:?}"), "GenericGauge");
1260        g.set(9);
1261        assert_eq!(g.get(), 9);
1262        assert_eq!(gathered_names(&reg), Vec::<String>::new());
1263    }
1264
1265    #[test]
1266    fn prefixed_registry_records_exposed_family_names() {
1267        let exposed = Registry::new_custom(
1268            Some("consensus".to_owned()),
1269            None,
1270            Some(std::sync::Arc::new(Filter::parse(""))),
1271        )
1272        .unwrap();
1273        crate::register_int_gauge_with_registry!("g", "h", &exposed; MetricLevel::Warn).unwrap();
1274        assert_eq!(gathered_names(&exposed), ["consensus_g"]);
1275
1276        // The filter keys on the module path, so the prefixed family is
1277        // matched and hidden even though its gathered name differs.
1278        let hidden = Registry::new_custom(
1279            Some("consensus".to_owned()),
1280            None,
1281            Some(std::sync::Arc::new(Filter::parse(
1282                "gather_filter_tests=off",
1283            ))),
1284        )
1285        .unwrap();
1286        crate::register_int_gauge_with_registry!("g", "h", &hidden; MetricLevel::Warn).unwrap();
1287        assert_eq!(gathered_names(&hidden), Vec::<String>::new());
1288    }
1289
1290    #[test]
1291    fn directly_registered_collectors_bypass_filter() {
1292        let reg = registry("off");
1293        crate::register_int_gauge_with_registry!("g_macro", "h", &reg).unwrap();
1294        let gauge = prometheus::IntGauge::new("g_direct", "h").unwrap();
1295        reg.register(Box::new(gauge)).unwrap();
1296
1297        // Not registered through the macros -> no module/level recorded ->
1298        // the exposure filter does not apply.
1299        assert_eq!(gathered_names(&reg), ["g_direct"]);
1300    }
1301}
1302
1303#[cfg(test)]
1304mod level_macro_tests {
1305    use super::{IntGauge, MetricLevel, Registry};
1306
1307    fn registry(filter: &str) -> Registry {
1308        Registry::new_custom(
1309            None,
1310            None,
1311            Some(std::sync::Arc::new(super::Filter::parse(filter))),
1312        )
1313        .unwrap()
1314    }
1315
1316    #[test]
1317    fn hidden_metrics_still_register_and_collect() {
1318        // At a `warn` threshold, a default (`debug`) metric still registers
1319        // and collects — it is only hidden from `gather` output.
1320        let reg = registry("g_default=warn");
1321        let g: IntGauge = crate::register_int_gauge_with_registry!("g_default", "h", &reg).unwrap();
1322        // `IntGauge` is a type alias for `core::GenericGauge<AtomicI64>`; its
1323        // `Debug` impl prints the underlying `GenericGauge` name, not the alias.
1324        assert_eq!(format!("{g:?}"), "GenericGauge");
1325        g.set(42);
1326        assert_eq!(g.get(), 42);
1327        assert!(reg.gather().is_empty());
1328
1329        // Even an `off` threshold registers the metric; it only hides it.
1330        let reg = registry("g_off=off");
1331        let g: IntGauge = crate::register_int_gauge_with_registry!(
1332            "g_off", "h", &reg; MetricLevel::Warn
1333        )
1334        .unwrap();
1335        assert_eq!(format!("{g:?}"), "GenericGauge");
1336        assert!(reg.gather().is_empty());
1337    }
1338}