1use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
22use std::sync::{Mutex, OnceLock};
23use std::time::{Duration, Instant};
24
25fn base_instant() -> Instant {
29 static BASE: OnceLock<Instant> = OnceLock::new();
30 *BASE.get_or_init(Instant::now)
31}
32
33fn nanos_since_base(now: Instant) -> u64 {
34 now.saturating_duration_since(base_instant()).as_nanos() as u64
35}
36
37#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
39#[non_exhaustive]
40pub enum LogFormat {
41 #[default]
44 Json,
45 Pretty,
47}
48
49pub fn init(format: LogFormat, default_filter: &str) -> bool {
56 let filter = tracing_subscriber::EnvFilter::try_from_default_env()
57 .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new(default_filter));
58 match format {
59 LogFormat::Json => tracing_subscriber::fmt()
60 .json()
61 .flatten_event(true)
62 .with_target(true)
63 .with_env_filter(filter)
64 .try_init()
65 .is_ok(),
66 LogFormat::Pretty => tracing_subscriber::fmt()
67 .with_target(true)
68 .with_env_filter(filter)
69 .try_init()
70 .is_ok(),
71 }
72}
73
74#[derive(Clone, Copy, Debug, PartialEq, Eq)]
76pub enum Decision {
77 Allow {
80 suppressed_before: u64,
82 },
83 Suppress,
85}
86
87#[derive(Debug)]
88struct RateLimitState {
89 window_start: Option<Instant>,
90 allowed_in_window: u32,
91 suppressed: u64,
92}
93
94#[derive(Debug)]
109pub struct RateLimit {
110 capacity: u32,
111 window: Duration,
112 state: Mutex<RateLimitState>,
113 saturated: AtomicBool,
117 window_end_nanos: AtomicU64,
119 fast_suppressed: AtomicU64,
122}
123
124impl RateLimit {
125 #[must_use]
127 pub const fn new(capacity: u32, window: Duration) -> Self {
128 RateLimit {
129 capacity,
130 window,
131 state: Mutex::new(RateLimitState {
132 window_start: None,
133 allowed_in_window: 0,
134 suppressed: 0,
135 }),
136 saturated: AtomicBool::new(false),
137 window_end_nanos: AtomicU64::new(0),
138 fast_suppressed: AtomicU64::new(0),
139 }
140 }
141
142 pub fn check(&self) -> Decision {
144 self.check_at(Instant::now())
145 }
146
147 pub fn check_at(&self, now: Instant) -> Decision {
149 if self.saturated.load(Ordering::Relaxed)
153 && nanos_since_base(now) < self.window_end_nanos.load(Ordering::Relaxed)
154 {
155 self.fast_suppressed.fetch_add(1, Ordering::Relaxed);
156 return Decision::Suppress;
157 }
158
159 let mut s = self.state.lock().expect("rate limit lock");
160 let window_expired = match s.window_start {
161 None => true,
162 Some(start) => now.saturating_duration_since(start) >= self.window,
163 };
164 if window_expired {
165 let suppressed_before = s.suppressed + self.fast_suppressed.swap(0, Ordering::Relaxed);
166 s.window_start = Some(now);
167 s.suppressed = 0;
168 if self.capacity == 0 {
169 s.allowed_in_window = 0;
170 s.suppressed = 1;
171 self.arm(now);
172 return Decision::Suppress;
173 }
174 s.allowed_in_window = 1;
175 self.saturated.store(false, Ordering::Relaxed);
176 return Decision::Allow { suppressed_before };
177 }
178 if s.allowed_in_window < self.capacity {
179 s.allowed_in_window += 1;
180 Decision::Allow {
181 suppressed_before: 0,
182 }
183 } else {
184 s.suppressed += 1;
185 if let Some(start) = s.window_start {
186 self.arm(start);
187 }
188 Decision::Suppress
189 }
190 }
191
192 fn arm(&self, window_start: Instant) {
194 self.window_end_nanos.store(
195 nanos_since_base(window_start + self.window),
196 Ordering::Relaxed,
197 );
198 self.saturated.store(true, Ordering::Relaxed);
199 }
200}
201
202#[macro_export]
206macro_rules! rate_limited_warn {
207 ($limiter:expr, $($arg:tt)+) => {
208 match $limiter.check() {
209 $crate::telemetry::Decision::Allow { suppressed_before } => {
210 if suppressed_before > 0 {
211 ::tracing::warn!(suppressed = suppressed_before, $($arg)+);
212 } else {
213 ::tracing::warn!($($arg)+);
214 }
215 }
216 $crate::telemetry::Decision::Suppress => {}
217 }
218 };
219}
220
221#[cfg(test)]
222mod tests {
223 use super::*;
224
225 #[test]
226 fn allows_up_to_capacity_then_suppresses() {
227 let limit = RateLimit::new(2, Duration::from_secs(10));
228 let t0 = Instant::now();
229 assert_eq!(
230 limit.check_at(t0),
231 Decision::Allow {
232 suppressed_before: 0
233 }
234 );
235 assert_eq!(
236 limit.check_at(t0 + Duration::from_secs(1)),
237 Decision::Allow {
238 suppressed_before: 0
239 }
240 );
241 for i in 2..5 {
242 assert_eq!(
243 limit.check_at(t0 + Duration::from_secs(i)),
244 Decision::Suppress
245 );
246 }
247 assert_eq!(
249 limit.check_at(t0 + Duration::from_secs(10)),
250 Decision::Allow {
251 suppressed_before: 3
252 }
253 );
254 assert_eq!(
256 limit.check_at(t0 + Duration::from_secs(11)),
257 Decision::Allow {
258 suppressed_before: 0
259 }
260 );
261 assert_eq!(
262 limit.check_at(t0 + Duration::from_secs(12)),
263 Decision::Suppress
264 );
265 }
266
267 #[test]
268 fn fast_path_suppression_preserves_the_carried_count() {
269 let limit = RateLimit::new(1, Duration::from_secs(10));
272 let t0 = Instant::now();
273 assert_eq!(
274 limit.check_at(t0),
275 Decision::Allow {
276 suppressed_before: 0
277 }
278 );
279 for i in 1..=100 {
280 assert_eq!(
281 limit.check_at(t0 + Duration::from_millis(i)),
282 Decision::Suppress
283 );
284 }
285 assert_eq!(
286 limit.check_at(t0 + Duration::from_secs(10)),
287 Decision::Allow {
288 suppressed_before: 100
289 }
290 );
291 }
292
293 #[test]
294 fn zero_capacity_suppresses_everything() {
295 let limit = RateLimit::new(0, Duration::from_secs(1));
296 let t0 = Instant::now();
297 assert_eq!(limit.check_at(t0), Decision::Suppress);
298 assert_eq!(
299 limit.check_at(t0 + Duration::from_secs(2)),
300 Decision::Suppress
301 );
302 }
303
304 #[test]
305 fn usable_from_a_static_via_the_macro() {
306 static LIMIT: RateLimit = RateLimit::new(1, Duration::from_secs(60));
307 rate_limited_warn!(LIMIT, code = 7, "first is allowed");
309 rate_limited_warn!(LIMIT, code = 8, "second is suppressed");
310 }
311
312 #[test]
313 fn init_is_idempotent() {
314 let _ = init(LogFormat::Pretty, "warn");
316 let second = init(LogFormat::Json, "warn");
317 assert!(!second, "second init reports already-installed");
318 }
319}