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}