use std::collections::HashMap;
use std::fs::File;
use std::io::Write;
use std::path::PathBuf;
use std::sync::mpsc;
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
use tracing::Level;
use tracing_subscriber::layer::Context;
use tracing_subscriber::registry::LookupSpan;
use tracing_subscriber::Layer;
struct DeduplicationState {
recent_messages: HashMap<u64, (Instant, usize)>,
window: Duration,
max_entries: usize,
}
impl DeduplicationState {
fn new() -> Self {
Self {
recent_messages: HashMap::new(),
window: Duration::from_secs(5), max_entries: 100, }
}
fn check_message(&mut self, message: &str) -> (bool, usize) {
let hash = self.hash_message(message);
let now = Instant::now();
if self.recent_messages.len() > self.max_entries {
self.recent_messages
.retain(|_, (time, _)| now.duration_since(*time) < self.window * 2);
}
if let Some((last_seen, count)) = self.recent_messages.get_mut(&hash) {
if now.duration_since(*last_seen) < self.window {
*count += 1;
*last_seen = now;
(false, 0)
} else {
let suppressed = *count;
*count = 1;
*last_seen = now;
(true, suppressed.saturating_sub(1))
}
} else {
self.recent_messages.insert(hash, (now, 1));
(true, 0)
}
}
fn hash_message(&self, message: &str) -> u64 {
use std::hash::{Hash, Hasher};
let mut hasher = std::collections::hash_map::DefaultHasher::new();
message.hash(&mut hasher);
hasher.finish()
}
}
pub struct WarningLogLayer {
file: Arc<Mutex<File>>,
sender: mpsc::Sender<()>,
dedup: Arc<Mutex<DeduplicationState>>,
}
pub struct WarningLogHandle {
pub receiver: mpsc::Receiver<()>,
pub path: PathBuf,
}
pub fn create() -> std::io::Result<(WarningLogLayer, WarningLogHandle)> {
create_with_path(super::log_dirs::warnings_log_path())
}
pub fn create_with_path(path: PathBuf) -> std::io::Result<(WarningLogLayer, WarningLogHandle)> {
let file = File::create(&path)?;
let (sender, receiver) = mpsc::channel();
let layer = WarningLogLayer {
file: Arc::new(Mutex::new(file)),
sender,
dedup: Arc::new(Mutex::new(DeduplicationState::new())),
};
let handle = WarningLogHandle { receiver, path };
Ok((layer, handle))
}
impl<S> Layer<S> for WarningLogLayer
where
S: tracing::Subscriber + for<'a> LookupSpan<'a>,
{
fn on_event(&self, event: &tracing::Event<'_>, _ctx: Context<'_, S>) {
let level = *event.metadata().level();
if level > Level::WARN {
return;
}
let mut visitor = StringVisitor::default();
event.record(&mut visitor);
let (should_log, suppressed_count) = if let Ok(mut dedup) = self.dedup.lock() {
dedup.check_message(&visitor.0)
} else {
(true, 0) };
if !should_log {
return; }
let timestamp = chrono::Local::now().format("%Y-%m-%d %H:%M:%S%.3f");
let target = event.metadata().target();
let line = if suppressed_count > 0 {
format!(
"{} {} {}: {} (suppressed {} similar messages)\n",
timestamp, level, target, visitor.0, suppressed_count
)
} else {
format!("{} {} {}: {}\n", timestamp, level, target, visitor.0)
};
if let Ok(mut file) = self.file.lock() {
let _ = file.write_all(line.as_bytes());
let _ = file.flush();
}
let _ = self.sender.send(());
}
}
#[derive(Default)]
struct StringVisitor(String);
impl tracing::field::Visit for StringVisitor {
fn record_debug(&mut self, field: &tracing::field::Field, value: &dyn std::fmt::Debug) {
if field.name() == "message" {
self.0 = format!("{:?}", value);
} else if !self.0.is_empty() {
self.0.push_str(&format!(" {}={:?}", field.name(), value));
} else {
self.0 = format!("{}={:?}", field.name(), value);
}
}
fn record_str(&mut self, field: &tracing::field::Field, value: &str) {
if field.name() == "message" {
self.0 = value.to_string();
} else if !self.0.is_empty() {
self.0.push_str(&format!(" {}={}", field.name(), value));
} else {
self.0 = format!("{}={}", field.name(), value);
}
}
}