use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::{Mutex, OnceLock};
use std::time::{Duration, Instant};
fn base_instant() -> Instant {
static BASE: OnceLock<Instant> = OnceLock::new();
*BASE.get_or_init(Instant::now)
}
fn nanos_since_base(now: Instant) -> u64 {
now.saturating_duration_since(base_instant()).as_nanos() as u64
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
#[non_exhaustive]
pub enum LogFormat {
#[default]
Json,
Pretty,
}
pub fn init(format: LogFormat, default_filter: &str) -> bool {
let filter = tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new(default_filter));
match format {
LogFormat::Json => tracing_subscriber::fmt()
.json()
.flatten_event(true)
.with_target(true)
.with_env_filter(filter)
.try_init()
.is_ok(),
LogFormat::Pretty => tracing_subscriber::fmt()
.with_target(true)
.with_env_filter(filter)
.try_init()
.is_ok(),
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Decision {
Allow {
suppressed_before: u64,
},
Suppress,
}
#[derive(Debug)]
struct RateLimitState {
window_start: Option<Instant>,
allowed_in_window: u32,
suppressed: u64,
}
#[derive(Debug)]
pub struct RateLimit {
capacity: u32,
window: Duration,
state: Mutex<RateLimitState>,
saturated: AtomicBool,
window_end_nanos: AtomicU64,
fast_suppressed: AtomicU64,
}
impl RateLimit {
#[must_use]
pub const fn new(capacity: u32, window: Duration) -> Self {
RateLimit {
capacity,
window,
state: Mutex::new(RateLimitState {
window_start: None,
allowed_in_window: 0,
suppressed: 0,
}),
saturated: AtomicBool::new(false),
window_end_nanos: AtomicU64::new(0),
fast_suppressed: AtomicU64::new(0),
}
}
pub fn check(&self) -> Decision {
self.check_at(Instant::now())
}
pub fn check_at(&self, now: Instant) -> Decision {
if self.saturated.load(Ordering::Relaxed)
&& nanos_since_base(now) < self.window_end_nanos.load(Ordering::Relaxed)
{
self.fast_suppressed.fetch_add(1, Ordering::Relaxed);
return Decision::Suppress;
}
let mut s = self.state.lock().expect("rate limit lock");
let window_expired = match s.window_start {
None => true,
Some(start) => now.saturating_duration_since(start) >= self.window,
};
if window_expired {
let suppressed_before = s.suppressed + self.fast_suppressed.swap(0, Ordering::Relaxed);
s.window_start = Some(now);
s.suppressed = 0;
if self.capacity == 0 {
s.allowed_in_window = 0;
s.suppressed = 1;
self.arm(now);
return Decision::Suppress;
}
s.allowed_in_window = 1;
self.saturated.store(false, Ordering::Relaxed);
return Decision::Allow { suppressed_before };
}
if s.allowed_in_window < self.capacity {
s.allowed_in_window += 1;
Decision::Allow {
suppressed_before: 0,
}
} else {
s.suppressed += 1;
if let Some(start) = s.window_start {
self.arm(start);
}
Decision::Suppress
}
}
fn arm(&self, window_start: Instant) {
self.window_end_nanos.store(
nanos_since_base(window_start + self.window),
Ordering::Relaxed,
);
self.saturated.store(true, Ordering::Relaxed);
}
}
#[macro_export]
macro_rules! rate_limited_warn {
($limiter:expr, $($arg:tt)+) => {
match $limiter.check() {
$crate::telemetry::Decision::Allow { suppressed_before } => {
if suppressed_before > 0 {
::tracing::warn!(suppressed = suppressed_before, $($arg)+);
} else {
::tracing::warn!($($arg)+);
}
}
$crate::telemetry::Decision::Suppress => {}
}
};
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn allows_up_to_capacity_then_suppresses() {
let limit = RateLimit::new(2, Duration::from_secs(10));
let t0 = Instant::now();
assert_eq!(
limit.check_at(t0),
Decision::Allow {
suppressed_before: 0
}
);
assert_eq!(
limit.check_at(t0 + Duration::from_secs(1)),
Decision::Allow {
suppressed_before: 0
}
);
for i in 2..5 {
assert_eq!(
limit.check_at(t0 + Duration::from_secs(i)),
Decision::Suppress
);
}
assert_eq!(
limit.check_at(t0 + Duration::from_secs(10)),
Decision::Allow {
suppressed_before: 3
}
);
assert_eq!(
limit.check_at(t0 + Duration::from_secs(11)),
Decision::Allow {
suppressed_before: 0
}
);
assert_eq!(
limit.check_at(t0 + Duration::from_secs(12)),
Decision::Suppress
);
}
#[test]
fn fast_path_suppression_preserves_the_carried_count() {
let limit = RateLimit::new(1, Duration::from_secs(10));
let t0 = Instant::now();
assert_eq!(
limit.check_at(t0),
Decision::Allow {
suppressed_before: 0
}
);
for i in 1..=100 {
assert_eq!(
limit.check_at(t0 + Duration::from_millis(i)),
Decision::Suppress
);
}
assert_eq!(
limit.check_at(t0 + Duration::from_secs(10)),
Decision::Allow {
suppressed_before: 100
}
);
}
#[test]
fn zero_capacity_suppresses_everything() {
let limit = RateLimit::new(0, Duration::from_secs(1));
let t0 = Instant::now();
assert_eq!(limit.check_at(t0), Decision::Suppress);
assert_eq!(
limit.check_at(t0 + Duration::from_secs(2)),
Decision::Suppress
);
}
#[test]
fn usable_from_a_static_via_the_macro() {
static LIMIT: RateLimit = RateLimit::new(1, Duration::from_secs(60));
rate_limited_warn!(LIMIT, code = 7, "first is allowed");
rate_limited_warn!(LIMIT, code = 8, "second is suppressed");
}
#[test]
fn init_is_idempotent() {
let _ = init(LogFormat::Pretty, "warn");
let second = init(LogFormat::Json, "warn");
assert!(!second, "second init reports already-installed");
}
}