use std::collections::HashMap;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use tokio::sync::watch;
pub type LogId = crate::ids::LogId;
#[derive(Clone, Debug)]
pub struct LimitedLogControl {
interval_tx: watch::Sender<u64>,
}
impl LimitedLogControl {
pub fn set_interval(&self, interval: Duration) {
let _ = self.interval_tx.send_replace(interval.as_secs());
}
}
#[derive(Debug)]
pub struct LimitedLogRegistry {
last_log: HashMap<LogId, AtomicU64>,
interval_rx: watch::Receiver<u64>,
}
impl LimitedLogRegistry {
fn new(interval_rx: watch::Receiver<u64>) -> Self {
let mut last_log = HashMap::with_capacity(LogId::ALL.len());
for id in LogId::ALL {
last_log.insert(*id, AtomicU64::new(0));
}
Self {
last_log,
interval_rx,
}
}
pub fn should_log(&self, id: LogId) -> bool {
let Some(last) = self.last_log.get(&id) else {
return true;
};
let interval = *self.interval_rx.borrow();
if interval == 0 {
return true;
}
let now = now_secs();
let mut current = last.load(Ordering::SeqCst);
loop {
if now.saturating_sub(current) < interval {
return false;
}
match last.compare_exchange(current, now, Ordering::SeqCst, Ordering::SeqCst) {
Ok(_) => return true,
Err(updated) => current = updated,
}
}
}
}
#[derive(Clone, Debug)]
pub struct LimitedLogContext {
registry: Arc<LimitedLogRegistry>,
control: LimitedLogControl,
}
impl LimitedLogContext {
pub fn new(interval: Duration) -> Self {
let (tx, rx) = watch::channel(interval.as_secs());
let registry = Arc::new(LimitedLogRegistry::new(rx));
let control = LimitedLogControl { interval_tx: tx };
Self { registry, control }
}
pub fn registry(&self) -> Arc<LimitedLogRegistry> {
Arc::clone(&self.registry)
}
pub fn control(&self) -> LimitedLogControl {
self.control.clone()
}
}
pub fn init_limited_logging(interval: Duration) -> LimitedLogContext {
LimitedLogContext::new(interval)
}
fn now_secs() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs()
}
#[doc(hidden)]
#[macro_export]
macro_rules! __limited_event {
($level:expr, $registry:expr, $id:expr, $($arg:tt)+) => {{
if $registry.should_log($id) {
tracing::event!($level, log_id = $id.as_u32(), $($arg)+);
}
}};
}
#[doc(hidden)]
#[macro_export]
macro_rules! info_limited {
($registry:expr, $id:expr, $($arg:tt)+) => {
$crate::__limited_event!(tracing::Level::INFO, $registry, $id, $($arg)+)
};
}
#[doc(hidden)]
#[macro_export]
macro_rules! warn_limited {
($registry:expr, $id:expr, $($arg:tt)+) => {
$crate::__limited_event!(tracing::Level::WARN, $registry, $id, $($arg)+)
};
}
#[doc(hidden)]
#[macro_export]
macro_rules! debug_limited {
($registry:expr, $id:expr, $($arg:tt)+) => {
$crate::__limited_event!(tracing::Level::DEBUG, $registry, $id, $($arg)+)
};
}
#[doc(hidden)]
#[macro_export]
macro_rules! error_limited {
($registry:expr, $id:expr, $($arg:tt)+) => {
$crate::__limited_event!(tracing::Level::ERROR, $registry, $id, $($arg)+)
};
}
#[cfg(test)]
#[path = "tests/limited.rs"]
mod tests;