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}