Skip to main content

oxihuman_core/
logger.rs

1//! Structured logging system with levels and categories.
2
3// ---------------------------------------------------------------------------
4// Types
5// ---------------------------------------------------------------------------
6
7/// Log severity levels.
8#[allow(dead_code)]
9#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
10pub enum LogLevel {
11    Trace = 0,
12    Debug = 1,
13    Info = 2,
14    Warn = 3,
15    Error = 4,
16}
17
18impl LogLevel {
19    fn as_str(self) -> &'static str {
20        match self {
21            LogLevel::Trace => "TRACE",
22            LogLevel::Debug => "DEBUG",
23            LogLevel::Info => "INFO",
24            LogLevel::Warn => "WARN",
25            LogLevel::Error => "ERROR",
26        }
27    }
28}
29
30/// A single log entry.
31#[allow(dead_code)]
32#[derive(Clone, Debug)]
33pub struct LogEntry {
34    /// Entry sequence number.
35    pub id: u64,
36    /// Severity level.
37    pub level: LogLevel,
38    /// Category tag (e.g. "render", "physics", "io").
39    pub category: String,
40    /// The log message.
41    pub message: String,
42}
43
44/// A structured logger that stores entries in memory.
45#[allow(dead_code)]
46pub struct Logger {
47    /// All recorded entries.
48    pub entries: Vec<LogEntry>,
49    /// Minimum level to accept (entries below this are ignored).
50    pub min_level: LogLevel,
51    /// Auto-incrementing ID counter.
52    next_id: u64,
53}
54
55// ---------------------------------------------------------------------------
56// Construction
57// ---------------------------------------------------------------------------
58
59/// Create a new logger with the given minimum level.
60#[allow(dead_code)]
61pub fn new_logger(min_level: LogLevel) -> Logger {
62    Logger {
63        entries: Vec::new(),
64        min_level,
65        next_id: 1,
66    }
67}
68
69// ---------------------------------------------------------------------------
70// Logging functions
71// ---------------------------------------------------------------------------
72
73/// Log a message at the specified level and category.
74/// Entries below the logger's minimum level are silently ignored.
75#[allow(dead_code)]
76pub fn log_message(logger: &mut Logger, level: LogLevel, category: &str, message: &str) {
77    if level < logger.min_level {
78        return;
79    }
80    let entry = LogEntry {
81        id: logger.next_id,
82        level,
83        category: category.to_string(),
84        message: message.to_string(),
85    };
86    logger.next_id += 1;
87    logger.entries.push(entry);
88}
89
90/// Log a trace-level message.
91#[allow(dead_code)]
92pub fn log_trace(logger: &mut Logger, category: &str, message: &str) {
93    log_message(logger, LogLevel::Trace, category, message);
94}
95
96/// Log a debug-level message.
97#[allow(dead_code)]
98pub fn log_debug(logger: &mut Logger, category: &str, message: &str) {
99    log_message(logger, LogLevel::Debug, category, message);
100}
101
102/// Log an info-level message.
103#[allow(dead_code)]
104pub fn log_info(logger: &mut Logger, category: &str, message: &str) {
105    log_message(logger, LogLevel::Info, category, message);
106}
107
108/// Log a warn-level message.
109#[allow(dead_code)]
110pub fn log_warn(logger: &mut Logger, category: &str, message: &str) {
111    log_message(logger, LogLevel::Warn, category, message);
112}
113
114/// Log an error-level message.
115#[allow(dead_code)]
116pub fn log_error(logger: &mut Logger, category: &str, message: &str) {
117    log_message(logger, LogLevel::Error, category, message);
118}
119
120// ---------------------------------------------------------------------------
121// Configuration
122// ---------------------------------------------------------------------------
123
124/// Change the minimum log level (entries below this are dropped).
125#[allow(dead_code)]
126pub fn set_min_level(logger: &mut Logger, level: LogLevel) {
127    logger.min_level = level;
128}
129
130// ---------------------------------------------------------------------------
131// Queries
132// ---------------------------------------------------------------------------
133
134/// Return the total number of stored entries.
135#[allow(dead_code)]
136pub fn entry_count(logger: &Logger) -> usize {
137    logger.entries.len()
138}
139
140/// Return entries matching a specific level.
141#[allow(dead_code)]
142pub fn entries_by_level(logger: &Logger, level: LogLevel) -> Vec<&LogEntry> {
143    logger.entries.iter().filter(|e| e.level == level).collect()
144}
145
146/// Clear all stored log entries.
147#[allow(dead_code)]
148pub fn clear_log(logger: &mut Logger) {
149    logger.entries.clear();
150}
151
152/// Serialize the log to a JSON string.
153#[allow(dead_code)]
154pub fn logger_to_json(logger: &Logger) -> String {
155    let mut buf = String::from("[");
156    for (i, entry) in logger.entries.iter().enumerate() {
157        if i > 0 {
158            buf.push(',');
159        }
160        buf.push_str(&format!(
161            r#"{{"id":{},"level":"{}","category":"{}","message":"{}"}}"#,
162            entry.id,
163            entry.level.as_str(),
164            entry.category,
165            entry.message
166        ));
167    }
168    buf.push(']');
169    buf
170}
171
172/// Return entries matching a specific category.
173#[allow(dead_code)]
174pub fn filter_by_category<'a>(logger: &'a Logger, category: &str) -> Vec<&'a LogEntry> {
175    logger
176        .entries
177        .iter()
178        .filter(|e| e.category == category)
179        .collect()
180}
181
182/// Return the last `n` entries (or fewer if the log is smaller).
183#[allow(dead_code)]
184pub fn last_n_entries(logger: &Logger, n: usize) -> Vec<&LogEntry> {
185    let len = logger.entries.len();
186    let start = len.saturating_sub(n);
187    logger.entries[start..].iter().collect()
188}
189
190/// Check whether the log contains any error-level entries.
191#[allow(dead_code)]
192pub fn has_errors(logger: &Logger) -> bool {
193    logger.entries.iter().any(|e| e.level == LogLevel::Error)
194}
195
196// ---------------------------------------------------------------------------
197// Tests
198// ---------------------------------------------------------------------------
199
200#[cfg(test)]
201mod tests {
202    use super::*;
203
204    #[test]
205    fn test_new_logger() {
206        let l = new_logger(LogLevel::Info);
207        assert_eq!(entry_count(&l), 0);
208        assert_eq!(l.min_level, LogLevel::Info);
209    }
210
211    #[test]
212    fn test_log_message_above_min() {
213        let mut l = new_logger(LogLevel::Debug);
214        log_message(&mut l, LogLevel::Info, "cat", "hello");
215        assert_eq!(entry_count(&l), 1);
216    }
217
218    #[test]
219    fn test_log_message_below_min_ignored() {
220        let mut l = new_logger(LogLevel::Warn);
221        log_message(&mut l, LogLevel::Debug, "cat", "dropped");
222        assert_eq!(entry_count(&l), 0);
223    }
224
225    #[test]
226    fn test_log_trace() {
227        let mut l = new_logger(LogLevel::Trace);
228        log_trace(&mut l, "test", "trace msg");
229        assert_eq!(entry_count(&l), 1);
230        assert_eq!(l.entries[0].level, LogLevel::Trace);
231    }
232
233    #[test]
234    fn test_log_debug() {
235        let mut l = new_logger(LogLevel::Trace);
236        log_debug(&mut l, "test", "debug msg");
237        assert_eq!(l.entries[0].level, LogLevel::Debug);
238    }
239
240    #[test]
241    fn test_log_info() {
242        let mut l = new_logger(LogLevel::Trace);
243        log_info(&mut l, "test", "info msg");
244        assert_eq!(l.entries[0].level, LogLevel::Info);
245    }
246
247    #[test]
248    fn test_log_warn() {
249        let mut l = new_logger(LogLevel::Trace);
250        log_warn(&mut l, "test", "warn msg");
251        assert_eq!(l.entries[0].level, LogLevel::Warn);
252    }
253
254    #[test]
255    fn test_log_error() {
256        let mut l = new_logger(LogLevel::Trace);
257        log_error(&mut l, "test", "error msg");
258        assert_eq!(l.entries[0].level, LogLevel::Error);
259    }
260
261    #[test]
262    fn test_set_min_level() {
263        let mut l = new_logger(LogLevel::Trace);
264        log_trace(&mut l, "a", "ok");
265        assert_eq!(entry_count(&l), 1);
266        set_min_level(&mut l, LogLevel::Error);
267        log_trace(&mut l, "a", "dropped");
268        assert_eq!(entry_count(&l), 1);
269    }
270
271    #[test]
272    fn test_entries_by_level() {
273        let mut l = new_logger(LogLevel::Trace);
274        log_info(&mut l, "a", "i1");
275        log_warn(&mut l, "a", "w1");
276        log_info(&mut l, "a", "i2");
277        assert_eq!(entries_by_level(&l, LogLevel::Info).len(), 2);
278        assert_eq!(entries_by_level(&l, LogLevel::Warn).len(), 1);
279    }
280
281    #[test]
282    fn test_clear_log() {
283        let mut l = new_logger(LogLevel::Trace);
284        log_info(&mut l, "a", "msg");
285        clear_log(&mut l);
286        assert_eq!(entry_count(&l), 0);
287    }
288
289    #[test]
290    fn test_logger_to_json() {
291        let mut l = new_logger(LogLevel::Trace);
292        log_info(&mut l, "render", "frame done");
293        let json = logger_to_json(&l);
294        assert!(json.contains("render"));
295        assert!(json.contains("frame done"));
296        assert!(json.starts_with('['));
297        assert!(json.ends_with(']'));
298    }
299
300    #[test]
301    fn test_filter_by_category() {
302        let mut l = new_logger(LogLevel::Trace);
303        log_info(&mut l, "render", "r1");
304        log_info(&mut l, "physics", "p1");
305        log_info(&mut l, "render", "r2");
306        assert_eq!(filter_by_category(&l, "render").len(), 2);
307        assert_eq!(filter_by_category(&l, "physics").len(), 1);
308        assert_eq!(filter_by_category(&l, "audio").len(), 0);
309    }
310
311    #[test]
312    fn test_last_n_entries() {
313        let mut l = new_logger(LogLevel::Trace);
314        for i in 0..10 {
315            log_info(&mut l, "t", &format!("msg{i}"));
316        }
317        let last3 = last_n_entries(&l, 3);
318        assert_eq!(last3.len(), 3);
319        assert!(last3[0].message.contains("msg7"));
320    }
321
322    #[test]
323    fn test_last_n_entries_more_than_available() {
324        let mut l = new_logger(LogLevel::Trace);
325        log_info(&mut l, "t", "only one");
326        let result = last_n_entries(&l, 100);
327        assert_eq!(result.len(), 1);
328    }
329
330    #[test]
331    fn test_has_errors_false() {
332        let mut l = new_logger(LogLevel::Trace);
333        log_info(&mut l, "a", "fine");
334        log_warn(&mut l, "a", "warning");
335        assert!(!has_errors(&l));
336    }
337
338    #[test]
339    fn test_has_errors_true() {
340        let mut l = new_logger(LogLevel::Trace);
341        log_error(&mut l, "a", "bad");
342        assert!(has_errors(&l));
343    }
344}