Skip to main content

intercept_bounce/
logger.rs

1// This module defines the Logger thread, which handles logging events
2// and accumulating/reporting statistics based on messages received
3// from the main processing thread.
4
5use 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
22/// Represents a message sent from the main thread to the logger thread.
23pub enum LogMessage {
24    /// Contains detailed information about a single processed event.
25    Event(EventInfo),
26}
27
28/// Detailed information about a single processed event, sent to the logger.
29pub struct EventInfo {
30    /// The raw input event.
31    pub event: input_event,
32    /// Timestamp of the event in microseconds.
33    pub event_us: u64,
34    /// Result of the bounce check (`true` if bounced/dropped).
35    pub is_bounce: bool,
36    /// Time difference (µs) between this event and the previous passed event
37    /// of the same type, *only if* `is_bounce` is true.
38    pub diff_us: Option<u64>,
39    /// Timestamp (µs) of the previous event of the same type that *passed* the filter.
40    /// This is needed by the logger thread to calculate near-miss statistics.
41    pub last_passed_us: Option<u64>,
42}
43
44/// Manages the state and execution loop for the logger thread.
45pub 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    // Optional OTLP Meter for logger-specific metrics
57    otel_meter: Option<Meter>,
58}
59
60impl Logger {
61    /// Creates a new Logger instance.
62    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    /// Manages the logger thread's main loop.
81    ///
82    /// It receives messages from the main thread, processes them (logging and stats),
83    /// and handles periodic stats dumping. It exits when the `logger_running` flag
84    /// is set to false and the channel is empty or disconnected.
85    ///
86    /// Returns the final cumulative statistics upon exit.
87    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); // Used for periodic checks
91
92        // --- OTLP Metrics Setup (in logger thread) ---
93        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            // Check running flag first
101            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            // Check periodic stats dump timer
114            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            // Receive messages with timeout
122            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                    // No message received, loop continues to check flags/timer
130                    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; // Exit loop on disconnect
143                }
144            }
145        } // End loop
146
147        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    /// Processes a single message received from the main thread.
153    /// Updates statistics and performs logging if enabled.
154    #[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                // Log EventInfo fields individually at trace level
159                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                // Record event details in the current span
168                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                // --- Increment Near-Miss Counter ---
182                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; // Skip logging SYN/MSC events even in log-all mode
197                    }
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    /// Dumps the current interval statistics to stderr.
212    #[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, // Runtime is only for cumulative
223                "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    /// Adapts logic from the old BounceFilter::log_event.
241    /// Logs details of a single event (passed or dropped) using tracing.
242    #[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            ("", "") // Not a key event, no key/value names
263        };
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            // Not a bounce or not a key event
273            "".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            // Not a passed key event or no previous passed event
294            "".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        // Use info! macro for event logging
305        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    /// Adapts logic from the old BounceFilter::log_simple_bounce.
326    /// This is used when only `--log-bounces` is enabled. Logs only dropped key events.
327    #[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        // This function is only called if data.is_bounce is true and it's a key event.
330        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", // Should not happen based on call site logic, but handle defensively
339            _ => "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        // Use info! macro for bounce logging
355        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
373/// Helper to format relative timestamps consistently for logging.
374fn 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}") // Keep format! here for padding
383}