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}