use std::collections::HashMap;
use std::sync::LazyLock;
use std::sync::Mutex;
use tracing::callsite::Identifier;
use tracing::{Level, Metadata, Subscriber};
use tracing_subscriber::layer::{Context, Filter};
const WARN_REPEATS_SHOWN: u64 = 3;
#[derive(Default)]
struct WarnRepeatState {
counts: HashMap<Identifier, CallsiteCount>,
}
struct CallsiteCount {
seen: u64,
target: String,
location: String,
}
static WARN_REPEATS: LazyLock<Mutex<WarnRepeatState>> =
LazyLock::new(|| Mutex::new(WarnRepeatState::default()));
pub(crate) struct WarnRepeatLimit;
impl<S: Subscriber> Filter<S> for WarnRepeatLimit {
fn enabled(&self, meta: &Metadata<'_>, _cx: &Context<'_, S>) -> bool {
if !meta.is_event() || *meta.level() != Level::WARN {
return true;
}
if !meta.target().starts_with("keyhog") {
return true;
}
let mut state = match WARN_REPEATS.lock() {
Ok(state) => state,
Err(poisoned) => {
eprintln!(
"keyhog: warning-dedup state was poisoned by a prior panic; recovering its counted warnings"
);
poisoned.into_inner()
}
};
let entry = state
.counts
.entry(meta.callsite())
.or_insert_with(|| CallsiteCount {
seen: 0,
target: meta.target().to_string(),
location: match (meta.file(), meta.line()) {
(Some(file), Some(line)) => format!("{file}:{line}"),
_ => meta.target().to_string(),
},
});
entry.seen += 1;
entry.seen <= WARN_REPEATS_SHOWN
}
}
pub(crate) struct WarnDedupSummaryGuard;
pub(crate) fn dump_warn_dedup_summary() {
let state = match WARN_REPEATS.lock() {
Ok(state) => state,
Err(poisoned) => {
eprintln!(
"keyhog: warning-dedup state was poisoned by a prior panic; reporting its recovered summary"
);
poisoned.into_inner()
}
};
for count in state.counts.values() {
if count.seen > WARN_REPEATS_SHOWN {
eprintln!(
"keyhog: warning at {} ({}) repeated {} more times (first {} shown)",
count.location,
count.target,
count.seen - WARN_REPEATS_SHOWN,
WARN_REPEATS_SHOWN,
);
}
}
}
impl Drop for WarnDedupSummaryGuard {
fn drop(&mut self) {
dump_warn_dedup_summary();
}
}
#[cfg(test)]
mod tests;