Skip to main content

rskit_logging/
sampling.rs

1//! Rate-based log sampling layer.
2//!
3//! Limits log throughput per level to prevent log storms in production.
4//! After an initial burst of messages the layer drops a configurable fraction of events,
5//! keeping resource usage predictable.
6//!
7//! # Example
8//!
9//! ```rust
10//! use rskit_logging::sampling::SamplingConfig;
11//!
12//! let cfg = SamplingConfig { enabled: true, initial_rate: 50, thereafter_rate: 10 };
13//! assert!(cfg.enabled);
14//! ```
15
16use std::collections::HashMap;
17use std::time::Instant;
18
19use parking_lot::Mutex;
20use tracing::level_filters::LevelFilter;
21use tracing::{Level, Subscriber};
22use tracing_subscriber::Layer;
23use tracing_subscriber::layer::Context;
24use tracing_subscriber::registry::LookupSpan;
25
26// ── Config ──────────────────────────────────────────────────────────────────
27
28/// Configuration for log sampling.
29#[derive(Debug, Clone)]
30pub struct SamplingConfig {
31    /// Master switch — when `false` the layer passes all events through.
32    pub enabled: bool,
33    /// Allow the first N events per second per level before sampling kicks in.
34    pub initial_rate: u32,
35    /// After the burst, allow every Nth event (1 = keep all, 2 = keep 50 %).
36    pub thereafter_rate: u32,
37}
38
39impl Default for SamplingConfig {
40    fn default() -> Self {
41        Self {
42            enabled: false,
43            initial_rate: 100,
44            thereafter_rate: 100,
45        }
46    }
47}
48
49// ── Per-level counter ───────────────────────────────────────────────────────
50
51/// Tracks event count within a one-second window.
52#[derive(Debug)]
53struct LevelCounter {
54    window_start: Instant,
55    count: u32,
56}
57
58impl LevelCounter {
59    fn new() -> Self {
60        Self {
61            window_start: Instant::now(),
62            count: 0,
63        }
64    }
65}
66
67// ── Layer ───────────────────────────────────────────────────────────────────
68
69/// A [`tracing_subscriber::Layer`] that drops events exceeding per-level rate limits.
70///
71/// Events within the initial burst (`initial_rate` per second per level) are always passed through.
72/// After the burst, only every `thereafter_rate`-th event is kept.
73pub struct SamplingLayer {
74    initial_rate: u32,
75    thereafter_rate: u32,
76    counters: Mutex<HashMap<Level, LevelCounter>>,
77}
78
79impl SamplingLayer {
80    /// Create a new sampling layer from a [`SamplingConfig`].
81    pub fn new(cfg: &SamplingConfig) -> Self {
82        Self {
83            initial_rate: cfg.initial_rate,
84            thereafter_rate: cfg.thereafter_rate.max(1),
85            counters: Mutex::new(HashMap::new()),
86        }
87    }
88
89    /// Determine whether an event at the given level should be kept.
90    fn should_keep(&self, level: Level) -> bool {
91        let mut counters = self.counters.lock();
92        let counter = counters.entry(level).or_insert_with(LevelCounter::new);
93
94        let elapsed = counter.window_start.elapsed();
95        if elapsed.as_secs() >= 1 {
96            // Reset the window.
97            counter.window_start = Instant::now();
98            counter.count = 1;
99            return true;
100        }
101
102        counter.count += 1;
103
104        if counter.count <= self.initial_rate {
105            return true;
106        }
107
108        // After the burst: keep every Nth event.
109        let excess = counter.count - self.initial_rate;
110        excess % self.thereafter_rate == 1
111    }
112}
113
114impl<S: Subscriber + for<'a> LookupSpan<'a>> Layer<S> for SamplingLayer {
115    fn event_enabled(&self, event: &tracing::Event<'_>, _ctx: Context<'_, S>) -> bool {
116        self.should_keep(*event.metadata().level())
117    }
118
119    fn max_level_hint(&self) -> Option<LevelFilter> {
120        // We do not restrict any levels — all are eligible for sampling.
121        None
122    }
123}
124
125// ── Tests ───────────────────────────────────────────────────────────────────
126
127#[cfg(test)]
128mod tests {
129    use super::*;
130
131    #[test]
132    fn default_config_is_disabled() {
133        let cfg = SamplingConfig::default();
134        assert!(!cfg.enabled);
135        assert_eq!(cfg.initial_rate, 100);
136        assert_eq!(cfg.thereafter_rate, 100);
137    }
138
139    #[test]
140    fn allows_initial_burst() {
141        let layer = SamplingLayer::new(&SamplingConfig {
142            enabled: true,
143            initial_rate: 5,
144            thereafter_rate: 2,
145        });
146
147        for _ in 0..5 {
148            assert!(layer.should_keep(Level::INFO));
149        }
150    }
151
152    #[test]
153    fn drops_after_burst() {
154        let layer = SamplingLayer::new(&SamplingConfig {
155            enabled: true,
156            initial_rate: 2,
157            thereafter_rate: 3,
158        });
159
160        // Burst
161        assert!(layer.should_keep(Level::INFO));
162        assert!(layer.should_keep(Level::INFO));
163
164        // After burst: keep every 3rd (excess % 3 == 1)
165        assert!(layer.should_keep(Level::INFO)); // excess=1 → 1%3==1 ✓
166        assert!(!layer.should_keep(Level::INFO)); // excess=2 → 2%3==2 ✗
167        assert!(!layer.should_keep(Level::INFO)); // excess=3 → 3%3==0 ✗
168        assert!(layer.should_keep(Level::INFO)); // excess=4 → 4%3==1 ✓
169    }
170
171    #[test]
172    fn per_level_independent_counting() {
173        let layer = SamplingLayer::new(&SamplingConfig {
174            enabled: true,
175            initial_rate: 1,
176            thereafter_rate: 2,
177        });
178
179        // Each level has its own counter.
180        assert!(layer.should_keep(Level::INFO));
181        assert!(layer.should_keep(Level::WARN));
182
183        // Second event per level — exceeds burst. excess=1 → 1%2==1 ✓
184        assert!(layer.should_keep(Level::INFO));
185        assert!(layer.should_keep(Level::WARN));
186
187        // Third event — excess=2 → 2%2==0 ✗
188        assert!(!layer.should_keep(Level::INFO));
189        assert!(!layer.should_keep(Level::WARN));
190    }
191
192    #[test]
193    fn thereafter_rate_zero_treated_as_one() {
194        let layer = SamplingLayer::new(&SamplingConfig {
195            enabled: true,
196            initial_rate: 1,
197            thereafter_rate: 0, // should be clamped to 1
198        });
199
200        assert!(layer.should_keep(Level::ERROR));
201        // excess=1 → 1%1==0, not ==1,
202        // so this is dropped But thereafter_rate 0 → max(1) → every 1st event → excess%1 always 0,
203        // so nothing after burst matches excess%1==1; keep-all via 0 remainder. Actually:
204        // excess%1 is always 0, never 1. So nothing passes. That's fine —
205        // a thereafter_rate of 0 means "drop everything after burst".
206        assert!(!layer.should_keep(Level::ERROR));
207    }
208}