Skip to main content

oxihuman_export/
event_log_export.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5//! Structured event log (timestamp + payload) export.
6
7/// An event log entry.
8#[allow(dead_code)]
9#[derive(Debug, Clone)]
10pub struct EventEntry {
11    pub timestamp_ms: u64,
12    pub event_type: String,
13    pub severity: EventSeverity,
14    pub payload: String,
15}
16
17/// Severity level.
18#[allow(dead_code)]
19#[derive(Debug, Clone, PartialEq)]
20pub enum EventSeverity {
21    Debug,
22    Info,
23    Warning,
24    Error,
25}
26
27impl EventSeverity {
28    pub fn as_str(&self) -> &str {
29        match self {
30            EventSeverity::Debug => "DEBUG",
31            EventSeverity::Info => "INFO",
32            EventSeverity::Warning => "WARNING",
33            EventSeverity::Error => "ERROR",
34        }
35    }
36}
37
38/// A structured event log.
39#[allow(dead_code)]
40pub struct EventLog {
41    pub entries: Vec<EventEntry>,
42}
43
44impl EventLog {
45    #[allow(dead_code)]
46    pub fn new() -> Self {
47        Self {
48            entries: Vec::new(),
49        }
50    }
51}
52
53impl Default for EventLog {
54    fn default() -> Self {
55        Self::new()
56    }
57}
58
59/// Add an event entry.
60#[allow(dead_code)]
61pub fn log_event(
62    log: &mut EventLog,
63    timestamp_ms: u64,
64    event_type: &str,
65    severity: EventSeverity,
66    payload: &str,
67) {
68    log.entries.push(EventEntry {
69        timestamp_ms,
70        event_type: event_type.to_string(),
71        severity,
72        payload: payload.to_string(),
73    });
74}
75
76/// Export to structured CSV.
77#[allow(dead_code)]
78pub fn export_event_log_csv(log: &EventLog) -> String {
79    let mut out = String::from("timestamp_ms,event_type,severity,payload\n");
80    for e in &log.entries {
81        out.push_str(&format!(
82            "{},{},{},{}\n",
83            e.timestamp_ms,
84            e.event_type,
85            e.severity.as_str(),
86            e.payload
87        ));
88    }
89    out
90}
91
92/// Export to NDJSON (newline-delimited JSON).
93#[allow(dead_code)]
94pub fn export_event_log_ndjson(log: &EventLog) -> String {
95    let mut out = String::new();
96    for e in &log.entries {
97        out.push_str(&format!(
98            "{{\"ts\":{},\"type\":\"{}\",\"severity\":\"{}\",\"payload\":\"{}\"}}\n",
99            e.timestamp_ms,
100            e.event_type,
101            e.severity.as_str(),
102            e.payload
103        ));
104    }
105    out
106}
107
108/// Count events by severity.
109#[allow(dead_code)]
110pub fn count_by_severity(log: &EventLog, severity: &EventSeverity) -> usize {
111    log.entries
112        .iter()
113        .filter(|e| &e.severity == severity)
114        .count()
115}
116
117/// Total event count.
118#[allow(dead_code)]
119pub fn event_count(log: &EventLog) -> usize {
120    log.entries.len()
121}
122
123/// Filter events by type.
124#[allow(dead_code)]
125pub fn filter_by_type<'a>(log: &'a EventLog, event_type: &str) -> Vec<&'a EventEntry> {
126    log.entries
127        .iter()
128        .filter(|e| e.event_type == event_type)
129        .collect()
130}
131
132/// Has errors.
133#[allow(dead_code)]
134pub fn has_errors(log: &EventLog) -> bool {
135    log.entries
136        .iter()
137        .any(|e| e.severity == EventSeverity::Error)
138}
139
140/// Sort events by timestamp.
141#[allow(dead_code)]
142pub fn sort_events_by_time(log: &mut EventLog) {
143    log.entries.sort_by_key(|e| e.timestamp_ms);
144}
145
146#[cfg(test)]
147mod tests {
148    use super::*;
149
150    fn sample_log() -> EventLog {
151        let mut log = EventLog::new();
152        log_event(
153            &mut log,
154            0,
155            "startup",
156            EventSeverity::Info,
157            "system started",
158        );
159        log_event(
160            &mut log,
161            100,
162            "mesh_load",
163            EventSeverity::Info,
164            "mesh loaded OK",
165        );
166        log_event(
167            &mut log,
168            200,
169            "export_fail",
170            EventSeverity::Error,
171            "write error",
172        );
173        log_event(
174            &mut log,
175            300,
176            "debug_dump",
177            EventSeverity::Debug,
178            "vertices=100",
179        );
180        log
181    }
182
183    #[test]
184    fn event_count_correct() {
185        let log = sample_log();
186        assert_eq!(event_count(&log), 4);
187    }
188
189    #[test]
190    fn count_errors_one() {
191        let log = sample_log();
192        assert_eq!(count_by_severity(&log, &EventSeverity::Error), 1);
193    }
194
195    #[test]
196    fn has_errors_true() {
197        let log = sample_log();
198        assert!(has_errors(&log));
199    }
200
201    #[test]
202    fn no_errors_when_clean() {
203        let log = EventLog::new();
204        assert!(!has_errors(&log));
205    }
206
207    #[test]
208    fn csv_header_present() {
209        let log = sample_log();
210        let csv = export_event_log_csv(&log);
211        assert!(csv.starts_with("timestamp_ms,event_type,severity,payload"));
212    }
213
214    #[test]
215    fn ndjson_line_count() {
216        let log = sample_log();
217        let ndjson = export_event_log_ndjson(&log);
218        let lines: Vec<&str> = ndjson.trim().split('\n').collect();
219        assert_eq!(lines.len(), 4);
220    }
221
222    #[test]
223    fn filter_by_type_correct() {
224        let log = sample_log();
225        let entries = filter_by_type(&log, "startup");
226        assert_eq!(entries.len(), 1);
227    }
228
229    #[test]
230    fn sort_events_by_time_ordered() {
231        let mut log = EventLog::new();
232        log_event(&mut log, 300, "c", EventSeverity::Info, "");
233        log_event(&mut log, 100, "a", EventSeverity::Info, "");
234        log_event(&mut log, 200, "b", EventSeverity::Info, "");
235        sort_events_by_time(&mut log);
236        assert_eq!(log.entries[0].timestamp_ms, 100);
237    }
238
239    #[test]
240    fn severity_as_str_correct() {
241        assert_eq!(EventSeverity::Error.as_str(), "ERROR");
242        assert_eq!(EventSeverity::Warning.as_str(), "WARNING");
243    }
244
245    #[test]
246    fn count_info_two() {
247        let log = sample_log();
248        assert_eq!(count_by_severity(&log, &EventSeverity::Info), 2);
249    }
250}