Skip to main content

oxihuman_core/
ring_log.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5//! Fixed-capacity ring-buffer log.
6
7/// Log level for ring log entries.
8#[derive(Debug, Clone, PartialEq, Eq)]
9pub enum RingLogLevel {
10    Trace,
11    Debug,
12    Info,
13    Warn,
14    Error,
15}
16
17/// A single log entry.
18#[derive(Debug, Clone)]
19pub struct RingLogEntry {
20    pub level: RingLogLevel,
21    pub message: String,
22    pub seq: u64,
23}
24
25/// Fixed-size ring-buffer log.
26pub struct RingLog {
27    buf: Vec<Option<RingLogEntry>>,
28    capacity: usize,
29    head: usize,
30    count: usize,
31    seq: u64,
32    error_count: usize,
33    warn_count: usize,
34}
35
36#[allow(dead_code)]
37impl RingLog {
38    pub fn new(capacity: usize) -> Self {
39        let cap = capacity.max(1);
40        RingLog {
41            buf: (0..cap).map(|_| None).collect(),
42            capacity: cap,
43            head: 0,
44            count: 0,
45            seq: 0,
46            error_count: 0,
47            warn_count: 0,
48        }
49    }
50
51    pub fn push(&mut self, level: RingLogLevel, message: &str) {
52        match level {
53            RingLogLevel::Error => self.error_count += 1,
54            RingLogLevel::Warn => self.warn_count += 1,
55            _ => {}
56        }
57        let entry = RingLogEntry {
58            level,
59            message: message.to_string(),
60            seq: self.seq,
61        };
62        self.seq += 1;
63        let slot = self.head % self.capacity;
64        self.buf[slot] = Some(entry);
65        self.head += 1;
66        if self.count < self.capacity {
67            self.count += 1;
68        }
69    }
70
71    pub fn len(&self) -> usize {
72        self.count
73    }
74
75    pub fn is_empty(&self) -> bool {
76        self.count == 0
77    }
78
79    pub fn capacity(&self) -> usize {
80        self.capacity
81    }
82
83    pub fn error_count(&self) -> usize {
84        self.error_count
85    }
86
87    pub fn warn_count(&self) -> usize {
88        self.warn_count
89    }
90
91    pub fn entries(&self) -> Vec<&RingLogEntry> {
92        let start = if self.count < self.capacity {
93            0
94        } else {
95            self.head % self.capacity
96        };
97        let mut result = Vec::with_capacity(self.count);
98        for i in 0..self.count {
99            let idx = (start + i) % self.capacity;
100            if let Some(e) = &self.buf[idx] {
101                result.push(e);
102            }
103        }
104        result
105    }
106
107    pub fn last(&self) -> Option<&RingLogEntry> {
108        if self.count == 0 {
109            return None;
110        }
111        let idx = (self.head + self.capacity - 1) % self.capacity;
112        self.buf[idx].as_ref()
113    }
114
115    pub fn by_level(&self, level: &RingLogLevel) -> Vec<&RingLogEntry> {
116        self.entries()
117            .into_iter()
118            .filter(|e| &e.level == level)
119            .collect()
120    }
121
122    pub fn clear(&mut self) {
123        for slot in &mut self.buf {
124            *slot = None;
125        }
126        self.head = 0;
127        self.count = 0;
128        self.error_count = 0;
129        self.warn_count = 0;
130    }
131
132    pub fn total_written(&self) -> u64 {
133        self.seq
134    }
135}
136
137pub fn new_ring_log(capacity: usize) -> RingLog {
138    RingLog::new(capacity)
139}
140
141#[cfg(test)]
142mod tests {
143    use super::*;
144
145    #[test]
146    fn push_and_len() {
147        let mut log = new_ring_log(10);
148        log.push(RingLogLevel::Info, "hello");
149        assert_eq!(log.len(), 1);
150    }
151
152    #[test]
153    fn wrap_around() {
154        let mut log = new_ring_log(3);
155        log.push(RingLogLevel::Info, "a");
156        log.push(RingLogLevel::Info, "b");
157        log.push(RingLogLevel::Info, "c");
158        log.push(RingLogLevel::Info, "d");
159        assert_eq!(log.len(), 3);
160        let entries = log.entries();
161        assert_eq!(entries.last().expect("should succeed").message, "d");
162    }
163
164    #[test]
165    fn error_count_tracked() {
166        let mut log = new_ring_log(10);
167        log.push(RingLogLevel::Error, "oops");
168        log.push(RingLogLevel::Warn, "meh");
169        assert_eq!(log.error_count(), 1);
170        assert_eq!(log.warn_count(), 1);
171    }
172
173    #[test]
174    fn last_entry() {
175        let mut log = new_ring_log(5);
176        log.push(RingLogLevel::Info, "first");
177        log.push(RingLogLevel::Debug, "last");
178        assert_eq!(log.last().expect("should succeed").message, "last");
179    }
180
181    #[test]
182    fn by_level_filter() {
183        let mut log = new_ring_log(10);
184        log.push(RingLogLevel::Info, "i");
185        log.push(RingLogLevel::Error, "e");
186        log.push(RingLogLevel::Info, "i2");
187        let errors = log.by_level(&RingLogLevel::Info);
188        assert_eq!(errors.len(), 2);
189    }
190
191    #[test]
192    fn clear_resets() {
193        let mut log = new_ring_log(5);
194        log.push(RingLogLevel::Error, "e");
195        log.clear();
196        assert!(log.is_empty());
197        assert_eq!(log.error_count(), 0);
198    }
199
200    #[test]
201    fn total_written_increases() {
202        let mut log = new_ring_log(2);
203        log.push(RingLogLevel::Info, "a");
204        log.push(RingLogLevel::Info, "b");
205        log.push(RingLogLevel::Info, "c");
206        assert_eq!(log.total_written(), 3);
207    }
208
209    #[test]
210    fn capacity_respected() {
211        let log = new_ring_log(8);
212        assert_eq!(log.capacity(), 8);
213    }
214
215    #[test]
216    fn seq_numbers_increment() {
217        let mut log = new_ring_log(10);
218        log.push(RingLogLevel::Info, "a");
219        log.push(RingLogLevel::Info, "b");
220        let entries = log.entries();
221        assert_eq!(entries[0].seq, 0);
222        assert_eq!(entries[1].seq, 1);
223    }
224}