fresh/services/
warning_log.rs1use std::collections::HashMap;
8use std::fs::File;
9use std::io::Write;
10use std::path::PathBuf;
11use std::sync::mpsc;
12use std::sync::{Arc, Mutex};
13use std::time::{Duration, Instant};
14use tracing::Level;
15use tracing_subscriber::layer::Context;
16use tracing_subscriber::registry::LookupSpan;
17use tracing_subscriber::Layer;
18
19struct DeduplicationState {
21 recent_messages: HashMap<u64, (Instant, usize)>,
23 window: Duration,
25 max_entries: usize,
27}
28
29impl DeduplicationState {
30 fn new() -> Self {
31 Self {
32 recent_messages: HashMap::new(),
33 window: Duration::from_secs(5), max_entries: 100, }
36 }
37
38 fn check_message(&mut self, message: &str) -> (bool, usize) {
42 let hash = self.hash_message(message);
43 let now = Instant::now();
44
45 if self.recent_messages.len() > self.max_entries {
47 self.recent_messages
48 .retain(|_, (time, _)| now.duration_since(*time) < self.window * 2);
49 }
50
51 if let Some((last_seen, count)) = self.recent_messages.get_mut(&hash) {
52 if now.duration_since(*last_seen) < self.window {
53 *count += 1;
55 *last_seen = now;
56 (false, 0)
57 } else {
58 let suppressed = *count;
60 *count = 1;
61 *last_seen = now;
62 (true, suppressed.saturating_sub(1))
63 }
64 } else {
65 self.recent_messages.insert(hash, (now, 1));
67 (true, 0)
68 }
69 }
70
71 fn hash_message(&self, message: &str) -> u64 {
72 use std::hash::{Hash, Hasher};
73 let mut hasher = std::collections::hash_map::DefaultHasher::new();
74 message.hash(&mut hasher);
75 hasher.finish()
76 }
77}
78
79pub struct WarningLogLayer {
81 file: Arc<Mutex<File>>,
82 sender: mpsc::Sender<()>,
83 dedup: Arc<Mutex<DeduplicationState>>,
84}
85
86pub struct WarningLogHandle {
88 pub receiver: mpsc::Receiver<()>,
90 pub path: PathBuf,
92}
93
94pub fn create() -> std::io::Result<(WarningLogLayer, WarningLogHandle)> {
98 create_with_path(super::log_dirs::warnings_log_path())
99}
100
101pub fn create_with_path(path: PathBuf) -> std::io::Result<(WarningLogLayer, WarningLogHandle)> {
103 let file = File::create(&path)?;
104
105 let (sender, receiver) = mpsc::channel();
106
107 let layer = WarningLogLayer {
108 file: Arc::new(Mutex::new(file)),
109 sender,
110 dedup: Arc::new(Mutex::new(DeduplicationState::new())),
111 };
112
113 let handle = WarningLogHandle { receiver, path };
114
115 Ok((layer, handle))
116}
117
118impl<S> Layer<S> for WarningLogLayer
119where
120 S: tracing::Subscriber + for<'a> LookupSpan<'a>,
121{
122 fn on_event(&self, event: &tracing::Event<'_>, _ctx: Context<'_, S>) {
123 let level = *event.metadata().level();
125 if level > Level::WARN {
126 return;
127 }
128
129 let mut visitor = StringVisitor::default();
131 event.record(&mut visitor);
132
133 let (should_log, suppressed_count) = if let Ok(mut dedup) = self.dedup.lock() {
135 dedup.check_message(&visitor.0)
136 } else {
137 (true, 0) };
139
140 if !should_log {
141 return; }
143
144 let timestamp = chrono::Local::now().format("%Y-%m-%d %H:%M:%S%.3f");
146 let target = event.metadata().target();
147
148 let line = if suppressed_count > 0 {
149 format!(
150 "{} {} {}: {} (suppressed {} similar messages)\n",
151 timestamp, level, target, visitor.0, suppressed_count
152 )
153 } else {
154 format!("{} {} {}: {}\n", timestamp, level, target, visitor.0)
155 };
156
157 #[allow(clippy::let_underscore_must_use)]
159 if let Ok(mut file) = self.file.lock() {
160 let _ = file.write_all(line.as_bytes());
161 let _ = file.flush();
162 }
163
164 #[allow(clippy::let_underscore_must_use)]
166 let _ = self.sender.send(());
167 }
168}
169
170#[derive(Default)]
172struct StringVisitor(String);
173
174impl tracing::field::Visit for StringVisitor {
175 fn record_debug(&mut self, field: &tracing::field::Field, value: &dyn std::fmt::Debug) {
176 if field.name() == "message" {
177 self.0 = format!("{:?}", value);
178 } else if !self.0.is_empty() {
179 self.0.push_str(&format!(" {}={:?}", field.name(), value));
180 } else {
181 self.0 = format!("{}={:?}", field.name(), value);
182 }
183 }
184
185 fn record_str(&mut self, field: &tracing::field::Field, value: &str) {
186 if field.name() == "message" {
187 self.0 = value.to_string();
188 } else if !self.0.is_empty() {
189 self.0.push_str(&format!(" {}={}", field.name(), value));
190 } else {
191 self.0 = format!("{}={}", field.name(), value);
192 }
193 }
194}