1use crate::config::Config;
6use crate::event;
7use crate::filter::keynames::{get_event_type_name, get_key_name};
8use crate::filter::stats::StatsCollector;
9use crate::util;
10use crossbeam_channel::{Receiver, RecvTimeoutError};
11
12use chrono::Local;
13use input_linux_sys::{input_event, EV_MSC, EV_SYN};
14use opentelemetry::metrics::{Counter, Meter};
15use std::io;
16use std::sync::atomic::{AtomicBool, Ordering};
17use std::sync::Arc;
18use std::time::{Duration, Instant};
19use tracing::info;
20use tracing::{instrument, Span};
21
22pub enum LogMessage {
24 Event(EventInfo),
26}
27
28pub struct EventInfo {
30 pub event: input_event,
32 pub event_us: u64,
34 pub is_bounce: bool,
36 pub diff_us: Option<u64>,
39 pub last_passed_us: Option<u64>,
42}
43
44pub struct Logger {
46 receiver: Receiver<LogMessage>,
47 logger_running: Arc<AtomicBool>,
48 config: Arc<Config>,
49
50 cumulative_stats: StatsCollector,
51 interval_stats: StatsCollector,
52
53 last_dump_time: Instant,
54 first_event_us: Option<u64>,
55
56 otel_meter: Option<Meter>,
58}
59
60impl Logger {
61 pub fn new(
63 receiver: Receiver<LogMessage>,
64 logger_running: Arc<AtomicBool>,
65 config: Arc<Config>,
66 otel_meter: Option<Meter>,
67 ) -> Self {
68 Logger {
69 receiver,
70 logger_running,
71 config,
72 cumulative_stats: StatsCollector::with_capacity(),
73 interval_stats: StatsCollector::with_capacity(),
74 last_dump_time: Instant::now(),
75 first_event_us: None,
76 otel_meter,
77 }
78 }
79
80 pub fn run(&mut self) -> StatsCollector {
88 tracing::debug!("Logger thread started");
89 let log_interval = self.config.log_interval();
90 let check_interval = Duration::from_millis(100); let near_miss_counter: Option<Counter<u64>> = self.otel_meter.as_ref().map(|m| {
94 m.u64_counter("events.near_miss")
95 .with_description("Passed events that were near misses")
96 .init()
97 });
98
99 loop {
100 if !self.logger_running.load(Ordering::SeqCst) {
102 tracing::debug!(
103 "Received shutdown signal via AtomicBool, attempting to drain channel"
104 );
105 while let Ok(msg) = self.receiver.try_recv() {
106 tracing::trace!("Draining channel: Processing message after shutdown signal");
107 self.process_message(msg, &near_miss_counter);
108 }
109 tracing::debug!("Finished draining channel. Exiting run loop");
110 break;
111 }
112
113 if log_interval > Duration::ZERO && self.last_dump_time.elapsed() >= log_interval {
115 tracing::debug!("Triggering periodic stats dump");
116 self.dump_periodic_stats();
117 self.last_dump_time = Instant::now();
118 tracing::debug!("Periodic stats dump complete. Timer reset");
119 }
120
121 match self.receiver.recv_timeout(check_interval) {
123 Ok(msg) => {
124 tracing::trace!("Logger thread received message from channel");
125 self.process_message(msg, &near_miss_counter);
126 tracing::trace!("Logger thread finished processing message");
127 }
128 Err(RecvTimeoutError::Timeout) => {
129 tracing::trace!("Logger thread receive timed out. Re-checking flags");
131 continue;
132 }
133 Err(RecvTimeoutError::Disconnected) => {
134 tracing::warn!("Detected channel disconnected. Attempting to drain channel");
135 while let Ok(msg) = self.receiver.try_recv() {
136 tracing::trace!(
137 "Logger thread draining channel: Processing message after disconnect"
138 );
139 self.process_message(msg, &near_miss_counter);
140 }
141 tracing::warn!("Finished draining channel. Exiting run loop");
142 break; }
144 }
145 } tracing::debug!("Run loop exited. Preparing final stats");
148 tracing::debug!("Taking cumulative_stats for return");
149 std::mem::take(&mut self.cumulative_stats)
150 }
151
152 #[instrument(name = "logger_process_message", skip(self, msg, near_miss_counter), fields(event_type=tracing::field::Empty, is_bounce=tracing::field::Empty))]
155 pub fn process_message(&mut self, msg: LogMessage, near_miss_counter: &Option<Counter<u64>>) {
156 match msg {
157 LogMessage::Event(data) => {
158 tracing::trace!(event_type = data.event.type_,
160 event_code = data.event.code,
161 event_value = data.event.value,
162 event_us = data.event_us,
163 is_bounce = data.is_bounce,
164 diff_us = ?data.diff_us,
165 last_passed_us = ?data.last_passed_us,
166 "Logger processing EventInfo");
167 Span::current().record("event_type", data.event.type_);
169 Span::current().record("is_bounce", data.is_bounce);
170
171 self.cumulative_stats
172 .record_event_info_with_config(&data, &self.config);
173 self.interval_stats
174 .record_event_info_with_config(&data, &self.config);
175
176 if self.first_event_us.is_none() {
177 self.first_event_us = Some(data.event_us);
178 tracing::trace!(ts = data.event_us, "Logger recorded first event timestamp");
179 }
180
181 if !data.is_bounce && event::is_key_event(&data.event) {
183 if let Some(last_us) = data.last_passed_us {
184 if let Some(diff) = data.event_us.checked_sub(last_us) {
185 if diff <= self.config.near_miss_threshold_us() {
186 if let Some(counter) = near_miss_counter {
187 counter.add(1, &[]);
188 }
189 }
190 }
191 }
192 }
193
194 if self.config.log_all_events {
195 if data.event.type_ == EV_SYN as u16 || data.event.type_ == EV_MSC as u16 {
196 return; }
198 tracing::trace!("Logger logging all events");
199 self.log_event_detailed(&data);
200 } else if self.config.log_bounces
201 && data.is_bounce
202 && event::is_key_event(&data.event)
203 {
204 tracing::trace!("Logger logging bounce event");
205 self.log_simple_bounce_detailed(&data);
206 }
207 }
208 }
209 }
210
211 #[instrument(name = "dump_periodic_stats", skip(self))]
213 fn dump_periodic_stats(&mut self) {
214 let wallclock = Local::now().format("%Y-%m-%d %H:%M:%S%.3f").to_string();
215 tracing::info!(target: "stats", kind = "periodic", wallclock = %wallclock, "Periodic stats dump");
216
217 let mut interval_stats_clone = self.interval_stats.clone();
218 if self.config.stats_json {
219 tracing::debug!("Logger thread printing periodic stats in JSON format");
220 interval_stats_clone.print_stats_json(
221 &self.config,
222 None, "Periodic",
224 &mut io::stderr().lock(),
225 );
226 tracing::debug!("Logger thread finished printing periodic stats in JSON format");
227 } else {
228 tracing::debug!("Logger thread printing periodic stats in human-readable format");
229 interval_stats_clone.print_stats_to_stderr(&self.config, "Periodic");
230 tracing::debug!(
231 "Logger thread finished printing periodic stats in human-readable format"
232 );
233 }
234
235 tracing::debug!("Logger thread resetting interval stats");
236 self.interval_stats = StatsCollector::with_capacity();
237 tracing::debug!("Logger thread interval stats reset");
238 }
239
240 #[instrument(name = "log_event_detailed", skip(self, data), fields(status=tracing::field::Empty, key_code=data.event.code))]
243 fn log_event_detailed(&self, data: &EventInfo) {
244 let status = if data.is_bounce { "DROP" } else { "PASS" };
245
246 let relative_us = data
247 .event_us
248 .saturating_sub(self.first_event_us.unwrap_or(data.event_us));
249
250 let type_name = get_event_type_name(data.event.type_);
251
252 let (key_name_str, value_name_str) = if event::is_key_event(&data.event) {
253 let key_name = get_key_name(data.event.code);
254 let value_name = match data.event.value {
255 0 => "Release",
256 1 => "Press",
257 2 => "Repeat",
258 _ => "Unknown",
259 };
260 (key_name, value_name)
261 } else {
262 ("", "") };
264
265 let bounce_info_str = if data.is_bounce && event::is_key_event(&data.event) {
266 if let Some(diff) = data.diff_us {
267 format!(" (Bounce Time: {})", util::format_us(diff))
268 } else {
269 " (Bounce Time: N/A)".to_string()
270 }
271 } else {
272 "".to_string()
274 };
275
276 let near_miss_info_str = if !data.is_bounce && event::is_key_event(&data.event) {
277 if let Some(last_us) = data.last_passed_us {
278 if let Some(diff) = data.event_us.checked_sub(last_us) {
279 if Duration::from_micros(diff) >= self.config.debounce_time()
280 && Duration::from_micros(diff) <= self.config.near_miss_threshold()
281 {
282 format!(" (Diff since last passed: {})", util::format_us(diff))
283 } else {
284 "".to_string()
285 }
286 } else {
287 "".to_string()
288 }
289 } else {
290 "".to_string()
291 }
292 } else {
293 "".to_string()
295 };
296
297 let relative_human = format_relative_us(relative_us);
298 let key_info_str = if event::is_key_event(&data.event) {
299 format!(" Key [{key_name_str}] ({})", data.event.code)
300 } else {
301 "".to_string()
302 };
303
304 info!(
306 status,
307 relative_us = relative_us,
308 relative_human = %format_relative_us(relative_us),
309 event_type = data.event.type_,
310 event_type_name = type_name,
311 event_code = data.event.code,
312 event_value = data.event.value,
313 key_name = key_name_str,
314 value_name = value_name_str,
315 is_bounce = data.is_bounce,
316 bounce_time_us = data.diff_us,
317 bounce_info = %bounce_info_str,
318 near_miss_diff_us = if !data.is_bounce && event::is_key_event(&data.event) { data.event_us.checked_sub(data.last_passed_us.unwrap_or(0)) } else { None },
319 near_miss_info = %near_miss_info_str,
320 "[{status}] {relative_human} {type_name} ({}, {value_name_str} {}){key_info_str}{bounce_info_str}{near_miss_info_str}",
321 data.event.code, data.event.value
322 );
323 }
324
325 #[instrument(name = "log_simple_bounce_detailed", skip(self, data), fields(key_code=data.event.code))]
328 fn log_simple_bounce_detailed(&self, data: &EventInfo) {
329 let code = data.event.code;
331 let value = data.event.value;
332 let type_name = get_event_type_name(data.event.type_);
333 let key_name = get_key_name(code);
334
335 let value_name = match value {
336 0 => "Release",
337 1 => "Press",
338 2 => "Repeat", _ => "Unknown",
340 };
341
342 let relative_us = data
343 .event_us
344 .saturating_sub(self.first_event_us.unwrap_or(data.event_us));
345
346 let bounce_info_str = if let Some(diff) = data.diff_us {
347 format!(" (Bounce Time: {})", util::format_us(diff))
348 } else {
349 " (Bounce Time: N/A)".to_string()
350 };
351
352 let relative_human = format_relative_us(relative_us);
353
354 info!(
356 status = "DROP",
357 relative_us = relative_us,
358 relative_human = %format_relative_us(relative_us),
359 event_type = data.event.type_,
360 event_type_name = type_name,
361 event_code = code,
362 event_value = value,
363 key_name = key_name,
364 value_name = value_name,
365 is_bounce = true,
366 bounce_time_us = data.diff_us,
367 bounce_info = %bounce_info_str,
368 "[DROP] {relative_human} {type_name} ({code}, {value_name} {value}) Key [{key_name}] ({code}){bounce_info_str}",
369 );
370 }
371}
372
373fn format_relative_us(relative_us: u64) -> String {
375 let s = if relative_us < 1_000 {
376 format!("+{relative_us} µs")
377 } else if relative_us < 1_000_000 {
378 format!("+{:.1} ms", relative_us as f64 / 1000.0)
379 } else {
380 format!("+{:.3} s", relative_us as f64 / 1_000_000.0)
381 };
382 format!("{s:<10}") }