⎇ c22d2480b93b7802be8e8ea5a0075b8175c4fedc
crates/modules/shipote/shipote-core/src/logbuf.rs
Ñawpa willay
Pi imata llamkarqan
ch'uya
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}