adaptive_pipeline_bootstrap/
logger.rs

1// /////////////////////////////////////////////////////////////////////////////
2// Adaptive Pipeline
3// Copyright (c) 2025 Michael Gardner, A Bit of Help, Inc.
4// SPDX-License-Identifier: BSD-3-Clause
5// See LICENSE file in the project root.
6// /////////////////////////////////////////////////////////////////////////////
7
8//! # Bootstrap Logger
9//!
10//! Lightweight logging abstraction for the bootstrap phase.
11//!
12//! ## Design Rationale
13//!
14//! The bootstrap logger is a **simplified logging interface** specifically for
15//! bootstrap-phase operations. It provides:
16//!
17//! - **Minimal API** - Only essential log levels
18//! - **Trait-based** - Testable with no-op implementation
19//! - **Integration-ready** - Can wrap tracing, env_logger, or custom loggers
20//! - **Bootstrap-specific** - Separate from application logging
21//!
22//! ## Log Levels
23//!
24//! - **Error** - Fatal errors during bootstrap
25//! - **Warn** - Non-fatal issues (missing optional config, etc.)
26//! - **Info** - Normal bootstrap messages
27//! - **Debug** - Detailed bootstrap information
28//!
29//! ## Usage
30//!
31//! ```rust
32//! use adaptive_pipeline_bootstrap::logger::{BootstrapLogger, ConsoleLogger};
33//!
34//! let logger = ConsoleLogger::new();
35//! logger.info("Starting application bootstrap");
36//! logger.debug("Parsing command line arguments");
37//! ```
38
39#[cfg(test)]
40use std::fmt;
41
42/// Bootstrap logging abstraction
43///
44/// Provides a simple logging interface for bootstrap operations.
45/// Implementations can use tracing, env_logger, or custom backends.
46pub trait BootstrapLogger: Send + Sync {
47    /// Log an error message
48    ///
49    /// Used for fatal errors during bootstrap that will cause termination.
50    fn error(&self, message: &str);
51
52    /// Log a warning message
53    ///
54    /// Used for non-fatal issues that may affect operation.
55    fn warn(&self, message: &str);
56
57    /// Log an info message
58    ///
59    /// Used for normal bootstrap progress messages.
60    fn info(&self, message: &str);
61
62    /// Log a debug message
63    ///
64    /// Used for detailed diagnostic information during bootstrap.
65    fn debug(&self, message: &str);
66}
67
68/// Console logger implementation using tracing
69///
70/// Routes bootstrap logs through the tracing crate for consistent logging.
71pub struct ConsoleLogger {
72    prefix: String,
73}
74
75impl ConsoleLogger {
76    /// Create a new console logger with default prefix
77    pub fn new() -> Self {
78        Self::with_prefix("bootstrap")
79    }
80
81    /// Create a new console logger with custom prefix
82    pub fn with_prefix(prefix: impl Into<String>) -> Self {
83        Self { prefix: prefix.into() }
84    }
85}
86
87impl Default for ConsoleLogger {
88    fn default() -> Self {
89        Self::new()
90    }
91}
92
93impl BootstrapLogger for ConsoleLogger {
94    fn error(&self, message: &str) {
95        tracing::error!(target: "bootstrap", "[{}] {}", self.prefix, message);
96    }
97
98    fn warn(&self, message: &str) {
99        tracing::warn!(target: "bootstrap", "[{}] {}", self.prefix, message);
100    }
101
102    fn info(&self, message: &str) {
103        tracing::info!(target: "bootstrap", "[{}] {}", self.prefix, message);
104    }
105
106    fn debug(&self, message: &str) {
107        tracing::debug!(target: "bootstrap", "[{}] {}", self.prefix, message);
108    }
109}
110
111/// No-op logger for testing
112///
113/// Discards all log messages. Useful for testing bootstrap logic
114/// without generating log output.
115pub struct NoOpLogger;
116
117impl NoOpLogger {
118    /// Create a new no-op logger
119    pub fn new() -> Self {
120        Self
121    }
122}
123
124impl Default for NoOpLogger {
125    fn default() -> Self {
126        Self::new()
127    }
128}
129
130impl BootstrapLogger for NoOpLogger {
131    fn error(&self, _message: &str) {}
132    fn warn(&self, _message: &str) {}
133    fn info(&self, _message: &str) {}
134    fn debug(&self, _message: &str) {}
135}
136
137/// Capturing logger for testing
138///
139/// Captures log messages in memory for assertion in tests.
140#[cfg(test)]
141pub struct CapturingLogger {
142    messages: std::sync::Arc<std::sync::Mutex<Vec<LogMessage>>>,
143}
144
145#[cfg(test)]
146#[derive(Debug, Clone)]
147pub struct LogMessage {
148    pub level: LogLevel,
149    pub message: String,
150}
151
152#[cfg(test)]
153#[derive(Debug, Clone, Copy, PartialEq, Eq)]
154pub enum LogLevel {
155    Error,
156    Warn,
157    Info,
158    Debug,
159}
160
161#[cfg(test)]
162impl Default for CapturingLogger {
163    fn default() -> Self {
164        Self::new()
165    }
166}
167
168#[cfg(test)]
169impl CapturingLogger {
170    pub fn new() -> Self {
171        Self {
172            messages: std::sync::Arc::new(std::sync::Mutex::new(Vec::new())),
173        }
174    }
175
176    pub fn messages(&self) -> Vec<LogMessage> {
177        self.messages.lock().unwrap().clone()
178    }
179
180    pub fn clear(&self) {
181        self.messages.lock().unwrap().clear();
182    }
183
184    fn log(&self, level: LogLevel, message: &str) {
185        self.messages.lock().unwrap().push(LogMessage {
186            level,
187            message: message.to_string(),
188        });
189    }
190}
191
192#[cfg(test)]
193impl BootstrapLogger for CapturingLogger {
194    fn error(&self, message: &str) {
195        self.log(LogLevel::Error, message);
196    }
197
198    fn warn(&self, message: &str) {
199        self.log(LogLevel::Warn, message);
200    }
201
202    fn info(&self, message: &str) {
203        self.log(LogLevel::Info, message);
204    }
205
206    fn debug(&self, message: &str) {
207        self.log(LogLevel::Debug, message);
208    }
209}
210
211#[cfg(test)]
212impl fmt::Display for LogLevel {
213    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
214        match self {
215            LogLevel::Error => write!(f, "ERROR"),
216            LogLevel::Warn => write!(f, "WARN"),
217            LogLevel::Info => write!(f, "INFO"),
218            LogLevel::Debug => write!(f, "DEBUG"),
219        }
220    }
221}
222
223#[cfg(test)]
224mod tests {
225    use super::*;
226
227    #[test]
228    fn test_console_logger_creation() {
229        let logger = ConsoleLogger::new();
230        // Just verify it doesn't panic
231        logger.info("test message");
232    }
233
234    #[test]
235    fn test_console_logger_with_prefix() {
236        let logger = ConsoleLogger::with_prefix("custom");
237        // Just verify it doesn't panic
238        logger.debug("test message");
239    }
240
241    #[test]
242    fn test_noop_logger() {
243        let logger = NoOpLogger::new();
244        // Should not panic or produce output
245        logger.error("error");
246        logger.warn("warning");
247        logger.info("info");
248        logger.debug("debug");
249    }
250
251    #[test]
252    fn test_capturing_logger() {
253        let logger = CapturingLogger::new();
254
255        logger.error("error message");
256        logger.warn("warning message");
257        logger.info("info message");
258        logger.debug("debug message");
259
260        let messages = logger.messages();
261        assert_eq!(messages.len(), 4);
262
263        assert_eq!(messages[0].level, LogLevel::Error);
264        assert_eq!(messages[0].message, "error message");
265
266        assert_eq!(messages[1].level, LogLevel::Warn);
267        assert_eq!(messages[1].message, "warning message");
268
269        assert_eq!(messages[2].level, LogLevel::Info);
270        assert_eq!(messages[2].message, "info message");
271
272        assert_eq!(messages[3].level, LogLevel::Debug);
273        assert_eq!(messages[3].message, "debug message");
274    }
275
276    #[test]
277    fn test_capturing_logger_clear() {
278        let logger = CapturingLogger::new();
279
280        logger.info("message 1");
281        logger.info("message 2");
282        assert_eq!(logger.messages().len(), 2);
283
284        logger.clear();
285        assert_eq!(logger.messages().len(), 0);
286
287        logger.info("message 3");
288        assert_eq!(logger.messages().len(), 1);
289    }
290
291    #[test]
292    fn test_log_level_display() {
293        assert_eq!(format!("{}", LogLevel::Error), "ERROR");
294        assert_eq!(format!("{}", LogLevel::Warn), "WARN");
295        assert_eq!(format!("{}", LogLevel::Info), "INFO");
296        assert_eq!(format!("{}", LogLevel::Debug), "DEBUG");
297    }
298}