Skip to main content

obs_core/
forensic.rs

1//! Per-callsite rate limiter used by the `obs::forensic!` macro.
2//!
3//! Spec 11 § 6.3 / spec 13 § 8: forensic emits are an emergency
4//! escape hatch; they bypass head sampling but **must** be capped
5//! per-callsite so a single buggy caller cannot drown the LOG tier.
6//! We use `governor::DefaultDirectRateLimiter` (token bucket; std
7//! clock; default 1 emit/sec, burst 5).
8
9use std::{
10    num::NonZeroU32,
11    sync::{Arc, OnceLock},
12};
13
14use governor::{
15    Quota, RateLimiter,
16    clock::DefaultClock,
17    state::{InMemoryState, NotKeyed},
18};
19
20/// Concrete rate-limiter type used per callsite.
21pub type ForensicLimiter =
22    RateLimiter<NotKeyed, InMemoryState, DefaultClock, governor::middleware::NoOpMiddleware>;
23
24/// Default quota — 1 emit/sec with a burst of 5. Spec 11 § 6.3 lists
25/// "rate-limited per (file, line)" without precise numbers; this
26/// matches what observability vendors typically allow before paging.
27#[must_use]
28pub fn default_forensic_quota() -> Quota {
29    // The unwraps below are compile-time provable: `NonZeroU32::new`
30    // on a non-zero literal cannot return `None`. We can't currently
31    // express that to the type system without `unsafe`, which the
32    // crate forbids. Allow the lint locally with a justifying
33    // comment per CLAUDE.md "Code Quality" guidance.
34    #[allow(clippy::unwrap_used)]
35    let per_sec = NonZeroU32::new(1).unwrap();
36    #[allow(clippy::unwrap_used)]
37    let burst = NonZeroU32::new(5).unwrap();
38    Quota::per_second(per_sec).allow_burst(burst)
39}
40
41/// Per-callsite limiter accessor. The macro expansion stores a
42/// [`OnceLock<Arc<ForensicLimiter>>`] in a static; the first call
43/// constructs the limiter under the default quota.
44pub fn ensure_limiter(
45    slot: &'static OnceLock<Arc<ForensicLimiter>>,
46) -> &'static Arc<ForensicLimiter> {
47    slot.get_or_init(|| Arc::new(RateLimiter::direct(default_forensic_quota())))
48}
49
50/// Returns `true` if the callsite is currently allowed to emit; the
51/// macro silently drops the call when this returns `false`.
52pub fn try_acquire_forensic(slot: &'static OnceLock<Arc<ForensicLimiter>>) -> bool {
53    let l = ensure_limiter(slot);
54    l.check().is_ok()
55}
56
57#[cfg(test)]
58mod tests {
59    use super::*;
60
61    #[test]
62    fn test_default_quota_should_allow_burst() {
63        static SLOT: OnceLock<Arc<ForensicLimiter>> = OnceLock::new();
64        assert!(try_acquire_forensic(&SLOT));
65    }
66
67    #[test]
68    fn test_per_callsite_isolation() {
69        static A: OnceLock<Arc<ForensicLimiter>> = OnceLock::new();
70        static B: OnceLock<Arc<ForensicLimiter>> = OnceLock::new();
71        // Burst is 5; we should be able to fire up to 5 from each
72        // independently before the limiter says no.
73        let mut fires_a = 0;
74        for _ in 0..5 {
75            if try_acquire_forensic(&A) {
76                fires_a += 1;
77            }
78        }
79        let mut fires_b = 0;
80        for _ in 0..5 {
81            if try_acquire_forensic(&B) {
82                fires_b += 1;
83            }
84        }
85        assert_eq!(fires_a, 5);
86        assert_eq!(fires_b, 5);
87    }
88}