Skip to main content

oxihuman_core/
log_aggregator.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5//! Log level aggregator and filter.
6
7/// Log level severity ordering.
8#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
9pub enum AggLogLevel {
10    Trace = 0,
11    Debug = 1,
12    Info = 2,
13    Warn = 3,
14    Error = 4,
15    Fatal = 5,
16}
17
18/// An aggregated log entry.
19#[derive(Debug, Clone)]
20pub struct AggLogEntry {
21    pub level: AggLogLevel,
22    pub message: String,
23    pub source: String,
24}
25
26/// Aggregates log entries with level filtering.
27#[derive(Debug)]
28pub struct LogAggregator {
29    min_level: AggLogLevel,
30    entries: Vec<AggLogEntry>,
31}
32
33impl LogAggregator {
34    pub fn new(min_level: AggLogLevel) -> Self {
35        Self {
36            min_level,
37            entries: Vec::new(),
38        }
39    }
40
41    pub fn push(&mut self, level: AggLogLevel, source: &str, message: &str) {
42        if level >= self.min_level {
43            self.entries.push(AggLogEntry {
44                level,
45                message: message.to_string(),
46                source: source.to_string(),
47            });
48        }
49    }
50
51    pub fn entries(&self) -> &[AggLogEntry] {
52        &self.entries
53    }
54
55    pub fn count(&self) -> usize {
56        self.entries.len()
57    }
58
59    pub fn count_at_level(&self, level: AggLogLevel) -> usize {
60        self.entries.iter().filter(|e| e.level == level).count()
61    }
62
63    pub fn set_min_level(&mut self, level: AggLogLevel) {
64        self.min_level = level;
65    }
66
67    pub fn clear(&mut self) {
68        self.entries.clear();
69    }
70
71    pub fn filter_source<'a>(&'a self, source: &str) -> Vec<&'a AggLogEntry> {
72        self.entries.iter().filter(|e| e.source == source).collect()
73    }
74}
75
76pub fn new_log_aggregator(min_level: AggLogLevel) -> LogAggregator {
77    LogAggregator::new(min_level)
78}
79
80pub fn agg_push(agg: &mut LogAggregator, level: AggLogLevel, source: &str, msg: &str) {
81    agg.push(level, source, msg);
82}
83
84pub fn agg_count(agg: &LogAggregator) -> usize {
85    agg.count()
86}
87
88pub fn agg_count_level(agg: &LogAggregator, level: AggLogLevel) -> usize {
89    agg.count_at_level(level)
90}
91
92pub fn agg_set_min(agg: &mut LogAggregator, level: AggLogLevel) {
93    agg.set_min_level(level);
94}
95
96#[cfg(test)]
97mod tests {
98    use super::*;
99
100    #[test]
101    fn test_push_above_min() {
102        let mut agg = new_log_aggregator(AggLogLevel::Info);
103        agg_push(&mut agg, AggLogLevel::Info, "srv", "started");
104        assert_eq!(agg_count(&agg), 1);
105    }
106
107    #[test]
108    fn test_filter_below_min() {
109        let mut agg = new_log_aggregator(AggLogLevel::Warn);
110        agg_push(&mut agg, AggLogLevel::Debug, "srv", "verbose");
111        assert_eq!(agg_count(&agg), 0);
112    }
113
114    #[test]
115    fn test_count_at_level() {
116        let mut agg = new_log_aggregator(AggLogLevel::Debug);
117        agg_push(&mut agg, AggLogLevel::Error, "src", "boom");
118        agg_push(&mut agg, AggLogLevel::Info, "src", "ok");
119        assert_eq!(agg_count_level(&agg, AggLogLevel::Error), 1);
120    }
121
122    #[test]
123    fn test_clear() {
124        let mut agg = new_log_aggregator(AggLogLevel::Info);
125        agg_push(&mut agg, AggLogLevel::Info, "s", "msg");
126        agg.clear();
127        assert_eq!(agg_count(&agg), 0);
128    }
129
130    #[test]
131    fn test_set_min_level_changes_filtering() {
132        let mut agg = new_log_aggregator(AggLogLevel::Error);
133        agg_set_min(&mut agg, AggLogLevel::Debug);
134        agg_push(&mut agg, AggLogLevel::Debug, "s", "now visible");
135        assert_eq!(agg_count(&agg), 1);
136    }
137
138    #[test]
139    fn test_filter_source() {
140        let mut agg = new_log_aggregator(AggLogLevel::Info);
141        agg_push(&mut agg, AggLogLevel::Info, "auth", "login");
142        agg_push(&mut agg, AggLogLevel::Info, "api", "req");
143        let auth_entries = agg.filter_source("auth");
144        assert_eq!(auth_entries.len(), 1);
145    }
146
147    #[test]
148    fn test_level_ordering() {
149        assert!(AggLogLevel::Error > AggLogLevel::Info);
150    }
151
152    #[test]
153    fn test_fatal_always_captured() {
154        let mut agg = new_log_aggregator(AggLogLevel::Error);
155        agg_push(&mut agg, AggLogLevel::Fatal, "sys", "crashed");
156        assert_eq!(agg_count(&agg), 1);
157    }
158
159    #[test]
160    fn test_multiple_sources() {
161        let mut agg = new_log_aggregator(AggLogLevel::Info);
162        agg_push(&mut agg, AggLogLevel::Info, "a", "msg1");
163        agg_push(&mut agg, AggLogLevel::Info, "b", "msg2");
164        agg_push(&mut agg, AggLogLevel::Info, "a", "msg3");
165        assert_eq!(agg.filter_source("a").len(), 2);
166        assert_eq!(agg.filter_source("b").len(), 1);
167    }
168}