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 is one directive set, built
9//! by merging its inputs in precedence order — the node config's directives,
10//! the `METRICS_FILTER` environment variable's, and an optional runtime
11//! override: a higher-precedence directive replaces the directive with the
12//! same pattern, and otherwise the sets' directives apply side by side (see
13//! [`Filter`]).
14//!
15//! Filter syntax: comma-separated `pattern=LEVEL` directives, where `LEVEL`
16//! is one of `off`, `warn`, `info`, `debug`, `trace`. A bare `LEVEL` token
17//! (no `pattern=`) and its reserved `default=LEVEL` spelling both set the
18//! global default: the level for the metrics no other directive matches.
19//! The bare spelling additionally makes its source replace the
20//! lower-precedence sources' directives instead of merging over them —
21//! `METRICS_FILTER=trace` exposes everything, whatever the config sets. A
22//! pattern matches if it is a
23//! prefix of the metric name OR is a component/prefix of the calling module
24//! path (e.g. `traffic_controller` matches
25//! `iota_core::traffic_controller::metrics`). When several directives
26//! match the same metric, the most specific one wins regardless of order: a
27//! metric-name match over a module match, then the longest pattern; among
28//! directives with the same pattern, the last one wins.
29//!
30//! Examples:
31//! - `METRICS_FILTER=off,authority=warn`
32//! - `METRICS_FILTER=authority=off`
33//!
34//! The directives act as **exposure**
35//! thresholds deciding which metrics [`Registry::gather`] includes in its
36//! output (`off` exposes none of the matched metrics). Metrics matched by no
37//! directive are exposed unconditionally, so with no filter configured the
38//! crate behaves exactly like plain `prometheus`; use a `default=LEVEL`
39//! directive to set a stricter global default.
40
41use std::{
42    result::Result as StdResult,
43    sync::{Arc, OnceLock, RwLock},
44};
45
46/// Re-exported under a hidden alias so `$crate::prometheus::xxx!` works
47/// inside `#[macro_export]` macros without requiring callers to depend
48/// directly on the `prometheus` crate.
49#[doc(hidden)]
50pub use prometheus;
51// ---------------------------------------------------------------------------
52// prometheus re-exports
53// ---------------------------------------------------------------------------
54
55// Filtering is enforced by a collector wrapper installed at registration (see
56// `Registry::register_filtered`), so the metric types need no wrapping:
57// re-export prometheus's own types and generic primitives directly.
58pub use prometheus::{
59    Counter, CounterVec, Gauge, GaugeVec, Histogram, HistogramTimer, HistogramVec, IntCounter,
60    IntCounterVec, IntGauge, IntGaugeVec, core,
61};
62// Re-export the prometheus items callers reach for through this crate.
63pub use prometheus::{
64    DEFAULT_BUCKETS, Encoder, Error, HistogramOpts, Opts, PROTOBUF_FORMAT, ProtobufEncoder, Result,
65    TextEncoder, exponential_buckets, gather, histogram_opts, linear_buckets, opts, proto,
66};
67use tracing::warn;
68
69// ---------------------------------------------------------------------------
70// Filter
71// ---------------------------------------------------------------------------
72
73/// Verbosity level for a metric.
74#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
75#[serde(rename_all = "lowercase")]
76pub enum MetricLevel {
77    /// As a filter threshold: expose none of the matched metrics. Not
78    /// meaningful as a per-metric level — tag metrics `Warn`..`Trace`.
79    Off,
80    Warn,
81    Info,
82    // The default for an untagged metric.
83    #[default]
84    Debug,
85    Trace,
86}
87
88impl MetricLevel {
89    pub(crate) const fn verbosity(self) -> u8 {
90        match self {
91            Self::Off => 0,
92            Self::Warn => 1,
93            Self::Info => 2,
94            Self::Debug => 3,
95            Self::Trace => 4,
96        }
97    }
98
99    pub const fn as_str(self) -> &'static str {
100        match self {
101            Self::Off => "off",
102            Self::Warn => "warn",
103            Self::Info => "info",
104            Self::Debug => "debug",
105            Self::Trace => "trace",
106        }
107    }
108}
109
110/// Environment variable holding filter directives, read by
111/// [`Filter::from_env`].
112pub const METRICS_FILTER_ENV: &str = "METRICS_FILTER";
113
114/// Default threshold when no directive matches a metric: expose it. Filtering
115/// is opt-in, so an unfiltered registry behaves like plain `prometheus`.
116const DEFAULT_THRESHOLD: u8 = MetricLevel::Trace.verbosity();
117
118#[derive(Clone)]
119struct FilterDirective {
120    /// The empty string (a bare level) and the reserved `default` pattern
121    /// both mean the global default: they match every metric but lose to
122    /// any other matching directive.
123    pattern: String,
124    /// `pattern` prefixed with `::`, precomputed so the per-gather module
125    /// component match allocates nothing.
126    component_pattern: String,
127    /// Metrics matched by this directive are exposed iff their verbosity is
128    /// at most this level's.
129    level: MetricLevel,
130}
131
132/// Filter holds two directive sets: the immutable startup directives (the
133/// node config's with the `METRICS_FILTER` env var's merged over them) and
134/// the directives currently in effect — the startup directives, with the
135/// runtime override merged over them while one is set.
136#[derive(Default)]
137pub struct Filter {
138    /// The startup directives; what [`Filter::reset_runtime_filter`] restores.
139    startup: Arc<DirectiveSet>,
140    /// The directives consulted by [`Filter::is_exposed`].
141    runtime: RwLock<Arc<DirectiveSet>>,
142}
143
144/// The source strings for one filter input. `directives` is parsed for
145/// matching; `display` is what [`Filter::filter_string`] and
146/// [`Filter::startup_filter_string`] echo back (e.g. the group-form string a
147/// caller expanded before building the filter). Use [`FilterSource::new`]
148/// when the two are the same string.
149#[derive(Clone, Copy)]
150pub struct FilterSource<'a> {
151    pub directives: &'a str,
152    pub display: &'a str,
153}
154
155impl<'a> FilterSource<'a> {
156    pub fn new(s: &'a str) -> Self {
157        Self {
158            directives: s,
159            display: s,
160        }
161    }
162
163    /// `directives` drive matching; `display` is echoed by the admin
164    /// endpoint.
165    pub fn with_display(directives: &'a str, display: &'a str) -> Self {
166        Self {
167            directives,
168            display,
169        }
170    }
171}
172
173/// One parsed filter input: the matching directives plus the display
174/// directives they are reported as.
175#[derive(Default, Clone)]
176struct DirectiveSet {
177    directives: Vec<FilterDirective>,
178    display: Vec<FilterDirective>,
179}
180
181impl DirectiveSet {
182    fn from_source(source: FilterSource<'_>) -> Self {
183        Self {
184            directives: parse_valid_directives(source.directives),
185            display: parse_valid_directives(source.display),
186        }
187    }
188
189    /// Returns whether the set contains a bare-level directive.
190    fn replaces(&self) -> bool {
191        self.directives.iter().any(|dir| dir.pattern.is_empty())
192    }
193
194    /// Merges `over` on top of `self`: an `over` directive replaces the
195    /// directive with the same pattern; otherwise both sets' directives
196    /// apply and the usual most-specific-pattern-wins matching decides each
197    /// metric. As the exception, an `over` set with a bare level replaces
198    /// `self` entirely — `METRICS_FILTER=trace` exposes everything no
199    /// matter what the config directives say, while `default=trace` raises
200    /// only the global default.
201    fn merged(&self, over: &Self) -> Self {
202        if over.replaces() {
203            return over.clone();
204        }
205        Self {
206            directives: merge_directives(&self.directives, &over.directives),
207            display: merge_directives(&self.display, &over.display),
208        }
209    }
210}
211
212/// Appends `over` to `base`, dropping the `base` directives that an `over`
213/// directive with the same pattern replaces.
214fn merge_directives(base: &[FilterDirective], over: &[FilterDirective]) -> Vec<FilterDirective> {
215    base.iter()
216        .filter(|dir| !over.iter().any(|o| o.pattern == dir.pattern))
217        .chain(over.iter())
218        .cloned()
219        .collect()
220}
221
222/// Parses a directive string, returning the valid directives and an error
223/// for each invalid one; the caller decides whether an error drops the
224/// directive or rejects the whole string.
225fn parse_directives(s: &str) -> (Vec<FilterDirective>, Vec<String>) {
226    let mut directives = Vec::new();
227    let mut errors = Vec::new();
228    for part in directive_parts(s) {
229        match split_directive(part) {
230            Ok((pattern, level)) => directives.push(FilterDirective {
231                component_pattern: format!("::{pattern}"),
232                pattern: pattern.to_owned(),
233                level,
234            }),
235            Err(err) => errors.push(err),
236        }
237    }
238    (directives, errors)
239}
240
241/// Parses a directive string, dropping invalid directives with a warning.
242fn parse_valid_directives(s: &str) -> Vec<FilterDirective> {
243    let (directives, errors) = parse_directives(s);
244    for err in errors {
245        warn!("dropping prometheus filter directive: {err}");
246    }
247    directives
248}
249
250/// Splits a `METRICS_FILTER`-style string into its non-empty, trimmed
251/// directive segments.
252pub fn directive_parts(s: &str) -> impl Iterator<Item = &str> + '_ {
253    s.split(',').map(str::trim).filter(|part| !part.is_empty())
254}
255
256/// Splits one directive into its `(pattern, level)` parts, rejecting an
257/// invalid level with an error describing the offending directive.
258pub fn split_directive(part: &str) -> StdResult<(&str, MetricLevel), String> {
259    let (pattern, value) = match part.rfind('=') {
260        Some(eq) => (part[..eq].trim(), part[eq + 1..].trim()),
261        None => ("", part.trim()),
262    };
263    let level = match value {
264        "off" => MetricLevel::Off,
265        "warn" => MetricLevel::Warn,
266        "info" => MetricLevel::Info,
267        "debug" => MetricLevel::Debug,
268        "trace" => MetricLevel::Trace,
269        other => {
270            return Err(format!(
271                "invalid level {other:?} in directive {part:?}: expected one of \
272                 off/warn/info/debug/trace"
273            ));
274        }
275    };
276    Ok((pattern, level))
277}
278
279/// Renders directives back into their `pattern=LEVEL` string.
280fn render_directives(directives: &[FilterDirective]) -> String {
281    directives
282        .iter()
283        .map(|dir| {
284            if dir.pattern.is_empty() {
285                dir.level.as_str().to_owned()
286            } else {
287                format!("{}={}", dir.pattern, dir.level.as_str())
288            }
289        })
290        .collect::<Vec<_>>()
291        .join(",")
292}
293
294/// Evaluates `directives` for a metric, returning the most specific matching
295/// directive's threshold, or the permissive default when no directive
296/// matches.
297///
298/// A directive matches when its pattern is:
299/// 1. Empty (a bare level) or the reserved `default` — global default.
300/// 2. A metric name prefix — `name.starts_with(pattern)`.
301/// 3. A module path prefix — `module.starts_with(pattern)`.
302/// 4. An exact module component — `module` contains `"::{pattern}"`.
303///
304/// Among matching directives, a metric-name match wins over a module match:
305/// a name pattern targets the metric directly, and can never be longer than
306/// the name itself, so on length alone it would silently lose to any longer
307/// module directive covering the same metric. Within the same match kind,
308/// the longest pattern wins (so a directive for a submodule overrides one
309/// for its parent, and any pattern overrides the bare global level); among
310/// equal patterns, the last one wins.
311fn threshold(directives: &[FilterDirective], name: &str, module: &str) -> u8 {
312    // Ordered comparison of (matched-by-name, pattern length): name matches
313    // rank above module matches, longer patterns above shorter; the global
314    // default patterns rank below every other match.
315    let mut best: Option<((bool, usize), u8)> = None;
316    for dir in directives {
317        let specificity = if dir.pattern.is_empty() || dir.pattern == "default" {
318            Some((false, 0))
319        } else if name.starts_with(dir.pattern.as_str()) {
320            Some((true, dir.pattern.len()))
321        } else if module.starts_with(dir.pattern.as_str())
322            || module.contains(dir.component_pattern.as_str())
323        {
324            Some((false, dir.pattern.len()))
325        } else {
326            None
327        };
328        match specificity {
329            Some(specificity) if best.is_none_or(|(prev, _)| specificity >= prev) => {
330                best = Some((specificity, dir.level.verbosity()));
331            }
332            _ => {}
333        }
334    }
335    best.map_or(DEFAULT_THRESHOLD, |(_, threshold)| threshold)
336}
337
338impl Filter {
339    // Parses a single directive string as the config source, ignoring the
340    // `METRICS_FILTER` env var. Convenience for
341    // `from_sources(FilterSource::new(s), None)`.
342    pub fn parse(s: &str) -> Self {
343        Self::from_sources(FilterSource::new(s), None)
344    }
345
346    /// Builds a filter with an empty config source and the
347    /// [`METRICS_FILTER_ENV`] variable's directives (permissive when unset).
348    pub fn from_env() -> Self {
349        let env = std::env::var(METRICS_FILTER_ENV).ok();
350        Self::from_sources(FilterSource::new(""), env.as_deref().map(FilterSource::new))
351    }
352
353    /// Builds a filter whose startup directives are the env source merged
354    /// over the config source: env directives win on conflict.
355    pub fn from_sources(config: FilterSource<'_>, env: Option<FilterSource<'_>>) -> Self {
356        let startup = Arc::new(DirectiveSet::from_source(config).merged(
357            &DirectiveSet::from_source(env.unwrap_or(FilterSource::new(""))),
358        ));
359        Self {
360            runtime: RwLock::new(startup.clone()),
361            startup,
362        }
363    }
364
365    /// Returns `true` if a registered metric named `name` in `module` at
366    /// verbosity `level` should be exposed when gathering, per the directives
367    /// currently in effect.
368    #[inline]
369    pub fn is_exposed(&self, name: &str, module: &str, level: MetricLevel) -> bool {
370        let runtime = self.runtime.read().unwrap();
371        threshold(&runtime.directives, name, module) >= level.verbosity()
372    }
373
374    /// Returns the display string of the directives currently in effect.
375    pub fn filter_string(&self) -> String {
376        render_directives(&self.runtime.read().unwrap().display)
377    }
378
379    /// Returns the startup directives' display string.
380    pub fn startup_filter_string(&self) -> String {
381        render_directives(&self.startup.display)
382    }
383
384    /// Replaces the directives in effect with the runtime override merged
385    /// over the startup directives (the override wins on conflict) — each call
386    /// starts from the startup directives again rather than stacking on the
387    /// previous override; an override with a bare level replaces the startup
388    /// directives entirely. Rejects the whole update if any directive is
389    /// invalid.
390    pub fn set_runtime_filter(&self, source: FilterSource<'_>) -> StdResult<(), String> {
391        let (directives, errors) = parse_directives(source.directives);
392        if !errors.is_empty() {
393            return Err(errors.join("; "));
394        }
395        let over = DirectiveSet {
396            directives,
397            display: parse_valid_directives(source.display),
398        };
399        *self.runtime.write().unwrap() = Arc::new(self.startup.merged(&over));
400        Ok(())
401    }
402
403    /// Drops the runtime override, restoring the startup directives.
404    pub fn reset_runtime_filter(&self) {
405        *self.runtime.write().unwrap() = self.startup.clone();
406    }
407}
408
409// ---------------------------------------------------------------------------
410// Registry
411// ---------------------------------------------------------------------------
412
413/// A collector registered through the wrapper macros, wrapped so the filter
414/// decides its exposure at gather time: while the filter hides the metric,
415/// `collect` returns nothing, and the underlying metric keeps collecting.
416struct FilteredCollector<C> {
417    name: String,
418    module: String,
419    level: MetricLevel,
420    filter: Arc<Filter>,
421    inner: C,
422}
423
424impl<C: prometheus::core::Collector> prometheus::core::Collector for FilteredCollector<C> {
425    fn desc(&self) -> Vec<&prometheus::core::Desc> {
426        self.inner.desc()
427    }
428
429    fn collect(&self) -> Vec<prometheus::proto::MetricFamily> {
430        if self.filter.is_exposed(&self.name, &self.module, self.level) {
431            self.inner.collect()
432        } else {
433            Vec::new()
434        }
435    }
436}
437
438/// Wraps `prometheus::Registry` with an embedded `Filter` so that
439/// `register_*_with_registry!` macros can decide whether a metric is exposed.
440///
441/// Metrics registered through the wrapper macros join the inner registry
442/// wrapped in a private collector type, which consults the filter on every
443/// `collect`.
444/// Exposure changes need no bookkeeping here: the next gather simply sees the
445/// new filter.
446#[derive(Clone)]
447pub struct Registry {
448    inner: prometheus::Registry,
449    filter: Arc<Filter>,
450    /// Name prefix passed to [`Registry::new_custom`]; gathered family names
451    /// include it.
452    prefix: Option<String>,
453}
454
455impl Registry {
456    /// Creates a registry whose filter honours the `METRICS_FILTER` env var
457    /// (permissive when unset).
458    pub fn new() -> Self {
459        Self {
460            inner: prometheus::Registry::new(),
461            filter: Arc::new(Filter::from_env()),
462            prefix: None,
463        }
464    }
465
466    /// Creates a custom-prefixed registry.
467    pub fn new_custom(
468        prefix: Option<String>,
469        labels: Option<std::collections::HashMap<String, String>>,
470        filter: Option<Arc<Filter>>,
471    ) -> prometheus::Result<Self> {
472        Ok(Self {
473            inner: prometheus::Registry::new_custom(prefix.clone(), labels)?,
474            filter: filter.unwrap_or_else(|| Arc::new(Filter::from_env())),
475            prefix,
476        })
477    }
478
479    /// Returns the registry's filter, so related registries can be built to
480    /// share it via [`Registry::new_custom`].
481    #[inline]
482    pub fn filter(&self) -> Arc<Filter> {
483        self.filter.clone()
484    }
485
486    fn exposed_name(&self, name: &str) -> String {
487        match &self.prefix {
488            Some(prefix) => format!("{prefix}_{name}"),
489            None => name.to_owned(),
490        }
491    }
492
493    /// Used by the wrapper macros: registers `collector` wrapped in a private
494    /// collector type, so the filter in effect at each gather decides
495    /// whether the metric is exposed. Duplicate registrations are rejected by
496    /// the inner registry's descriptor check, hidden or not.
497    #[inline]
498    pub fn register_filtered<C>(
499        &self,
500        name: &str,
501        module: &str,
502        level: MetricLevel,
503        collector: C,
504    ) -> prometheus::Result<C>
505    where
506        C: prometheus::core::Collector + Clone + 'static,
507    {
508        self.inner.register(Box::new(FilteredCollector {
509            name: self.exposed_name(name),
510            module: module.to_owned(),
511            level,
512            filter: self.filter.clone(),
513            inner: collector.clone(),
514        }))?;
515        Ok(collector)
516    }
517
518    pub fn register(&self, c: Box<dyn prometheus::core::Collector>) -> prometheus::Result<()> {
519        self.inner.register(c)
520    }
521
522    pub fn unregister(&self, c: Box<dyn prometheus::core::Collector>) -> prometheus::Result<()> {
523        self.inner.unregister(c)
524    }
525
526    /// Gathers the registry's metric families.
527    pub fn gather(&self) -> Vec<prometheus::proto::MetricFamily> {
528        self.inner.gather()
529    }
530}
531
532impl Default for Registry {
533    fn default() -> Self {
534        Self::new()
535    }
536}
537
538impl std::fmt::Debug for Registry {
539    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
540        f.debug_struct("Registry").finish_non_exhaustive()
541    }
542}
543
544/// Returns the process-wide `Filter` of the [`default_registry`], resolved
545/// once from `METRICS_FILTER`.
546fn default_filter() -> &'static Arc<Filter> {
547    static INSTANCE: OnceLock<Arc<Filter>> = OnceLock::new();
548    INSTANCE.get_or_init(|| Arc::new(Filter::from_env()))
549}
550
551/// Returns a reference to the global default `Registry`, wrapping the
552/// underlying `prometheus::default_registry()`. Metrics registered here
553/// appear in the standard prometheus default gather output.
554pub fn default_registry() -> &'static Registry {
555    use std::sync::OnceLock;
556    static INSTANCE: OnceLock<Registry> = OnceLock::new();
557    INSTANCE.get_or_init(|| Registry {
558        inner: prometheus::default_registry().clone(),
559        filter: default_filter().clone(),
560        prefix: None,
561    })
562}
563
564// ---------------------------------------------------------------------------
565// Wrapper macros
566// ---------------------------------------------------------------------------
567//
568// Each macro captures `module_path!()` at the call site so the filter can
569// match by subsystem in addition to metric name.
570//
571// The `$registry` must be a `prometheus_filtered::Registry`. Each macro builds
572// the prometheus metric unregistered, then hands it to
573// [`Registry::register_filtered`], which owns registration and returns the
574// metric. The filter never turns a successful construction into an `Err`.
575//
576// `$crate::prometheus::` names the metric constructors so callers don't need a
577// direct `prometheus` crate dependency.
578//
579// `let _n = $name; let name: &str = &*_n;` handles both `&str` literals and
580// `format!(...)` String expressions uniformly.
581
582/// register_int_counter_with_registry!(name, help, registry)
583#[macro_export]
584macro_rules! register_int_counter_with_registry {
585    ($name:expr, $help:expr, $registry:expr $(,)?) => {
586        $crate::register_int_counter_with_registry!(
587            $name, $help, $registry; $crate::MetricLevel::Debug
588        )
589    };
590    ($name:expr, $help:expr, $registry:expr ; $level:expr $(,)?) => {{
591        let _n = $name;
592        let name: &str = &*_n;
593        let module: &str = module_path!();
594        $crate::prometheus::IntCounter::new(name, $help)
595            .and_then(|metric| ($registry).register_filtered(name, module, $level, metric))
596    }};
597}
598
599/// register_int_counter_vec_with_registry!(name, help, labels, registry)
600#[macro_export]
601macro_rules! register_int_counter_vec_with_registry {
602    ($name:expr, $help:expr, $labels:expr, $registry:expr $(,)?) => {
603        $crate::register_int_counter_vec_with_registry!(
604            $name, $help, $labels, $registry; $crate::MetricLevel::Debug
605        )
606    };
607    ($name:expr, $help:expr, $labels:expr, $registry:expr ; $level:expr $(,)?) => {{
608        let _n = $name;
609        let name: &str = &*_n;
610        let module: &str = module_path!();
611        $crate::prometheus::IntCounterVec::new($crate::prometheus::Opts::new(name, $help), $labels)
612            .and_then(|metric| ($registry).register_filtered(name, module, $level, metric))
613    }};
614}
615
616/// register_int_gauge_with_registry!(name, help, registry)
617#[macro_export]
618macro_rules! register_int_gauge_with_registry {
619    ($name:expr, $help:expr, $registry:expr $(,)?) => {
620        $crate::register_int_gauge_with_registry!(
621            $name, $help, $registry; $crate::MetricLevel::Debug
622        )
623    };
624    ($name:expr, $help:expr, $registry:expr ; $level:expr $(,)?) => {{
625        let _n = $name;
626        let name: &str = &*_n;
627        let module: &str = module_path!();
628        $crate::prometheus::IntGauge::new(name, $help)
629            .and_then(|metric| ($registry).register_filtered(name, module, $level, metric))
630    }};
631}
632
633/// register_int_gauge_vec_with_registry!(name, help, labels, registry)
634#[macro_export]
635macro_rules! register_int_gauge_vec_with_registry {
636    ($name:expr, $help:expr, $labels:expr, $registry:expr $(,)?) => {
637        $crate::register_int_gauge_vec_with_registry!(
638            $name, $help, $labels, $registry; $crate::MetricLevel::Debug
639        )
640    };
641    ($name:expr, $help:expr, $labels:expr, $registry:expr ; $level:expr $(,)?) => {{
642        let _n = $name;
643        let name: &str = &*_n;
644        let module: &str = module_path!();
645        $crate::prometheus::IntGaugeVec::new($crate::prometheus::Opts::new(name, $help), $labels)
646            .and_then(|metric| ($registry).register_filtered(name, module, $level, metric))
647    }};
648}
649
650/// register_histogram_with_registry!(name, help, registry)
651/// register_histogram_with_registry!(name, help, buckets, registry)
652#[macro_export]
653macro_rules! register_histogram_with_registry {
654    ($name:expr, $help:expr, $registry:expr $(,)?) => {
655        $crate::register_histogram_with_registry!($name, $help, $registry; $crate::MetricLevel::Debug)
656    };
657    ($name:expr, $help:expr, $buckets:expr, $registry:expr $(,)?) => {
658        $crate::register_histogram_with_registry!(
659            $name, $help, $buckets, $registry; $crate::MetricLevel::Debug
660        )
661    };
662    ($name:expr, $help:expr, $registry:expr ; $level:expr $(,)?) => {{
663        let _n = $name;
664        let name: &str = &*_n;
665        let module: &str = module_path!();
666        $crate::prometheus::Histogram::with_opts($crate::prometheus::HistogramOpts::new(name, $help))
667            .and_then(|metric| ($registry).register_filtered(name, module, $level, metric))
668    }};
669    ($name:expr, $help:expr, $buckets:expr, $registry:expr ; $level:expr $(,)?) => {{
670        let _n = $name;
671        let name: &str = &*_n;
672        let module: &str = module_path!();
673        $crate::prometheus::Histogram::with_opts(
674            $crate::prometheus::HistogramOpts::new(name, $help).buckets($buckets),
675        )
676        .and_then(|metric| ($registry).register_filtered(name, module, $level, metric))
677    }};
678}
679
680/// register_histogram_vec_with_registry!(name, help, labels, registry)
681/// register_histogram_vec_with_registry!(name, help, labels, buckets, registry)
682#[macro_export]
683macro_rules! register_histogram_vec_with_registry {
684    ($name:expr, $help:expr, $labels:expr, $registry:expr $(,)?) => {
685        $crate::register_histogram_vec_with_registry!(
686            $name, $help, $labels, $registry; $crate::MetricLevel::Debug
687        )
688    };
689    ($name:expr, $help:expr, $labels:expr, $buckets:expr, $registry:expr $(,)?) => {
690        $crate::register_histogram_vec_with_registry!(
691            $name, $help, $labels, $buckets, $registry; $crate::MetricLevel::Debug
692        )
693    };
694    ($name:expr, $help:expr, $labels:expr, $registry:expr ; $level:expr $(,)?) => {{
695        let _n = $name;
696        let name: &str = &*_n;
697        let module: &str = module_path!();
698        $crate::prometheus::HistogramVec::new(
699            $crate::prometheus::HistogramOpts::new(name, $help),
700            $labels,
701        )
702        .and_then(|metric| ($registry).register_filtered(name, module, $level, metric))
703    }};
704    ($name:expr, $help:expr, $labels:expr, $buckets:expr, $registry:expr ; $level:expr $(,)?) => {{
705        let _n = $name;
706        let name: &str = &*_n;
707        let module: &str = module_path!();
708        $crate::prometheus::HistogramVec::new(
709            $crate::prometheus::HistogramOpts::new(name, $help).buckets($buckets),
710            $labels,
711        )
712        .and_then(|metric| ($registry).register_filtered(name, module, $level, metric))
713    }};
714}
715
716/// register_gauge_vec_with_registry!(name, help, labels, registry)
717#[macro_export]
718macro_rules! register_gauge_vec_with_registry {
719    ($name:expr, $help:expr, $labels:expr, $registry:expr $(,)?) => {
720        $crate::register_gauge_vec_with_registry!(
721            $name, $help, $labels, $registry; $crate::MetricLevel::Debug
722        )
723    };
724    ($name:expr, $help:expr, $labels:expr, $registry:expr ; $level:expr $(,)?) => {{
725        let _n = $name;
726        let name: &str = &*_n;
727        let module: &str = module_path!();
728        $crate::prometheus::GaugeVec::new($crate::prometheus::Opts::new(name, $help), $labels)
729            .and_then(|metric| ($registry).register_filtered(name, module, $level, metric))
730    }};
731}
732
733/// register_gauge_with_registry!(name, help, registry)
734#[macro_export]
735macro_rules! register_gauge_with_registry {
736    ($name:expr, $help:expr, $registry:expr $(,)?) => {
737        $crate::register_gauge_with_registry!($name, $help, $registry; $crate::MetricLevel::Debug)
738    };
739    ($name:expr, $help:expr, $registry:expr ; $level:expr $(,)?) => {{
740        let _n = $name;
741        let name: &str = &*_n;
742        let module: &str = module_path!();
743        $crate::prometheus::Gauge::new(name, $help)
744            .and_then(|metric| ($registry).register_filtered(name, module, $level, metric))
745    }};
746}
747
748/// register_counter!(name, help) - global prometheus registry, filtered.
749#[macro_export]
750macro_rules! register_counter {
751    ($name:expr, $help:expr $(,)?) => {
752        $crate::register_counter!($name, $help; $crate::MetricLevel::Debug)
753    };
754    ($name:expr, $help:expr ; $level:expr $(,)?) => {{
755        let _n = $name;
756        let name: &str = &*_n;
757        let module: &str = module_path!();
758        $crate::prometheus::Counter::new(name, $help).and_then(|metric| {
759            $crate::default_registry().register_filtered(name, module, $level, metric)
760        })
761    }};
762}
763
764/// register_counter_with_registry!(name, help, registry)
765#[macro_export]
766macro_rules! register_counter_with_registry {
767    ($name:expr, $help:expr, $registry:expr $(,)?) => {
768        $crate::register_counter_with_registry!(
769            $name, $help, $registry; $crate::MetricLevel::Debug
770        )
771    };
772    ($name:expr, $help:expr, $registry:expr ; $level:expr $(,)?) => {{
773        let _n = $name;
774        let name: &str = &*_n;
775        let module: &str = module_path!();
776        $crate::prometheus::Counter::new(name, $help)
777            .and_then(|metric| ($registry).register_filtered(name, module, $level, metric))
778    }};
779}
780
781/// register_counter_vec_with_registry!(name, help, labels, registry)
782#[macro_export]
783macro_rules! register_counter_vec_with_registry {
784    ($name:expr, $help:expr, $labels:expr, $registry:expr $(,)?) => {
785        $crate::register_counter_vec_with_registry!(
786            $name, $help, $labels, $registry; $crate::MetricLevel::Debug
787        )
788    };
789    ($name:expr, $help:expr, $labels:expr, $registry:expr ; $level:expr $(,)?) => {{
790        let _n = $name;
791        let name: &str = &*_n;
792        let module: &str = module_path!();
793        $crate::prometheus::CounterVec::new($crate::prometheus::Opts::new(name, $help), $labels)
794            .and_then(|metric| {
795                ($registry).register_filtered(name, module, $level, metric)
796            })
797    }};
798}
799
800/// register_counter_vec!(name, help, labels) - global registry, filtered.
801#[macro_export]
802macro_rules! register_counter_vec {
803    ($name:expr, $help:expr, $labels:expr $(,)?) => {
804        $crate::register_counter_vec!($name, $help, $labels; $crate::MetricLevel::Debug)
805    };
806    ($name:expr, $help:expr, $labels:expr ; $level:expr $(,)?) => {{
807        let _n = $name;
808        let name: &str = &*_n;
809        let module: &str = module_path!();
810        $crate::prometheus::CounterVec::new($crate::prometheus::Opts::new(name, $help), $labels)
811            .and_then(|metric| {
812                $crate::default_registry().register_filtered(name, module, $level, metric)
813            })
814    }};
815}
816
817/// register_histogram_vec!(opts, labels) or (name, help, labels) or (name,
818/// help, labels, buckets) — global prometheus registry, filtered.
819#[macro_export]
820macro_rules! register_histogram_vec {
821    ($opts:expr, $labels:expr $(,)?) => {
822        $crate::register_histogram_vec!($opts, $labels; $crate::MetricLevel::Debug)
823    };
824    ($name:expr, $help:expr, $labels:expr $(,)?) => {
825        $crate::register_histogram_vec!($name, $help, $labels; $crate::MetricLevel::Debug)
826    };
827    ($name:expr, $help:expr, $labels:expr, $buckets:expr $(,)?) => {
828        $crate::register_histogram_vec!(
829            $name, $help, $labels, $buckets; $crate::MetricLevel::Debug
830        )
831    };
832    ($opts:expr, $labels:expr ; $level:expr $(,)?) => {{
833        let opts = $opts;
834        let name = opts.common_opts.name.clone();
835        let name: &str = &name;
836        let module: &str = module_path!();
837        $crate::prometheus::HistogramVec::new(opts, $labels).and_then(|metric| {
838            $crate::default_registry().register_filtered(name, module, $level, metric)
839        })
840    }};
841    ($name:expr, $help:expr, $labels:expr ; $level:expr $(,)?) => {{
842        let _n = $name;
843        let name: &str = &*_n;
844        let module: &str = module_path!();
845        $crate::prometheus::HistogramVec::new(
846            $crate::prometheus::HistogramOpts::new(name, $help),
847            $labels,
848        )
849        .and_then(|metric| {
850            $crate::default_registry().register_filtered(name, module, $level, metric)
851        })
852    }};
853    ($name:expr, $help:expr, $labels:expr, $buckets:expr ; $level:expr $(,)?) => {{
854        let _n = $name;
855        let name: &str = &*_n;
856        let module: &str = module_path!();
857        $crate::prometheus::HistogramVec::new(
858            $crate::prometheus::HistogramOpts::new(name, $help).buckets($buckets),
859            $labels,
860        )
861        .and_then(|metric| {
862            $crate::default_registry().register_filtered(name, module, $level, metric)
863        })
864    }};
865}
866
867#[cfg(test)]
868mod tests {
869    use super::MetricLevel::Debug;
870
871    #[test]
872    fn filter_matches_metric_or_module_name_prefix() {
873        // An `off` directive hides exactly the metrics its pattern matches;
874        // unmatched metrics stay exposed (the permissive default).
875        let filter = super::Filter::parse("authority=off");
876        assert!(filter.is_exposed("some_authority", "iota_core::checkpoints", Debug));
877        assert!(!filter.is_exposed("authority", "iota_core::checkpoints", Debug));
878        assert!(!filter.is_exposed("authority_aggregator", "iota_core::checkpoints", Debug));
879        assert!(filter.is_exposed("certs_total", "iota_core::some_authority", Debug));
880        assert!(!filter.is_exposed("certs_total", "iota_core::authority", Debug));
881        assert!(!filter.is_exposed("certs_total", "iota_core::authority_aggregator", Debug));
882
883        // the longer matching prefix shadows the shorter one
884        let filter = super::Filter::parse("authority=off,authority_aggregator=trace");
885        assert!(!filter.is_exposed("authority", "iota_core::checkpoints", Debug));
886        assert!(filter.is_exposed("authority_aggregator", "iota_core::checkpoints", Debug));
887        assert!(!filter.is_exposed("certs_total", "iota_core::authority", Debug));
888        assert!(filter.is_exposed("certs_total", "iota_core::authority_aggregator", Debug));
889
890        // filter can be set off by default
891        let filter = super::Filter::parse("off,authority_aggregator=trace");
892        assert!(!filter.is_exposed("some_authority", "iota_core::checkpoints", Debug));
893        assert!(!filter.is_exposed("authority", "iota_core::checkpoints", Debug));
894        assert!(filter.is_exposed("authority_aggregator", "iota_core::checkpoints", Debug));
895        assert!(!filter.is_exposed("certs_total", "iota_core::some_authority", Debug));
896        assert!(!filter.is_exposed("certs_total", "iota_core::authority", Debug));
897        assert!(filter.is_exposed("certs_total", "iota_core::authority_aggregator", Debug));
898
899        // the full prefix must be matched
900        let filter = super::Filter::parse("authority_aggregator=off");
901        assert!(filter.is_exposed("authority", "iota_core::checkpoints", Debug));
902        assert!(!filter.is_exposed("authority_aggregator", "iota_core::checkpoints", Debug));
903        assert!(filter.is_exposed("certs_total", "iota_core::authority", Debug));
904        assert!(!filter.is_exposed("certs_total", "iota_core::authority_aggregator", Debug));
905
906        // a pattern that is a prefix of the full module path (not only a `::`
907        // component) matches.
908        let filter = super::Filter::parse("iota_core=off");
909        assert!(!filter.is_exposed("certs_total", "iota_core::authority", Debug));
910        assert!(filter.is_exposed("certs_total", "starfish::core", Debug));
911    }
912
913    #[test]
914    fn more_specific_pattern_wins_regardless_of_order() {
915        use super::MetricLevel::{Info, Trace, Warn};
916        // A blanket module directive does not shadow a more specific one,
917        // whichever is written first ...
918        for input in [
919            "iota_core::authority=warn,iota_core=off",
920            "iota_core=off,iota_core::authority=warn",
921        ] {
922            let filter = super::Filter::parse(input);
923            assert!(
924                filter.is_exposed("x", "iota_core::authority", Warn),
925                "{input}"
926            );
927            assert!(
928                !filter.is_exposed("x", "iota_core::checkpoints", Warn),
929                "{input}"
930            );
931        }
932        // ... and a trailing bare level does not cancel earlier specific
933        // directives.
934        let filter = super::Filter::parse("authority=off,info");
935        assert!(!filter.is_exposed("authority", "m", Warn));
936        assert!(filter.is_exposed("certs_total", "m", Info));
937        assert!(!filter.is_exposed("certs_total", "m", Debug));
938        // Among directives with the same pattern the last one wins, whether
939        // the pattern is empty or not.
940        assert!(super::Filter::parse("off,trace").is_exposed("authority", "m", Debug));
941        assert!(
942            super::Filter::parse("authority=off,authority=trace").is_exposed(
943                "authority",
944                "m",
945                Debug
946            )
947        );
948        // A metric-name match beats a module match of any length (a name
949        // pattern can never be longer than the metric name), whichever is
950        // written first.
951        for input in [
952            "certs_total=trace,iota_core::execution_cache=warn",
953            "iota_core::execution_cache=warn,certs_total=trace",
954        ] {
955            let filter = super::Filter::parse(input);
956            assert!(
957                filter.is_exposed("certs_total", "iota_core::execution_cache", Trace),
958                "{input}"
959            );
960            // other metrics in the module keep the module directive's level.
961            assert!(
962                !filter.is_exposed("other_metric", "iota_core::execution_cache", Debug),
963                "{input}"
964            );
965        }
966        // The same holds when the name directive hides instead of exposes.
967        let filter = super::Filter::parse("iota_core::execution_cache=trace,certs_total=off");
968        assert!(!filter.is_exposed("certs_total", "iota_core::execution_cache", Warn));
969    }
970
971    #[test]
972    fn env_directives_merge_over_config_into_one_startup_filter() {
973        use super::MetricLevel::{Info, Trace, Warn};
974        // An env directive replaces the config directive with the same
975        // pattern; where the patterns differ, the most specific matching one
976        // decides each metric, whichever source it came from ...
977        let filter = super::Filter::from_sources(
978            super::FilterSource::new("iota_core::authority=off,starfish=warn"),
979            Some(super::FilterSource::new("iota_core=info,starfish=info")),
980        );
981        assert!(!filter.is_exposed("x", "iota_core::authority", Warn));
982        assert!(filter.is_exposed("x", "iota_core::checkpoints", Info));
983        assert!(!filter.is_exposed("x", "iota_core::checkpoints", Debug));
984        // ... same pattern: the env directive replaces the config's.
985        assert!(filter.is_exposed("x", "starfish::core", Info));
986        assert!(!filter.is_exposed("x", "starfish::core", Debug));
987        // The two sources collapse into a single startup string.
988        assert_eq!(
989            filter.startup_filter_string(),
990            "iota_core::authority=off,iota_core=info,starfish=info"
991        );
992
993        // An env `default=LEVEL` directive replaces the config's global
994        // default, while the config's more specific directives keep
995        // applying.
996        let filter = super::Filter::from_sources(
997            super::FilterSource::new("default=info,iota_core=warn"),
998            Some(super::FilterSource::new("default=trace")),
999        );
1000        assert!(filter.is_exposed("x", "iota_core::authority", Warn));
1001        assert!(!filter.is_exposed("x", "iota_core::authority", Info));
1002        assert!(filter.is_exposed("x", "m", Trace));
1003        assert_eq!(
1004            filter.startup_filter_string(),
1005            "iota_core=warn,default=trace"
1006        );
1007
1008        // A bare env level replaces the config's directives entirely:
1009        // `METRICS_FILTER=trace` exposes everything.
1010        let filter = super::Filter::from_sources(
1011            super::FilterSource::new("default=info,iota_core=warn"),
1012            Some(super::FilterSource::new("trace")),
1013        );
1014        assert!(filter.is_exposed("x", "iota_core::authority", Trace));
1015        assert!(filter.is_exposed("x", "m", Trace));
1016        // The bare spelling is kept in the echo, so the reported string
1017        // replays as a replacement, not a merge.
1018        assert_eq!(filter.startup_filter_string(), "trace");
1019
1020        // A blank env var contributes no directives, so the config directives
1021        // still apply.
1022        let filter = super::Filter::from_sources(
1023            super::FilterSource::new("off"),
1024            Some(super::FilterSource::new(" ")),
1025        );
1026        assert!(!filter.is_exposed("x", "m", Warn));
1027    }
1028
1029    #[test]
1030    fn unmatched_metrics_are_exposed() {
1031        use super::MetricLevel::{Info, Trace, Warn};
1032        // Filtering is opt-in: with no matching directive every metric is
1033        // exposed, matching plain `prometheus` behaviour.
1034        let mut filters = vec![
1035            super::Filter::parse(""),
1036            super::Filter::default(),
1037            // empty segments are ignored rather than treated as directives.
1038            super::Filter::parse(",,"),
1039        ];
1040        // A set env var would add env directives, so `Filter::from_env` is
1041        // only exercised when it is unset; `Filter::from_sources` covers the
1042        // set case.
1043        if std::env::var_os(super::METRICS_FILTER_ENV).is_none() {
1044            filters.push(super::Filter::from_env());
1045        }
1046        for filter in filters {
1047            assert!(filter.is_exposed("anything", "any::module", Warn));
1048            assert!(filter.is_exposed("anything", "any::module", Info));
1049            assert!(filter.is_exposed("anything", "any::module", Debug));
1050            assert!(filter.is_exposed("anything", "any::module", Trace));
1051        }
1052    }
1053
1054    #[test]
1055    fn invalid_directives_are_dropped() {
1056        use super::MetricLevel::Trace;
1057        // An unrecognised value leaves the directive out, falling back to the
1058        // permissive default. Only the RUST_LOG-style level names are
1059        // accepted; the former `on`/`true`/`1` and `false`/`0` aliases are
1060        // invalid too.
1061        for level in ["maybe", "on", "true", "1", "false", "0"] {
1062            let filter = super::Filter::parse(&format!("authority={level}"));
1063            assert!(
1064                filter.is_exposed("authority", "m", Trace),
1065                "{level} should be dropped as invalid, leaving the default"
1066            );
1067        }
1068        // a bare token without `=LEVEL` is parsed as a global value and, being
1069        // invalid, dropped — it does NOT enable/disable the `authority` subsystem.
1070        assert!(super::Filter::parse("authority").is_exposed("authority", "m", Trace));
1071        // a valid directive alongside an invalid one still takes effect.
1072        let filter = super::Filter::parse("authority=off,bogus=nope");
1073        assert!(!filter.is_exposed("authority", "m", Debug));
1074    }
1075
1076    #[test]
1077    fn whitespace_is_trimmed() {
1078        let filter = super::Filter::parse("  authority = off ,  authority_aggregator = trace  ");
1079        assert!(!filter.is_exposed("authority", "m", Debug));
1080        assert!(filter.is_exposed("authority_aggregator", "m", Debug));
1081    }
1082
1083    #[test]
1084    fn registries_built_to_share_a_filter_see_the_same_decisions() {
1085        use super::{Arc, Filter, MetricLevel, Registry};
1086
1087        let filter = Arc::new(Filter::parse("off,authority=trace"));
1088        assert!(filter.is_exposed("authority", "m", MetricLevel::Debug));
1089        assert!(!filter.is_exposed("consensus", "m", MetricLevel::Debug));
1090        let registry = Registry::new_custom(None, None, Some(filter.clone())).unwrap();
1091        let shared = Registry::new_custom(None, None, Some(filter)).unwrap();
1092        assert!(std::sync::Arc::ptr_eq(&registry.filter(), &shared.filter()));
1093    }
1094
1095    #[test]
1096    fn level_thresholds() {
1097        use super::MetricLevel::{Debug, Info, Trace, Warn};
1098        // `warn` threshold exposes only warn metrics.
1099        let f = super::Filter::parse("authority=warn");
1100        assert!(f.is_exposed("x", "iota_core::authority", Warn));
1101        assert!(!f.is_exposed("x", "iota_core::authority", Info));
1102        assert!(!f.is_exposed("x", "iota_core::authority", Debug));
1103        // `info` threshold exposes warn+info, hides debug.
1104        let f = super::Filter::parse("authority=info");
1105        assert!(f.is_exposed("x", "iota_core::authority", Warn));
1106        assert!(f.is_exposed("x", "iota_core::authority", Info));
1107        assert!(!f.is_exposed("x", "iota_core::authority", Debug));
1108        // `debug` exposes everything untagged and below, but not trace.
1109        let f = super::Filter::parse("authority=debug");
1110        assert!(f.is_exposed("x", "iota_core::authority", Debug));
1111        assert!(!f.is_exposed("x", "iota_core::authority", Trace));
1112        // `trace` exposes everything.
1113        let f = super::Filter::parse("authority=trace");
1114        assert!(f.is_exposed("x", "iota_core::authority", Trace));
1115        // `off` exposes nothing.
1116        assert!(!super::Filter::parse("authority=off").is_exposed(
1117            "x",
1118            "iota_core::authority",
1119            Warn
1120        ));
1121        // No directive -> exposed at every level.
1122        assert!(super::Filter::parse("").is_exposed("x", "m", Info));
1123        assert!(super::Filter::parse("").is_exposed("x", "m", Trace));
1124    }
1125}
1126
1127#[cfg(test)]
1128mod test_helpers {
1129    use super::{Filter, Registry};
1130
1131    pub fn registry(filter: &str) -> Registry {
1132        Registry::new_custom(None, None, Some(std::sync::Arc::new(Filter::parse(filter)))).unwrap()
1133    }
1134
1135    pub fn gathered_names(registry: &Registry) -> Vec<String> {
1136        let mut names: Vec<_> = registry
1137            .gather()
1138            .iter()
1139            .map(|f| f.name().to_owned())
1140            .collect();
1141        names.sort();
1142        names
1143    }
1144}
1145
1146#[cfg(test)]
1147mod gather_filter_tests {
1148    use super::{
1149        Filter, MetricLevel, Registry,
1150        test_helpers::{gathered_names, registry},
1151    };
1152
1153    #[test]
1154    fn gather_applies_level_thresholds_by_module() {
1155        // Metrics register in this module (`prometheus_filtered::gather_filter_tests`).
1156        let reg = registry("gather_filter_tests=warn");
1157        crate::register_int_gauge_with_registry!("g_warn", "h", &reg; MetricLevel::Warn).unwrap();
1158        let g_debug = crate::register_int_gauge_with_registry!("g_debug", "h", &reg).unwrap();
1159        g_debug.set(7);
1160
1161        // Only the warn-tagged metric is exposed; the debug one is registered
1162        // and keeps collecting.
1163        assert_eq!(gathered_names(&reg), ["g_warn"]);
1164        assert_eq!(g_debug.get(), 7);
1165    }
1166
1167    #[test]
1168    fn duplicate_name_is_rejected_even_when_filtered_out() {
1169        // The `off` directive hides the metric from gather, but its collector
1170        // stays registered, so a second registration of the same name is
1171        // still rejected by the inner registry's descriptor check.
1172        let reg = registry("g_dup=off");
1173        crate::register_int_gauge_with_registry!("g_dup", "h", &reg; MetricLevel::Warn).unwrap();
1174        let err = crate::register_int_gauge_with_registry!("g_dup", "h", &reg; MetricLevel::Warn)
1175            .unwrap_err();
1176        assert!(
1177            matches!(err, prometheus::Error::AlreadyReg),
1178            "unexpected error: {err:?}"
1179        );
1180    }
1181
1182    #[test]
1183    fn prefixed_registry_records_exposed_family_names() {
1184        let exposed = Registry::new_custom(
1185            Some("consensus".to_owned()),
1186            None,
1187            Some(std::sync::Arc::new(Filter::parse(""))),
1188        )
1189        .unwrap();
1190        crate::register_int_gauge_with_registry!("g", "h", &exposed; MetricLevel::Warn).unwrap();
1191        assert_eq!(gathered_names(&exposed), ["consensus_g"]);
1192
1193        // The filter keys on the module path, so the prefixed family is
1194        // matched and hidden even though its gathered name differs.
1195        let hidden = Registry::new_custom(
1196            Some("consensus".to_owned()),
1197            None,
1198            Some(std::sync::Arc::new(Filter::parse(
1199                "gather_filter_tests=off",
1200            ))),
1201        )
1202        .unwrap();
1203        crate::register_int_gauge_with_registry!("g", "h", &hidden; MetricLevel::Warn).unwrap();
1204        assert_eq!(gathered_names(&hidden), Vec::<String>::new());
1205    }
1206
1207    #[test]
1208    fn directly_registered_collectors_bypass_filter() {
1209        let reg = registry("off");
1210        crate::register_int_gauge_with_registry!("g_macro", "h", &reg).unwrap();
1211        let gauge = prometheus::IntGauge::new("g_direct", "h").unwrap();
1212        reg.register(Box::new(gauge)).unwrap();
1213
1214        // Not registered through the macros -> no module/level recorded ->
1215        // the exposure filter does not apply.
1216        assert_eq!(gathered_names(&reg), ["g_direct"]);
1217    }
1218}
1219
1220#[cfg(test)]
1221mod runtime_filter_tests {
1222    use super::{
1223        Filter, FilterSource, MetricLevel, Registry,
1224        test_helpers::{gathered_names, registry},
1225    };
1226
1227    // The node drives this via `RegistryService`; exposure follows the
1228    // filter change on the next gather, with no extra step.
1229    fn set_runtime(registry: &Registry, s: &str) {
1230        registry
1231            .filter()
1232            .set_runtime_filter(FilterSource::new(s))
1233            .unwrap();
1234    }
1235
1236    fn reset_runtime(registry: &Registry) {
1237        registry.filter().reset_runtime_filter();
1238    }
1239
1240    #[test]
1241    fn raising_runtime_level_exposes_collected_metrics() {
1242        // A `warn` startup threshold hides the debug metric …
1243        let reg = registry("runtime_filter_tests=warn");
1244        crate::register_int_gauge_with_registry!("g_warn", "h", &reg; MetricLevel::Warn).unwrap();
1245        let g_debug = crate::register_int_gauge_with_registry!("g_debug", "h", &reg).unwrap();
1246        g_debug.set(7);
1247        assert_eq!(gathered_names(&reg), ["g_warn"]);
1248
1249        // … so raising the exposure level at runtime reveals it, with the
1250        // values it collected while hidden.
1251        set_runtime(&reg, "runtime_filter_tests=debug");
1252        assert_eq!(gathered_names(&reg), ["g_debug", "g_warn"]);
1253        let family = reg
1254            .gather()
1255            .into_iter()
1256            .find(|f| f.name() == "g_debug")
1257            .unwrap();
1258        assert_eq!(family.get_metric()[0].get_gauge().value() as i64, 7);
1259
1260        // The same holds for a startup `off` directive: a runtime directive
1261        // matching the metric exposes it with its collected value.
1262        let reg = registry("g_hidden=off");
1263        let g = crate::register_int_gauge_with_registry!("g_hidden", "h", &reg; MetricLevel::Warn)
1264            .unwrap();
1265        g.set(9);
1266        assert_eq!(gathered_names(&reg), Vec::<String>::new());
1267        set_runtime(&reg, "g_hidden=warn");
1268        assert_eq!(gathered_names(&reg), ["g_hidden"]);
1269        let family = &reg.gather()[0];
1270        assert_eq!(family.get_metric()[0].get_gauge().value() as i64, 9);
1271    }
1272
1273    #[test]
1274    fn runtime_override_keeps_startup_directives_for_other_patterns() {
1275        let reg = registry("g_a=off");
1276        crate::register_int_gauge_with_registry!("g_a", "h", &reg; MetricLevel::Warn).unwrap();
1277        crate::register_int_gauge_with_registry!("g_b", "h", &reg; MetricLevel::Warn).unwrap();
1278        assert_eq!(gathered_names(&reg), ["g_b"]);
1279
1280        // The override hides g_b; no override directive matches g_a, so it
1281        // keeps its startup exposure (hidden).
1282        set_runtime(&reg, "g_b=off");
1283        assert_eq!(gathered_names(&reg), Vec::<String>::new());
1284
1285        // An empty override contributes nothing, leaving the startup
1286        // directives fully in effect — and replaces the previous override
1287        // rather than accumulating with it.
1288        set_runtime(&reg, "");
1289        assert_eq!(gathered_names(&reg), ["g_b"]);
1290
1291        reset_runtime(&reg);
1292        assert_eq!(gathered_names(&reg), ["g_b"]);
1293    }
1294
1295    #[test]
1296    fn runtime_directives_replace_same_pattern_startup_directives() {
1297        use MetricLevel::{Debug, Trace, Warn};
1298
1299        // Same pattern: the override directive replaces the startup one.
1300        let filter = Filter::parse("g_a=off,g_b=warn");
1301        filter
1302            .set_runtime_filter(FilterSource::new("g_b=trace"))
1303            .unwrap();
1304        assert!(!filter.is_exposed("g_a", "m", Warn));
1305        assert!(filter.is_exposed("g_b", "m", Trace));
1306        assert_eq!(filter.filter_string(), "g_a=off,g_b=trace");
1307        // The startup filter is untouched, ready for reset.
1308        assert_eq!(filter.startup_filter_string(), "g_a=off,g_b=warn");
1309
1310        // Different patterns: the most specific matching one decides each
1311        // metric, so a more specific startup directive survives a broader
1312        // override and vice versa.
1313        let filter = Filter::parse("iota_core=warn,iota_core::authority::sub=off");
1314        filter
1315            .set_runtime_filter(FilterSource::new("iota_core::authority=trace"))
1316            .unwrap();
1317        assert!(!filter.is_exposed("x", "iota_core::authority::sub", Warn));
1318        assert!(filter.is_exposed("x", "iota_core::authority::other", Trace));
1319        assert!(!filter.is_exposed("x", "iota_core::checkpoints", Debug));
1320        assert_eq!(
1321            filter.filter_string(),
1322            "iota_core=warn,iota_core::authority::sub=off,iota_core::authority=trace"
1323        );
1324
1325        // An override `default=LEVEL` directive raises only the global
1326        // default: the more specific startup directives keep applying
1327        // beneath it, beside the override's other directives.
1328        let filter = Filter::parse("g_a=off,g_b=warn");
1329        filter
1330            .set_runtime_filter(FilterSource::new("default=trace,g_c=off"))
1331            .unwrap();
1332        assert!(!filter.is_exposed("g_a", "m", Warn));
1333        assert!(filter.is_exposed("g_b", "m", Warn));
1334        assert!(!filter.is_exposed("g_b", "m", Debug));
1335        assert!(!filter.is_exposed("g_c", "m", Warn));
1336        assert!(filter.is_exposed("other", "m", Trace));
1337        assert_eq!(
1338            filter.filter_string(),
1339            "g_a=off,g_b=warn,default=trace,g_c=off"
1340        );
1341
1342        // A bare override level instead replaces the startup directives
1343        // entirely; only its sibling directives still apply.
1344        let filter = Filter::parse("g_a=off,g_b=warn");
1345        filter
1346            .set_runtime_filter(FilterSource::new("trace,g_c=off"))
1347            .unwrap();
1348        assert!(filter.is_exposed("g_a", "m", Trace));
1349        assert!(filter.is_exposed("g_b", "m", Trace));
1350        assert!(!filter.is_exposed("g_c", "m", Warn));
1351        assert_eq!(filter.filter_string(), "trace,g_c=off");
1352
1353        // Reset restores the startup directives.
1354        filter.reset_runtime_filter();
1355        assert_eq!(filter.filter_string(), filter.startup_filter_string());
1356        assert!(!filter.is_exposed("g_a", "m", Warn));
1357    }
1358
1359    #[test]
1360    fn default_pattern_does_not_match_a_module_named_default() {
1361        use MetricLevel::Warn;
1362
1363        // `default` matches as the global default, not as a module named
1364        // "default": any real pattern is more specific.
1365        let filter = Filter::parse("default=off,p2p=warn");
1366        assert!(filter.is_exposed("x", "p2p::discovery", Warn));
1367        assert!(!filter.is_exposed("x", "other_module", Warn));
1368    }
1369
1370    #[test]
1371    fn filter_reports_startup_and_current_strings() {
1372        // Both directive sets keep the group-form display the caller
1373        // supplies, while matching uses the expanded directives.
1374        let filter = Filter::from_sources(
1375            FilterSource::with_display("iota_core::authority=off", "authority=off"),
1376            Some(FilterSource::with_display(
1377                "iota_core::checkpoints=warn",
1378                "checkpoints=warn",
1379            )),
1380        );
1381        assert_eq!(
1382            filter.startup_filter_string(),
1383            "authority=off,checkpoints=warn"
1384        );
1385        assert_eq!(filter.filter_string(), filter.startup_filter_string());
1386        assert!(!filter.is_exposed("x", "iota_core::authority", MetricLevel::Warn));
1387
1388        // An override's display replaces the same-pattern startup display
1389        // entry the same way its directives do.
1390        filter
1391            .set_runtime_filter(FilterSource::with_display(
1392                "iota_core::authority=warn",
1393                "authority=warn",
1394            ))
1395            .unwrap();
1396        assert_eq!(filter.filter_string(), "checkpoints=warn,authority=warn");
1397        assert!(filter.is_exposed("x", "iota_core::authority", MetricLevel::Warn));
1398        assert!(!filter.is_exposed("x", "iota_core::authority", MetricLevel::Debug));
1399
1400        filter.reset_runtime_filter();
1401        assert_eq!(filter.filter_string(), filter.startup_filter_string());
1402        assert!(!filter.is_exposed("x", "iota_core::authority", MetricLevel::Warn));
1403    }
1404
1405    #[test]
1406    fn filter_string_is_canonical_and_round_trips() {
1407        // Invalid startup directives are dropped, and the reported startup
1408        // string reflects the directives actually in effect — so it can
1409        // always be POSTed back through the strict runtime setter.
1410        let filter = Filter::parse("foo=bogus, typed_store=warn ,default=info");
1411        let startup = filter.startup_filter_string();
1412        assert_eq!(startup, "typed_store=warn,default=info");
1413        filter
1414            .set_runtime_filter(FilterSource::new(&startup))
1415            .unwrap();
1416        assert_eq!(filter.filter_string(), "typed_store=warn,default=info");
1417
1418        // A bare level keeps its spelling in the echo, so replaying the
1419        // string replaces the startup directives again instead of merging
1420        // over them, reproducing the same filter.
1421        let filter = Filter::parse("g_a=off");
1422        filter
1423            .set_runtime_filter(FilterSource::new("trace,g_c=off"))
1424            .unwrap();
1425        let current = filter.filter_string();
1426        assert_eq!(current, "trace,g_c=off");
1427        filter
1428            .set_runtime_filter(FilterSource::new(&current))
1429            .unwrap();
1430        assert_eq!(filter.filter_string(), "trace,g_c=off");
1431        assert!(filter.is_exposed("g_a", "m", MetricLevel::Trace));
1432    }
1433
1434    #[test]
1435    fn set_runtime_filter_rejects_invalid_directives() {
1436        let filter = Filter::parse("authority=off");
1437        let err = filter
1438            .set_runtime_filter(FilterSource::new("authority=warn,bogus=nope"))
1439            .unwrap_err();
1440        assert!(err.contains("bogus=nope"), "unexpected error: {err}");
1441        // The failed update leaves the startup directives in effect.
1442        assert_eq!(filter.filter_string(), filter.startup_filter_string());
1443        assert!(!filter.is_exposed("x", "iota_core::authority", MetricLevel::Warn));
1444    }
1445}