Skip to main content

elfo_logger/
scope_filter.rs

1use std::sync::Arc;
2#[cfg(feature = "tracing-log")]
3use std::sync::OnceLock;
4
5use arc_swap::ArcSwap;
6use fxhash::FxHashMap;
7use tracing::{metadata::LevelFilter, subscriber::Interest, Metadata, Subscriber};
8use tracing_subscriber::{filter::Targets, layer};
9
10use elfo_core::{logging::CheckResult, scope};
11
12use crate::{config::LoggingTargetConfig, stats};
13
14#[derive(PartialEq)]
15struct ScopeFilterConfig {
16    targets: Targets,
17}
18
19impl Default for ScopeFilterConfig {
20    fn default() -> Self {
21        Self {
22            targets: Targets::new().with_default(LevelFilter::TRACE),
23        }
24    }
25}
26
27struct Inner {
28    config: ArcSwap<ScopeFilterConfig>,
29    #[cfg(feature = "tracing-log")]
30    log_metadata_name: OnceLock<&'static str>,
31}
32
33/// A tracing layer that filters events based on the current scope's permissions
34/// and the configured targets.
35///
36/// For details see:
37/// * [Actor configuration] for per-group logging levels and rate limits.
38/// * [The logger's configuration] for limiting targets.
39///
40/// It implements both `Layer` and `Filter` traits, so it can be used either
41/// as a layer of a subscriber or as a filter of another layer.
42///
43/// [Actor configuration]: elfo_core::config::system::logging::LoggingConfig
44/// [The logger's configuration]: crate::config::Config::targets
45pub struct ScopeFilter {
46    inner: Arc<Inner>,
47}
48
49impl ScopeFilter {
50    pub(crate) fn new() -> Self {
51        Self {
52            inner: Arc::new(Inner {
53                config: ArcSwap::new(Arc::new(ScopeFilterConfig::default())),
54                #[cfg(feature = "tracing-log")]
55                log_metadata_name: OnceLock::new(),
56            }),
57        }
58    }
59
60    // I'm not sure I would like to stabilize `Clone` for now.
61    pub(crate) fn clone(&self) -> Self {
62        Self {
63            inner: Arc::clone(&self.inner),
64        }
65    }
66
67    pub(crate) fn configure(&self, targets: &FxHashMap<String, LoggingTargetConfig>) {
68        let targets = Targets::new()
69            .with_default(LevelFilter::TRACE)
70            .with_targets(
71                targets
72                    .iter()
73                    .map(|(target, target_config)| (target, target_config.max_level)),
74            );
75
76        let config = Arc::new(ScopeFilterConfig { targets });
77        let old_config = self.inner.config.swap(Arc::clone(&config));
78        if config != old_config {
79            tracing::callsite::rebuild_interest_cache();
80        }
81    }
82
83    fn interested(&self, meta: &Metadata<'_>) -> Interest {
84        let config = self.inner.config.load();
85        if config.targets.would_enable(meta.target(), meta.level()) {
86            // Not `::always()`, because actor can impose its own limits.
87            Interest::sometimes()
88        } else {
89            // Won't be ever allowed by the `.targets`.
90            Interest::never()
91        }
92    }
93
94    fn enabled(&self, meta: &Metadata<'_>) -> bool {
95        // We don't need to recheck `.targets` here, because `.register_callsite()`
96        // would already eliminate logs that would be filtered by it.
97        let level = *meta.level();
98
99        #[cfg(feature = "tracing-log")]
100        {
101            // `tracing-log` doesn't call `.register_callsite()`, so we have to recheck
102            // `.targets` in this one case.
103            let name = meta.name();
104            if let Some(&log_name) = self.inner.log_metadata_name.get() {
105                // We've already got logs from `tracing-log`, just compare str
106                // pointers for performance.
107                if std::ptr::eq(log_name, name) {
108                    let config = self.inner.config.load();
109                    if !config.targets.would_enable(meta.target(), meta.level()) {
110                        return false;
111                    }
112                }
113            } else if name == "log record" {
114                let config = self.inner.config.load();
115                if !config.targets.would_enable(meta.target(), meta.level()) {
116                    return false;
117                }
118                // That's "initialized tracing_log adapter" to set `log_metadata_name`, just
119                // ignore it. Any actual logs from `elfo_logger` would use `tracing`.
120                if meta.target() == "elfo_logger" {
121                    self.inner.log_metadata_name.set(name).ok();
122                    return false;
123                }
124            }
125        }
126
127        scope::try_with(|scope| {
128            if !scope.permissions().is_logging_enabled(level) {
129                return false;
130            }
131
132            match scope.logging().check(meta) {
133                CheckResult::Passed => true,
134                CheckResult::NotInterested => false,
135                CheckResult::Limited => {
136                    stats::counter_per_level("elfo_limited_events_total", level);
137                    false
138                }
139            }
140        })
141        // `INFO` is a global cap for non-actor logs.
142        .unwrap_or(level <= LevelFilter::INFO)
143    }
144}
145
146impl<S: Subscriber> layer::Layer<S> for ScopeFilter {
147    fn register_callsite(&self, meta: &'static Metadata<'static>) -> Interest {
148        self.interested(meta)
149    }
150
151    fn enabled(&self, meta: &Metadata<'_>, _cx: layer::Context<'_, S>) -> bool {
152        self.enabled(meta)
153    }
154
155    // TODO: global max level and `max_level_hint()`.
156}
157
158// TODO: check either `rebuild_interest_cache` work here or not.
159impl<S> layer::Filter<S> for ScopeFilter {
160    fn callsite_enabled(&self, meta: &'static Metadata<'static>) -> Interest {
161        self.interested(meta)
162    }
163
164    fn enabled(&self, meta: &Metadata<'_>, _cx: &layer::Context<'_, S>) -> bool {
165        // Even though we are implementing `callsite_enabled`, we must still provide a
166        // working implementation of `enabled`, as returning `Interest::always()` or
167        // `Interest::never()` will *allow* caching, but will not *guarantee* it.
168        // Other filters may still return `Interest::sometimes()`, so we may be
169        // asked again in `enabled`.
170        !self.interested(meta).is_never() && self.enabled(meta)
171    }
172
173    // TODO: global max level and `max_level_hint()`.
174}