Skip to main content

termivar_scanner/
logging.rs

1//! Structured Logging System for Termivar Scanner
2//!
3//! ## Runtime scope
4//!
5//! - **Build:** opt-in via `legacy-scanner`.
6//! - **Execution:** historical scanner runtime support
7//!   (`ScanRunner` emits `LogEntry` values).
8//! - **Default `termivar scan`:** no.
9//! - **Support:** implemented.
10//!
11//! See `docs/internals/runtime-map.md`.
12//!
13//! Provides consistent, contextualized logging across all scanning phases
14//! with performance metrics, timing information, and structured output.
15
16use std::fmt;
17use std::time::{SystemTime, UNIX_EPOCH};
18
19/// Log severity levels
20#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
21pub enum LogLevel {
22    Debug,
23    Info,
24    Warn,
25    Error,
26}
27
28impl fmt::Display for LogLevel {
29    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
30        match self {
31            LogLevel::Debug => write!(f, "DEBUG"),
32            LogLevel::Info => write!(f, "INFO"),
33            LogLevel::Warn => write!(f, "WARN"),
34            LogLevel::Error => write!(f, "ERROR"),
35        }
36    }
37}
38
39/// Structured log entry with timestamp, level, and context
40#[derive(Debug, Clone)]
41pub struct LogEntry {
42    /// Unix timestamp (milliseconds)
43    pub timestamp: u64,
44    /// Log severity level
45    pub level: LogLevel,
46    /// Associated phase number (1-10)
47    pub phase: Option<u8>,
48    /// Log message
49    pub message: String,
50    /// Optional context (URL, parameter, etc.)
51    pub context: Option<String>,
52    /// Elapsed time in milliseconds (for performance logs)
53    pub duration_ms: Option<u64>,
54}
55
56impl LogEntry {
57    /// Creates a new log entry
58    pub fn new(level: LogLevel, message: String) -> Self {
59        Self {
60            timestamp: current_timestamp_ms(),
61            level,
62            phase: None,
63            message,
64            context: None,
65            duration_ms: None,
66        }
67    }
68
69    /// Adds phase information to log entry
70    pub fn with_phase(mut self, phase: u8) -> Self {
71        self.phase = Some(phase);
72        self
73    }
74
75    /// Adds context information to log entry
76    pub fn with_context(mut self, context: String) -> Self {
77        self.context = Some(context);
78        self
79    }
80
81    /// Adds performance timing to log entry
82    pub fn with_duration(mut self, duration_ms: u64) -> Self {
83        self.duration_ms = Some(duration_ms);
84        self
85    }
86
87    /// Formats log entry as a string
88    pub fn format(&self) -> String {
89        let mut output = format!("[{}] [{}]", self.timestamp, self.level);
90
91        if let Some(phase) = self.phase {
92            output.push_str(&format!(" [Phase {phase}]"));
93        }
94
95        output.push_str(&format!(" {}", self.message));
96
97        if let Some(ctx) = &self.context {
98            output.push_str(&format!(" | {ctx}"));
99        }
100
101        if let Some(duration) = self.duration_ms {
102            output.push_str(&format!(" | {duration}ms"));
103        }
104
105        output
106    }
107}
108
109impl fmt::Display for LogEntry {
110    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
111        write!(f, "{}", self.format())
112    }
113}
114
115/// Logger for structured output
116pub struct Logger {
117    level: LogLevel,
118}
119
120impl Logger {
121    /// Creates a new logger with specified minimum level
122    pub fn new(level: LogLevel) -> Self {
123        Self { level }
124    }
125
126    /// Logs a message if level permits
127    pub fn log(&self, entry: LogEntry) {
128        if entry.level >= self.level {
129            println!("{}", entry.format());
130        }
131    }
132
133    /// Logs debug message
134    pub fn debug(&self, message: String) {
135        self.log(LogEntry::new(LogLevel::Debug, message));
136    }
137
138    /// Logs info message
139    pub fn info(&self, message: String) {
140        self.log(LogEntry::new(LogLevel::Info, message));
141    }
142
143    /// Logs warning message
144    pub fn warn(&self, message: String) {
145        self.log(LogEntry::new(LogLevel::Warn, message));
146    }
147
148    /// Logs error message
149    pub fn error(&self, message: String) {
150        self.log(LogEntry::new(LogLevel::Error, message));
151    }
152
153    /// Logs phase execution with timing
154    pub fn phase_start(&self, phase: u8, name: &str) {
155        self.log(
156            LogEntry::new(LogLevel::Info, format!("Starting Phase {phase}: {name}"))
157                .with_phase(phase),
158        );
159    }
160
161    /// Logs phase completion with timing
162    pub fn phase_complete(&self, phase: u8, count: usize, duration_ms: u64) {
163        self.log(
164            LogEntry::new(
165                LogLevel::Info,
166                format!("Phase {phase} completed. Found {count} issues."),
167            )
168            .with_phase(phase)
169            .with_duration(duration_ms),
170        );
171    }
172}
173
174impl Default for Logger {
175    fn default() -> Self {
176        Self::new(LogLevel::Info)
177    }
178}
179
180/// Returns current Unix timestamp in milliseconds
181fn current_timestamp_ms() -> u64 {
182    SystemTime::now()
183        .duration_since(UNIX_EPOCH)
184        .map(|d| d.as_millis() as u64)
185        .unwrap_or(0)
186}
187
188#[cfg(test)]
189mod tests {
190    use super::*;
191
192    #[test]
193    fn test_log_entry_creation() {
194        let entry = LogEntry::new(LogLevel::Info, "Test message".to_string());
195        assert_eq!(entry.level, LogLevel::Info);
196        assert_eq!(entry.message, "Test message");
197        assert!(entry.phase.is_none());
198    }
199
200    #[test]
201    fn test_log_entry_with_context() {
202        let entry = LogEntry::new(LogLevel::Warn, "Warning".to_string())
203            .with_context("https://example.com".to_string());
204        assert_eq!(entry.context, Some("https://example.com".to_string()));
205    }
206
207    #[test]
208    fn test_log_entry_with_phase() {
209        let entry = LogEntry::new(LogLevel::Debug, "Debug message".to_string()).with_phase(5);
210        assert_eq!(entry.phase, Some(5));
211    }
212
213    #[test]
214    fn test_log_entry_with_duration() {
215        let entry = LogEntry::new(LogLevel::Info, "Completed".to_string()).with_duration(1234);
216        assert_eq!(entry.duration_ms, Some(1234));
217    }
218
219    #[test]
220    fn test_log_level_ordering() {
221        assert!(LogLevel::Error > LogLevel::Warn);
222        assert!(LogLevel::Warn > LogLevel::Info);
223        assert!(LogLevel::Info > LogLevel::Debug);
224    }
225
226    #[test]
227    fn test_logger_filtering() {
228        let logger = Logger::new(LogLevel::Warn);
229        // Should not panic, just not log debug/info
230        logger.debug("Debug message".to_string());
231        logger.info("Info message".to_string());
232        logger.warn("Warning message".to_string());
233        logger.phase_start(3, "bounded review");
234        logger.phase_complete(3, 2, 17);
235    }
236}