tawasuyu

tawasuyu/brahman

Rust 212TOML 64JSON 13HTML 12666 Sutichaykuna

⎇ c22d2480b93b7802be8e8ea5a0075b8175c4fedc crates/modules/shipote/shipote-core/src/logbuf.rs Rust · 122 siq'ikuna · 3.4 KB Ñawpa willay Pi imata llamkarqan ch'uya

shell c22d2480b · sergio ·

1//! Ring buffer en memoria para capturar stdout/stderr de comandos.
2//!
3//! Tamaño fijo por comando (config: `MAX_LOG_BYTES`). Cuando se llena,
4//! descarta los bytes más viejos. Pensado para diagnostico rápido, no
5//! para retención histórica — eso es trabajo de un journald-like aparte.
6
7use std::sync::{Arc, Mutex};
8
9/// Bytes máximos retenidos por comando. 64 KiB cubre logs típicos sin
10/// abusar de memoria si el daemon tiene cientos de comandos vivos.
11pub const MAX_LOG_BYTES: usize = 64 * 1024;
12
13#[derive(Debug, Clone)]
14pub struct LogBuf {
15    inner: Arc<Mutex<Inner>>,
16}
17
18#[derive(Debug)]
19struct Inner {
20    /// Bytes raw. Cuando se acerca al cap, descartamos head para mantener
21    /// el tail.
22    buf: Vec<u8>,
23    cap: usize,
24    /// Total escrito alguna vez (no decrementado al recortar).
25    written_total: u64,
26}
27
28impl LogBuf {
29    pub fn new() -> Self {
30        Self::with_cap(MAX_LOG_BYTES)
31    }
32
33    pub fn with_cap(cap: usize) -> Self {
34        Self {
35            inner: Arc::new(Mutex::new(Inner {
36                buf: Vec::with_capacity(cap.min(4096)),
37                cap,
38                written_total: 0,
39            })),
40        }
41    }
42
43    pub fn append(&self, data: &[u8]) {
44        let Ok(mut g) = self.inner.lock() else { return };
45        g.written_total += data.len() as u64;
46        g.buf.extend_from_slice(data);
47        // Recorte cuando excede cap (con un pequeño slack para evitar
48        // shift en cada append). El usuario ve sólo el tail.
49        if g.buf.len() > g.cap + 1024 {
50            let drop = g.buf.len() - g.cap;
51            g.buf.drain(..drop);
52        }
53    }
54
55    /// Devuelve el tail de hasta `n` bytes (o todo si `n=0`).
56    pub fn tail(&self, n: usize) -> Vec<u8> {
57        let g = match self.inner.lock() {
58            Ok(g) => g,
59            Err(_) => return Vec::new(),
60        };
61        if n == 0 || n >= g.buf.len() {
62            return g.buf.clone();
63        }
64        g.buf[g.buf.len() - n..].to_vec()
65    }
66
67    /// Cuántos bytes hay actualmente en el buffer.
68    pub fn len(&self) -> usize {
69        self.inner.lock().map(|g| g.buf.len()).unwrap_or(0)
70    }
71
72    pub fn is_empty(&self) -> bool {
73        self.len() == 0
74    }
75
76    pub fn written_total(&self) -> u64 {
77        self.inner.lock().map(|g| g.written_total).unwrap_or(0)
78    }
79}
80
81impl Default for LogBuf {
82    fn default() -> Self {
83        Self::new()
84    }
85}
86
87#[cfg(test)]
88mod tests {
89    use super::*;
90
91    #[test]
92    fn append_and_tail_basic() {
93        let lb = LogBuf::with_cap(100);
94        lb.append(b"hello ");
95        lb.append(b"world\n");
96        let t = lb.tail(0);
97        assert_eq!(t, b"hello world\n");
98    }
99
100    #[test]
101    fn cap_drops_oldest() {
102        let lb = LogBuf::with_cap(10);
103        lb.append(&[b'a'; 8]);
104        lb.append(&[b'b'; 8]);
105        // Después del recorte, debe quedar ~10 bytes pero el slack
106        // permite hasta 10+1024. Como pasamos slack, no se recorta aún
107        // en este caso (16 bytes < 10+1024). Forzamos un append grande.
108        lb.append(&[b'c'; 2048]);
109        assert!(lb.len() <= 10 + 1024);
110        let t = lb.tail(0);
111        // El tail debe contener 'c's (los más recientes).
112        assert!(t.iter().filter(|&&b| b == b'c').count() > 0);
113    }
114
115    #[test]
116    fn written_total_tracks_all() {
117        let lb = LogBuf::with_cap(10);
118        lb.append(b"abcdef");
119        lb.append(b"ghijkl");
120        assert_eq!(lb.written_total(), 12);
121    }
122}