Skip to main content

oxihuman_core/
event_log.rs

1//! Event logging and replay system.
2
3#[allow(dead_code)]
4#[derive(Clone, PartialEq, Debug)]
5pub enum LogLevel {
6    Trace,
7    Debug,
8    Info,
9    Warn,
10    Error,
11}
12
13impl LogLevel {
14    fn severity(&self) -> u8 {
15        match self {
16            LogLevel::Trace => 0,
17            LogLevel::Debug => 1,
18            LogLevel::Info => 2,
19            LogLevel::Warn => 3,
20            LogLevel::Error => 4,
21        }
22    }
23
24    fn as_str(&self) -> &'static str {
25        match self {
26            LogLevel::Trace => "TRACE",
27            LogLevel::Debug => "DEBUG",
28            LogLevel::Info => "INFO",
29            LogLevel::Warn => "WARN",
30            LogLevel::Error => "ERROR",
31        }
32    }
33}
34
35#[allow(dead_code)]
36#[derive(Clone)]
37pub struct LogEvent {
38    pub id: u64,
39    pub level: LogLevel,
40    pub category: String,
41    pub message: String,
42    pub timestamp: u64,
43    pub data: Vec<(String, String)>,
44}
45
46#[allow(dead_code)]
47pub struct EventLog {
48    pub events: Vec<LogEvent>,
49    pub max_events: usize,
50    pub tick: u64,
51    pub next_id: u64,
52    pub enabled: bool,
53}
54
55#[allow(dead_code)]
56pub fn new_event_log(max_events: usize) -> EventLog {
57    EventLog {
58        events: Vec::new(),
59        max_events,
60        tick: 0,
61        next_id: 1,
62        enabled: true,
63    }
64}
65
66#[allow(dead_code)]
67pub fn log_event(log: &mut EventLog, level: LogLevel, category: &str, msg: &str) {
68    log_with_data(log, level, category, msg, Vec::new());
69}
70
71#[allow(dead_code)]
72pub fn log_with_data(
73    log: &mut EventLog,
74    level: LogLevel,
75    category: &str,
76    msg: &str,
77    data: Vec<(String, String)>,
78) {
79    if !log.enabled {
80        return;
81    }
82    log.tick += 1;
83    let event = LogEvent {
84        id: log.next_id,
85        level,
86        category: category.to_string(),
87        message: msg.to_string(),
88        timestamp: log.tick,
89        data,
90    };
91    log.next_id += 1;
92    log.events.push(event);
93    trim_log(log);
94}
95
96#[allow(dead_code)]
97pub fn filter_by_level(log: &EventLog, min_level: LogLevel) -> Vec<&LogEvent> {
98    let min_sev = min_level.severity();
99    log.events
100        .iter()
101        .filter(|e| e.level.severity() >= min_sev)
102        .collect()
103}
104
105#[allow(dead_code)]
106pub fn filter_by_category<'a>(log: &'a EventLog, category: &str) -> Vec<&'a LogEvent> {
107    log.events
108        .iter()
109        .filter(|e| e.category == category)
110        .collect()
111}
112
113#[allow(dead_code)]
114pub fn event_count(log: &EventLog) -> usize {
115    log.events.len()
116}
117
118#[allow(dead_code)]
119pub fn clear_log(log: &mut EventLog) {
120    log.events.clear();
121}
122
123#[allow(dead_code)]
124pub fn last_event(log: &EventLog) -> Option<&LogEvent> {
125    log.events.last()
126}
127
128#[allow(dead_code)]
129pub fn events_since(log: &EventLog, tick: u64) -> Vec<&LogEvent> {
130    log.events.iter().filter(|e| e.timestamp > tick).collect()
131}
132
133#[allow(dead_code)]
134pub fn error_count(log: &EventLog) -> usize {
135    log.events
136        .iter()
137        .filter(|e| e.level == LogLevel::Error)
138        .count()
139}
140
141#[allow(dead_code)]
142pub fn warn_count(log: &EventLog) -> usize {
143    log.events
144        .iter()
145        .filter(|e| e.level == LogLevel::Warn)
146        .count()
147}
148
149#[allow(dead_code)]
150pub fn serialize_log_json(log: &EventLog) -> String {
151    let mut parts: Vec<String> = Vec::new();
152    for e in &log.events {
153        let data_parts: Vec<String> = e
154            .data
155            .iter()
156            .map(|(k, v)| format!("{{\"key\":\"{}\",\"val\":\"{}\"}}", k, v))
157            .collect();
158        let data_json = format!("[{}]", data_parts.join(","));
159        parts.push(format!(
160            "{{\"id\":{},\"level\":\"{}\",\"category\":\"{}\",\"message\":\"{}\",\"timestamp\":{},\"data\":{}}}",
161            e.id,
162            e.level.as_str(),
163            e.category,
164            e.message,
165            e.timestamp,
166            data_json
167        ));
168    }
169    format!("[{}]", parts.join(","))
170}
171
172#[allow(dead_code)]
173pub fn trim_log(log: &mut EventLog) {
174    if log.max_events == 0 {
175        return;
176    }
177    while log.events.len() > log.max_events {
178        log.events.remove(0);
179    }
180}
181
182#[cfg(test)]
183mod tests {
184    use super::*;
185
186    #[test]
187    fn test_new_event_log() {
188        let log = new_event_log(100);
189        assert!(log.events.is_empty());
190        assert_eq!(log.max_events, 100);
191        assert!(log.enabled);
192    }
193
194    #[test]
195    fn test_log_event_adds_event() {
196        let mut log = new_event_log(100);
197        log_event(&mut log, LogLevel::Info, "test", "hello");
198        assert_eq!(event_count(&log), 1);
199    }
200
201    #[test]
202    fn test_filter_by_level_info_up() {
203        let mut log = new_event_log(100);
204        log_event(&mut log, LogLevel::Debug, "cat", "debug msg");
205        log_event(&mut log, LogLevel::Info, "cat", "info msg");
206        log_event(&mut log, LogLevel::Error, "cat", "error msg");
207        let filtered = filter_by_level(&log, LogLevel::Info);
208        assert_eq!(filtered.len(), 2);
209    }
210
211    #[test]
212    fn test_filter_by_category() {
213        let mut log = new_event_log(100);
214        log_event(&mut log, LogLevel::Info, "mesh", "msg1");
215        log_event(&mut log, LogLevel::Info, "morph", "msg2");
216        log_event(&mut log, LogLevel::Info, "mesh", "msg3");
217        let filtered = filter_by_category(&log, "mesh");
218        assert_eq!(filtered.len(), 2);
219    }
220
221    #[test]
222    fn test_error_count() {
223        let mut log = new_event_log(100);
224        log_event(&mut log, LogLevel::Info, "c", "m");
225        log_event(&mut log, LogLevel::Error, "c", "e1");
226        log_event(&mut log, LogLevel::Error, "c", "e2");
227        assert_eq!(error_count(&log), 2);
228    }
229
230    #[test]
231    fn test_warn_count() {
232        let mut log = new_event_log(100);
233        log_event(&mut log, LogLevel::Warn, "c", "w1");
234        log_event(&mut log, LogLevel::Info, "c", "i");
235        assert_eq!(warn_count(&log), 1);
236    }
237
238    #[test]
239    fn test_clear_log() {
240        let mut log = new_event_log(100);
241        log_event(&mut log, LogLevel::Info, "c", "m");
242        log_event(&mut log, LogLevel::Info, "c", "m");
243        clear_log(&mut log);
244        assert_eq!(event_count(&log), 0);
245    }
246
247    #[test]
248    fn test_last_event() {
249        let mut log = new_event_log(100);
250        log_event(&mut log, LogLevel::Info, "c", "first");
251        log_event(&mut log, LogLevel::Error, "c", "last");
252        let last = last_event(&log).expect("should succeed");
253        assert_eq!(last.message, "last");
254    }
255
256    #[test]
257    fn test_last_event_empty() {
258        let log = new_event_log(100);
259        assert!(last_event(&log).is_none());
260    }
261
262    #[test]
263    fn test_events_since() {
264        let mut log = new_event_log(100);
265        log_event(&mut log, LogLevel::Info, "c", "m1");
266        let tick_after_first = log.tick;
267        log_event(&mut log, LogLevel::Info, "c", "m2");
268        log_event(&mut log, LogLevel::Info, "c", "m3");
269        let since = events_since(&log, tick_after_first);
270        assert_eq!(since.len(), 2);
271    }
272
273    #[test]
274    fn test_trim_enforces_max() {
275        let mut log = new_event_log(3);
276        for i in 0..6 {
277            log_event(&mut log, LogLevel::Info, "c", &format!("msg{}", i));
278        }
279        assert!(log.events.len() <= 3);
280    }
281
282    #[test]
283    fn test_serialize_non_empty() {
284        let mut log = new_event_log(100);
285        log_event(&mut log, LogLevel::Info, "cat", "hello world");
286        let json = serialize_log_json(&log);
287        assert!(json.contains("hello world"));
288        assert!(json.contains("INFO"));
289        assert!(json.starts_with('['));
290        assert!(json.ends_with(']'));
291    }
292
293    #[test]
294    fn test_log_with_data() {
295        let mut log = new_event_log(100);
296        log_with_data(
297            &mut log,
298            LogLevel::Debug,
299            "sys",
300            "test",
301            vec![("key1".to_string(), "val1".to_string())],
302        );
303        let e = last_event(&log).expect("should succeed");
304        assert_eq!(e.data.len(), 1);
305        assert_eq!(e.data[0].0, "key1");
306    }
307
308    #[test]
309    fn test_ids_increment() {
310        let mut log = new_event_log(100);
311        log_event(&mut log, LogLevel::Info, "c", "m1");
312        log_event(&mut log, LogLevel::Info, "c", "m2");
313        assert!(log.events[1].id > log.events[0].id);
314    }
315
316    #[test]
317    fn test_disabled_log_ignores_events() {
318        let mut log = new_event_log(100);
319        log.enabled = false;
320        log_event(&mut log, LogLevel::Error, "c", "msg");
321        assert_eq!(event_count(&log), 0);
322    }
323}