Skip to main content

kernelvex/util/
logger.rs

1//! Simple asynchronous logger with level filtering and multiple outputs.
2//!
3//! This module provides a lightweight logging system designed for VEX robotics
4//! applications. It runs a background thread to handle log output asynchronously,
5//! preventing logging from blocking time-critical robot code.
6//!
7//! # Features
8//!
9//! - **Level filtering**: Filter messages by severity (Trace, Debug, Info, Warn, Error)
10//! - **Multiple outputs**: Log to stdout, stderr, or a file
11//! - **Async design**: Background thread handles I/O to avoid blocking
12//! - **Colored output**: Terminal output includes ANSI colors for readability
13//! - **Thread-safe**: Logger can be cloned and used from multiple threads
14//!
15//! # Usage
16//!
17//! ```ignore
18//! use kernelvex::util::logger::{init, Level};
19//!
20//! // Initialize the logger
21//! let (logger, handle) = init();
22//!
23//! // Configure logging level and output
24//! let logger = logger.level(Level::Debug).stdout();
25//!
26//! // Log messages
27//! logger.info("Robot initialized");
28//! logger.debug("Motor voltages: left=5.0, right=5.0");
29//! logger.warn("Battery low");
30//!
31//! // Logger thread stops when handle is dropped
32//! drop(handle);
33//! ```
34//!
35//! # Output Format
36//!
37//! Log messages are formatted as:
38//! ```text
39//! [timestamp] [LEVEL] [tid:thread_id] message
40//! ```
41
42use std::fmt;
43use std::fs::{File, OpenOptions};
44use std::io::{self, BufWriter, Write};
45use std::sync::mpsc::{self, Sender};
46use std::sync::{Arc, Mutex};
47use std::thread::{self, JoinHandle};
48use std::time::SystemTime;
49
50/// Log level severity.
51///
52/// Levels are ordered from least to most severe. When a minimum level is set,
53/// only messages at that level or higher will be logged.
54///
55/// # Ordering
56///
57/// `Trace < Debug < Info < Warn < Error`
58#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
59pub enum Level {
60    /// Finest-grained information, typically for debugging specific issues
61    Trace,
62    /// Detailed information useful during development
63    Debug,
64    /// General operational information
65    Info,
66    /// Potentially problematic situations
67    Warn,
68    /// Error conditions that may allow continued operation
69    Error,
70}
71
72impl fmt::Display for Level {
73    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
74        match self {
75            Level::Trace => write!(f, "TRACE"),
76            Level::Debug => write!(f, "DEBUG"),
77            Level::Info => write!(f, "INFO"),
78            Level::Warn => write!(f, "WARN"),
79            Level::Error => write!(f, "ERROR"),
80        }
81    }
82}
83
84impl Level {
85    /// Returns the ANSI color code for this log level.
86    ///
87    /// Used for colored terminal output:
88    /// - Trace: Cyan
89    /// - Debug: Blue
90    /// - Info: Green
91    /// - Warn: Yellow
92    /// - Error: Red
93    fn color(&self) -> &'static str {
94        match self {
95            Level::Trace => "\x1b[36m", // cyan
96            Level::Debug => "\x1b[34m", // blue
97            Level::Info => "\x1b[32m",  // green
98            Level::Warn => "\x1b[33m",  // yellow
99            Level::Error => "\x1b[31m", // red
100        }
101    }
102}
103
104/// Output destination for log messages.
105///
106/// Logs can be directed to standard output, standard error, or a file.
107/// The output can be changed at runtime using the [`Logger`] builder methods.
108#[derive(Clone)]
109pub enum Output {
110    /// Write to standard output (with colors)
111    Stdout,
112    /// Write to standard error (with colors)
113    Stderr,
114    /// Write to a file (no colors)
115    File(Arc<Mutex<BufWriter<File>>>),
116}
117
118impl Output {
119    /// Writes bytes to this output destination.
120    ///
121    /// Errors are printed to stderr but do not propagate.
122    fn write(&mut self, buf: &[u8]) {
123        let result = match self {
124            Output::Stdout => io::stdout().write_all(buf),
125            Output::Stderr => io::stderr().write_all(buf),
126            Output::File(f) => {
127                let mut guard = f.lock().unwrap();
128                guard.write_all(buf).and_then(|_| guard.flush())
129            }
130        };
131        if let Err(e) = result {
132            eprintln!("[logger] write error: {e}");
133        }
134    }
135
136    /// Flushes any buffered output.
137    ///
138    /// Ensures all pending bytes are written to the underlying destination.
139    fn flush(&mut self) {
140        let result = match self {
141            Output::Stdout => io::stdout().flush(),
142            Output::Stderr => io::stderr().flush(),
143            Output::File(f) => f.lock().unwrap().flush(),
144        };
145        if let Err(e) = result {
146            eprintln!("[logger] flush error: {e}");
147        }
148    }
149}
150
151/// Internal command sent to the logger background thread.
152enum LogCommand {
153    /// A log message to be written
154    Message {
155        level: Level,
156        body: String,
157        timestamp: SystemTime,
158    },
159    /// Request to flush buffered output
160    Flush,
161    /// Request to shut down the logger thread
162    Shutdown,
163}
164
165/// A cloneable handle for logging messages.
166///
167/// `Logger` provides methods for logging at various levels and configuring
168/// the logging behavior. It can be freely cloned and shared across threads.
169///
170/// Messages are sent to a background thread for async I/O, so logging
171/// calls return immediately without blocking.
172///
173/// # Configuration
174///
175/// Use the builder-style methods to configure the logger:
176/// - [`level()`](Self::level): Set minimum log level
177/// - [`stdout()`](Self::stdout): Route output to stdout
178/// - [`stderr()`](Self::stderr): Route output to stderr
179/// - [`file()`](Self::file): Route output to a file
180///
181/// # Example
182///
183/// ```ignore
184/// let (logger, handle) = init();
185/// let logger = logger.level(Level::Info).stdout();
186///
187/// logger.info("Starting autonomous");
188/// logger.debug("This won't print because level is Info");
189/// ```
190#[derive(Clone)]
191pub struct Logger {
192    level: Level,
193    tx: Sender<LogCommand>,
194    output: Arc<Mutex<Output>>,
195}
196
197impl Logger {
198    /// Logs a message at the given level if it meets the current threshold.
199    pub fn log(&self, level: Level, message: &str) {
200        if level < self.level {
201            return;
202        }
203        let _ = self.tx.send(LogCommand::Message {
204            level,
205            body: message.to_owned(),
206            timestamp: SystemTime::now(),
207        });
208    }
209
210    /// Flushes any buffered log output.
211    pub fn flush(&self) {
212        let _ = self.tx.send(LogCommand::Flush);
213    }
214
215    /// Logs a trace-level message.
216    pub fn trace(&self, msg: &str) {
217        self.log(Level::Trace, msg);
218    }
219    /// Logs a debug-level message.
220    pub fn debug(&self, msg: &str) {
221        self.log(Level::Debug, msg);
222    }
223    /// Logs an info-level message.
224    pub fn info(&self, msg: &str) {
225        self.log(Level::Info, msg);
226    }
227    /// Logs a warning-level message.
228    pub fn warn(&self, msg: &str) {
229        self.log(Level::Warn, msg);
230    }
231    /// Logs an error-level message.
232    pub fn error(&self, msg: &str) {
233        self.log(Level::Error, msg);
234    }
235
236    /// Sets the minimum log level for this logger.
237    pub fn level(mut self, level: Level) -> Self {
238        self.level = level;
239        self
240    }
241
242    /// Routes output to stdout.
243    pub fn stdout(self) -> Self {
244        let mut guard = self.output.lock().unwrap();
245        *guard = Output::Stdout;
246        drop(guard);
247        self
248    }
249
250    /// Routes output to stderr.
251    pub fn stderr(self) -> Self {
252        let mut guard = self.output.lock().unwrap();
253        *guard = Output::Stderr;
254        drop(guard);
255        self
256    }
257
258    /// Routes output to a file at the provided path.
259    pub fn file(self, path: &str) -> io::Result<Self> {
260        let file = OpenOptions::new().create(true).append(true).open(path)?;
261        let mut guard = self.output.lock().unwrap();
262        *guard = Output::File(Arc::new(Mutex::new(BufWriter::new(file))));
263        drop(guard);
264        Ok(self)
265    }
266}
267
268/// Handle to the logger background thread.
269///
270/// `LoggerHandle` owns the logger thread and ensures proper cleanup on drop.
271/// When the handle is dropped (or [`shutdown()`](Self::shutdown) is called),
272/// the background thread is signaled to flush and terminate.
273///
274/// # Ownership
275///
276/// Keep this handle alive for the duration you want logging to work.
277/// The [`Logger`] instances will continue to send messages, but they
278/// won't be written once the handle is dropped.
279///
280/// # Example
281///
282/// ```ignore
283/// let (logger, handle) = init();
284///
285/// // ... use logger throughout your program ...
286///
287/// // Explicit shutdown (optional - happens automatically on drop)
288/// handle.shutdown();
289/// ```
290pub struct LoggerHandle {
291    handle: Option<JoinHandle<()>>,
292    tx: Sender<LogCommand>,
293}
294
295impl LoggerHandle {
296    /// Signals the logger thread to flush and stop.
297    ///
298    /// This method:
299    /// 1. Sends a shutdown command to the background thread
300    /// 2. Waits for the thread to finish processing pending messages
301    /// 3. Joins the thread to ensure clean termination
302    ///
303    /// Called automatically when the handle is dropped.
304    pub fn shutdown(&mut self) {
305        let _ = self.tx.send(LogCommand::Shutdown);
306        if let Some(h) = self.handle.take() {
307            let _ = h.join();
308        }
309    }
310}
311
312impl Drop for LoggerHandle {
313    fn drop(&mut self) {
314        self.shutdown();
315    }
316}
317
318/// Initializes the logging system and returns a logger and its handle.
319///
320/// This function spawns a background thread named "kernelvex::logger" that
321/// processes log messages asynchronously. The thread runs until the
322/// [`LoggerHandle`] is dropped or [`shutdown()`](LoggerHandle::shutdown) is called.
323///
324/// # Returns
325///
326/// A tuple of:
327/// - [`Logger`]: Cloneable handle for logging messages (default: Trace level, stdout)
328/// - [`LoggerHandle`]: Ownership handle for the background thread
329///
330/// # Example
331///
332/// ```ignore
333/// let (logger, handle) = init();
334/// let logger = logger.level(Level::Info);
335///
336/// logger.info("Logger initialized");
337/// ```
338///
339/// # Panics
340///
341/// Panics if the background thread cannot be spawned.
342pub fn init() -> (Logger, LoggerHandle) {
343    let (tx, rx) = mpsc::channel::<LogCommand>();
344
345    let output = Arc::new(Mutex::new(Output::Stdout));
346    let thread_output = Arc::clone(&output);
347
348    let handle = thread::Builder::new()
349        .name("kernelvex::logger".into())
350        .spawn(move || {
351            for cmd in rx {
352                match cmd {
353                    LogCommand::Message {
354                        level,
355                        body,
356                        timestamp,
357                    } => {
358                        let ts = humantime::format_rfc3339_seconds(timestamp);
359                        let tid = thread_id::get();
360                        let mut guard = thread_output.lock().unwrap();
361                        let line = if !matches!(*guard, Output::File(_)) {
362                            let color = level.color();
363                            let reset = "\x1b[0m";
364                            format!("[{ts}] [{color}{level}{reset}] [tid:{tid}] {body}\n")
365                        } else {
366                            format!("[{ts}] [{level}] [tid:{tid}] {body}\n")
367                        };
368
369                        guard.write(line.as_bytes());
370                    }
371                    LogCommand::Flush => {
372                        let mut guard = thread_output.lock().unwrap();
373                        guard.flush();
374                    }
375                    LogCommand::Shutdown => {
376                        let mut guard = thread_output.lock().unwrap();
377                        guard.flush();
378                        break;
379                    }
380                }
381            }
382        })
383        .expect("failed to spawn logger thread");
384
385    let logger = Logger {
386        level: Level::Trace,
387        tx: tx.clone(),
388        output,
389    };
390    let owner = LoggerHandle {
391        handle: Some(handle),
392        tx,
393    };
394
395    (logger, owner)
396}