Skip to main content

tracing_cache/
predicate.rs

1//! Filtering predicate that decides which callsites the cache observes.
2//!
3//! `LevelPredicate` is the default and trivial implementation; downstream
4//! consumers can plug in their own `EnabledPredicate` to filter by name,
5//! target, dynamic state, etc.  The trait mirrors the four points the
6//! `tracing::Subscriber` trait checks per callsite.
7
8use std::sync::Arc;
9use std::sync::atomic::{AtomicU8, AtomicU64, Ordering};
10
11use tracing::metadata::LevelFilter;
12use tracing::{Level, Metadata};
13
14/// Mirror of `tracing::subscriber::Interest` — kept as our own type so the
15/// predicate trait isn't bound to tracing's exact type.
16pub enum Interest {
17    Never,
18    Sometimes,
19    Always,
20}
21
22pub trait EnabledPredicate: Send + Sync + 'static {
23    fn max_level_hint(&self) -> Option<LevelFilter>;
24    fn callsite_enabled(&self, metadata: &'static Metadata<'static>) -> Interest;
25    fn enabled(&self, metadata: &Metadata<'_>) -> bool;
26    fn new_span_enabled(&self, span: &tracing::span::Attributes<'_>) -> bool;
27}
28
29/// Default predicate: enables everything at or below the current
30/// `LevelFilter` setting (including `OFF` for "disable everything").
31///
32/// The level is dynamic — call [`LevelPredicate::handle`] to grab a
33/// cheap-to-clone [`LevelHandle`] that can change it from another
34/// thread/task at runtime without rebuilding the subscriber.  Every
35/// `callsite_enabled` returns `Sometimes` so tracing-core re-asks
36/// `enabled` on each event/span, picking up live changes immediately;
37/// `LevelHandle::set` additionally calls
38/// `tracing::callsite::rebuild_interest_cache` so any callsites that
39/// were registered before the level changed get re-evaluated against
40/// the new `max_level_hint`.
41pub struct LevelPredicate {
42    level: Arc<AtomicU8>,
43}
44
45impl LevelPredicate {
46    /// Construct from a `tracing::Level` (legacy callers).  See
47    /// [`with_filter`](Self::with_filter) for the `LevelFilter` form
48    /// that can also express `OFF`.
49    pub fn new(level: Level) -> Self {
50        Self::with_filter(LevelFilter::from_level(level))
51    }
52
53    /// Construct from a `LevelFilter` (the only way to start `OFF`).
54    pub fn with_filter(filter: LevelFilter) -> Self {
55        Self {
56            level: Arc::new(AtomicU8::new(filter_to_u8(filter))),
57        }
58    }
59
60    /// A cheap-to-clone handle that sets/gets the current level from
61    /// other threads/tasks — typically held by an admin RPC.
62    pub fn handle(&self) -> LevelHandle {
63        LevelHandle {
64            level: Arc::clone(&self.level),
65        }
66    }
67}
68
69/// Remote control for a [`LevelPredicate`]'s active level.  Cloning
70/// shares the same atomic — multiple owners (e.g. one per
71/// administrative connection) all see and mutate the same value.
72#[derive(Clone)]
73pub struct LevelHandle {
74    level: Arc<AtomicU8>,
75}
76
77impl LevelHandle {
78    pub fn set(&self, filter: LevelFilter) {
79        self.level.store(filter_to_u8(filter), Ordering::Release);
80        // Invalidate tracing-core's per-callsite Interest cache so
81        // callsites that were registered before the level changed get
82        // re-evaluated against the new max_level_hint.  Without this,
83        // raising the level (e.g. OFF → INFO) would leave already-
84        // -registered Never-cached callsites disabled forever.
85        tracing::callsite::rebuild_interest_cache();
86    }
87
88    pub fn get(&self) -> LevelFilter {
89        u8_to_filter(self.level.load(Ordering::Acquire))
90    }
91}
92
93impl EnabledPredicate for LevelPredicate {
94    fn max_level_hint(&self) -> Option<LevelFilter> {
95        Some(u8_to_filter(self.level.load(Ordering::Acquire)))
96    }
97
98    fn callsite_enabled(&self, metadata: &'static Metadata<'static>) -> Interest {
99        // Return Always / Never so tracing-core caches the decision
100        // per callsite — disabled callsites get short-circuited at
101        // the macro level with no further work.  `LevelHandle::set`
102        // calls `rebuild_interest_cache` to invalidate this cache
103        // whenever the level changes, so callsites get re-evaluated
104        // against the new max_level_hint.
105        let filter = u8_to_filter(self.level.load(Ordering::Acquire));
106        if filter == LevelFilter::OFF {
107            return Interest::Never;
108        }
109        if LevelFilter::from_level(*metadata.level()) <= filter {
110            Interest::Always
111        } else {
112            Interest::Never
113        }
114    }
115
116    fn enabled(&self, metadata: &Metadata<'_>) -> bool {
117        // Reached only when `callsite_enabled` returned `Sometimes`,
118        // which we never do — but tracing's contract still requires
119        // a sane answer for any path that calls it directly.
120        let filter = u8_to_filter(self.level.load(Ordering::Relaxed));
121        if filter == LevelFilter::OFF {
122            return false;
123        }
124        LevelFilter::from_level(*metadata.level()) <= filter
125    }
126
127    fn new_span_enabled(&self, span: &tracing::span::Attributes<'_>) -> bool {
128        self.enabled(span.metadata())
129    }
130}
131
132fn filter_to_u8(f: LevelFilter) -> u8 {
133    if f == LevelFilter::OFF {
134        0
135    } else if f == LevelFilter::ERROR {
136        1
137    } else if f == LevelFilter::WARN {
138        2
139    } else if f == LevelFilter::INFO {
140        3
141    } else if f == LevelFilter::DEBUG {
142        4
143    } else {
144        5 // TRACE
145    }
146}
147
148fn u8_to_filter(n: u8) -> LevelFilter {
149    match n {
150        0 => LevelFilter::OFF,
151        1 => LevelFilter::ERROR,
152        2 => LevelFilter::WARN,
153        3 => LevelFilter::INFO,
154        4 => LevelFilter::DEBUG,
155        _ => LevelFilter::TRACE,
156    }
157}
158
159// ── Chance-gated root sampling ───────────────────────────────────────────────
160
161/// Predicate wrapper that probabilistically denies root spans before
162/// they get to the inner predicate.  Root spans (the ones whose
163/// `Attributes` carry [`tracing::span::Attributes::is_root()`] —
164/// typically `tracing::span!(parent: None, …)`) are gated by a
165/// runtime-tunable percentage; descendants and events pass straight
166/// through to the inner predicate.
167///
168/// Because the chance is read with a `Relaxed` load of an
169/// `AtomicU64` holding `f64::to_bits` of the percentage, updates
170/// from a [`ChanceHandle`] are picked up by the next root-span
171/// roll without needing a wake — the inner subscriber's existing
172/// `rebuild_interest_cache` invalidation isn't required either,
173/// since the dice are rerolled per span instance via
174/// [`EnabledPredicate::new_span_enabled`], not at
175/// callsite-registration time.
176pub struct ChancePredicate<P: EnabledPredicate> {
177    /// Bit-packed `f64` percentage in `[0.0, 100.0]`.
178    chance_pct_bits: Arc<AtomicU64>,
179    inner: P,
180}
181
182impl<P: EnabledPredicate> ChancePredicate<P> {
183    /// Construct with an initial chance percentage `[0.0, 100.0]`.
184    /// Out-of-range inputs are silently clamped.  Use `100.0` for
185    /// "always pass to inner".
186    pub fn new(inner: P, chance_pct: f64) -> Self {
187        let pct = clamp_pct(chance_pct);
188        Self {
189            chance_pct_bits: Arc::new(AtomicU64::new(pct.to_bits())),
190            inner,
191        }
192    }
193
194    /// Cheap-to-clone handle for changing the chance percentage at
195    /// runtime — typically held by an admin RPC.
196    pub fn handle(&self) -> ChanceHandle {
197        ChanceHandle {
198            bits: Arc::clone(&self.chance_pct_bits),
199        }
200    }
201}
202
203/// Remote control for a [`ChancePredicate`]'s active percentage.
204/// Cloning shares the same atomic — multiple owners observe and
205/// mutate the same value.  Reads are `Relaxed` since per-span
206/// freshness is not required; updates are visible to the next roll.
207#[derive(Clone)]
208pub struct ChanceHandle {
209    bits: Arc<AtomicU64>,
210}
211
212impl ChanceHandle {
213    pub fn set(&self, pct: f64) {
214        let pct = clamp_pct(pct);
215        self.bits.store(pct.to_bits(), Ordering::Relaxed);
216    }
217
218    pub fn get(&self) -> f64 {
219        f64::from_bits(self.bits.load(Ordering::Relaxed))
220    }
221}
222
223fn clamp_pct(pct: f64) -> f64 {
224    if pct.is_nan() {
225        0.0
226    } else {
227        pct.clamp(0.0, 100.0)
228    }
229}
230
231impl<P: EnabledPredicate> EnabledPredicate for ChancePredicate<P> {
232    fn max_level_hint(&self) -> Option<LevelFilter> {
233        // Chance doesn't constrain level — defer to inner.
234        self.inner.max_level_hint()
235    }
236
237    fn callsite_enabled(&self, metadata: &'static Metadata<'static>) -> Interest {
238        // The dice roll happens per span instance in
239        // `new_span_enabled`, not per callsite — let the inner
240        // predicate decide whether the callsite is enabled.
241        self.inner.callsite_enabled(metadata)
242    }
243
244    fn enabled(&self, metadata: &Metadata<'_>) -> bool {
245        // Events are not gated by chance — only root spans are.
246        self.inner.enabled(metadata)
247    }
248
249    fn new_span_enabled(&self, span: &tracing::span::Attributes<'_>) -> bool {
250        if span.is_root() {
251            let pct = f64::from_bits(self.chance_pct_bits.load(Ordering::Relaxed));
252            if pct <= 0.0 {
253                return false;
254            }
255            if pct < 100.0 {
256                // Roll a fresh u64 / 2^64 fraction and scale to [0, 100).
257                // Per-thread fast PRNG — cheap and doesn't touch the
258                // OS RNG on the hot path.
259                let roll = rand::random::<u64>() as f64 / (u64::MAX as f64 + 1.0) * 100.0;
260                if roll >= pct {
261                    return false;
262                }
263            }
264        }
265        self.inner.new_span_enabled(span)
266    }
267}