Skip to main content

oxihuman_core/
error_log.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5//! Error log: a bounded, categorised list of runtime errors.
6
7use std::fmt;
8
9/// Severity level of an error entry.
10#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
11#[allow(dead_code)]
12pub enum ErrorSeverity {
13    Info,
14    Warning,
15    Error,
16    Fatal,
17}
18
19impl fmt::Display for ErrorSeverity {
20    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
21        match self {
22            ErrorSeverity::Info => write!(f, "INFO"),
23            ErrorSeverity::Warning => write!(f, "WARNING"),
24            ErrorSeverity::Error => write!(f, "ERROR"),
25            ErrorSeverity::Fatal => write!(f, "FATAL"),
26        }
27    }
28}
29
30/// A single error entry.
31#[derive(Debug, Clone)]
32#[allow(dead_code)]
33pub struct ErrorEntry {
34    pub severity: ErrorSeverity,
35    pub category: String,
36    pub message: String,
37    pub code: u32,
38}
39
40/// Bounded error log.
41#[derive(Debug)]
42#[allow(dead_code)]
43pub struct ErrorLog {
44    entries: Vec<ErrorEntry>,
45    capacity: usize,
46    total_pushed: u64,
47}
48
49/// Create a new ErrorLog with given capacity.
50#[allow(dead_code)]
51pub fn new_error_log(capacity: usize) -> ErrorLog {
52    ErrorLog {
53        entries: Vec::new(),
54        capacity,
55        total_pushed: 0,
56    }
57}
58
59/// Push an entry; evicts the oldest if full.
60#[allow(dead_code)]
61pub fn push_error(
62    log: &mut ErrorLog,
63    severity: ErrorSeverity,
64    category: &str,
65    message: &str,
66    code: u32,
67) {
68    if log.entries.len() >= log.capacity && !log.entries.is_empty() {
69        log.entries.remove(0);
70    }
71    log.entries.push(ErrorEntry {
72        severity,
73        category: category.to_string(),
74        message: message.to_string(),
75        code,
76    });
77    log.total_pushed += 1;
78}
79
80/// Count entries at or above a given severity.
81#[allow(dead_code)]
82pub fn count_by_severity(log: &ErrorLog, min: &ErrorSeverity) -> usize {
83    log.entries.iter().filter(|e| &e.severity >= min).count()
84}
85
86/// Return all entries for a category.
87#[allow(dead_code)]
88pub fn entries_for_category<'a>(log: &'a ErrorLog, category: &str) -> Vec<&'a ErrorEntry> {
89    log.entries
90        .iter()
91        .filter(|e| e.category == category)
92        .collect()
93}
94
95/// Clear all entries.
96#[allow(dead_code)]
97pub fn clear_error_log(log: &mut ErrorLog) {
98    log.entries.clear();
99}
100
101/// Total entries pushed lifetime.
102#[allow(dead_code)]
103pub fn total_pushed(log: &ErrorLog) -> u64 {
104    log.total_pushed
105}
106
107/// Current entry count.
108#[allow(dead_code)]
109pub fn error_entry_count(log: &ErrorLog) -> usize {
110    log.entries.len()
111}
112
113/// Whether any fatal entries exist.
114#[allow(dead_code)]
115pub fn has_fatal(log: &ErrorLog) -> bool {
116    log.entries
117        .iter()
118        .any(|e| e.severity == ErrorSeverity::Fatal)
119}
120
121/// Last entry, if any.
122#[allow(dead_code)]
123pub fn last_error(log: &ErrorLog) -> Option<&ErrorEntry> {
124    log.entries.last()
125}
126
127/// Serialize to JSON string.
128#[allow(dead_code)]
129pub fn error_log_to_json(log: &ErrorLog) -> String {
130    let items: Vec<String> = log
131        .entries
132        .iter()
133        .map(|e| {
134            format!(
135                r#"{{"severity":"{}","category":"{}","message":"{}","code":{}}}"#,
136                e.severity, e.category, e.message, e.code
137            )
138        })
139        .collect();
140    format!("[{}]", items.join(","))
141}
142
143#[cfg(test)]
144mod tests {
145    use super::*;
146
147    #[test]
148    fn test_push_and_count() {
149        let mut log = new_error_log(10);
150        push_error(&mut log, ErrorSeverity::Error, "net", "timeout", 1);
151        push_error(&mut log, ErrorSeverity::Warning, "io", "slow", 2);
152        assert_eq!(error_entry_count(&log), 2);
153    }
154
155    #[test]
156    fn test_capacity_eviction() {
157        let mut log = new_error_log(3);
158        for i in 0..5u32 {
159            push_error(&mut log, ErrorSeverity::Info, "cat", "msg", i);
160        }
161        assert_eq!(error_entry_count(&log), 3);
162        assert_eq!(log.entries[0].code, 2);
163    }
164
165    #[test]
166    fn test_count_by_severity() {
167        let mut log = new_error_log(10);
168        push_error(&mut log, ErrorSeverity::Info, "a", "x", 0);
169        push_error(&mut log, ErrorSeverity::Error, "b", "y", 1);
170        push_error(&mut log, ErrorSeverity::Fatal, "c", "z", 2);
171        assert_eq!(count_by_severity(&log, &ErrorSeverity::Error), 2);
172    }
173
174    #[test]
175    fn test_entries_for_category() {
176        let mut log = new_error_log(10);
177        push_error(&mut log, ErrorSeverity::Error, "net", "a", 1);
178        push_error(&mut log, ErrorSeverity::Error, "io", "b", 2);
179        assert_eq!(entries_for_category(&log, "net").len(), 1);
180    }
181
182    #[test]
183    fn test_has_fatal() {
184        let mut log = new_error_log(10);
185        push_error(&mut log, ErrorSeverity::Warning, "x", "y", 0);
186        assert!(!has_fatal(&log));
187        push_error(&mut log, ErrorSeverity::Fatal, "x", "z", 1);
188        assert!(has_fatal(&log));
189    }
190
191    #[test]
192    fn test_clear() {
193        let mut log = new_error_log(10);
194        push_error(&mut log, ErrorSeverity::Error, "x", "y", 0);
195        clear_error_log(&mut log);
196        assert_eq!(error_entry_count(&log), 0);
197    }
198
199    #[test]
200    fn test_total_pushed() {
201        let mut log = new_error_log(2);
202        push_error(&mut log, ErrorSeverity::Info, "a", "b", 0);
203        push_error(&mut log, ErrorSeverity::Info, "a", "b", 1);
204        push_error(&mut log, ErrorSeverity::Info, "a", "b", 2);
205        assert_eq!(total_pushed(&log), 3);
206    }
207
208    #[test]
209    fn test_last_error() {
210        let mut log = new_error_log(10);
211        assert!(last_error(&log).is_none());
212        push_error(&mut log, ErrorSeverity::Error, "cat", "last", 99);
213        assert_eq!(last_error(&log).map(|e| e.code), Some(99));
214    }
215
216    #[test]
217    fn test_json_output() {
218        let mut log = new_error_log(10);
219        push_error(&mut log, ErrorSeverity::Info, "sys", "ok", 0);
220        let j = error_log_to_json(&log);
221        assert!(j.contains("INFO"));
222        assert!(j.contains("sys"));
223    }
224
225    #[test]
226    fn test_severity_ordering() {
227        assert!(ErrorSeverity::Fatal > ErrorSeverity::Error);
228        assert!(ErrorSeverity::Error > ErrorSeverity::Warning);
229        assert!(ErrorSeverity::Warning > ErrorSeverity::Info);
230    }
231}