//! Bounded in-memory ring buffer for server log output. //! //! Use the [`slog!`] macro as a drop-in replacement for `eprintln!`. It writes //! to stderr (same as before) and simultaneously appends the line to the global //! ring buffer, making it retrievable via the `get_server_logs` MCP tool. use std::collections::VecDeque; use std::sync::{Mutex, OnceLock}; const CAPACITY: usize = 1000; pub struct LogBuffer { lines: Mutex>, } impl LogBuffer { fn new() -> Self { Self { lines: Mutex::new(VecDeque::with_capacity(CAPACITY)), } } /// Append a log line, evicting the oldest entry when at capacity. pub fn push(&self, line: String) { if let Ok(mut buf) = self.lines.lock() { if buf.len() >= CAPACITY { buf.pop_front(); } buf.push_back(line); } } /// Return up to `count` recent lines, optionally filtered by a substring. /// Lines are returned in chronological order (oldest first). pub fn get_recent(&self, count: usize, filter: Option<&str>) -> Vec { let buf = match self.lines.lock() { Ok(b) => b, Err(_) => return vec![], }; let filtered: Vec<&String> = buf .iter() .filter(|line| filter.is_none_or(|f| line.contains(f))) .collect(); let start = filtered.len().saturating_sub(count); filtered[start..].iter().map(|s| (*s).clone()).collect() } } static GLOBAL: OnceLock = OnceLock::new(); /// Access the process-wide log ring buffer. pub fn global() -> &'static LogBuffer { GLOBAL.get_or_init(LogBuffer::new) } /// Write a formatted message to stderr **and** capture it in the ring buffer. /// /// Usage is identical to `eprintln!`: /// ```ignore /// slog!("agent {} started", name); /// ``` #[macro_export] macro_rules! slog { ($($arg:tt)*) => {{ let _line = format!($($arg)*); eprintln!("{}", _line); $crate::log_buffer::global().push(_line); }}; } #[cfg(test)] mod tests { use super::*; fn fresh_buffer() -> LogBuffer { LogBuffer::new() } #[test] fn push_and_retrieve() { let buf = fresh_buffer(); buf.push("line one".into()); buf.push("line two".into()); let recent = buf.get_recent(10, None); assert_eq!(recent, vec!["line one", "line two"]); } #[test] fn evicts_oldest_at_capacity() { let buf = LogBuffer { lines: Mutex::new(VecDeque::with_capacity(CAPACITY)), }; // Fill past capacity for i in 0..=CAPACITY { buf.push(format!("line {i}")); } let recent = buf.get_recent(CAPACITY + 1, None); // Should have exactly CAPACITY lines assert_eq!(recent.len(), CAPACITY); // The oldest (line 0) should have been evicted assert!(!recent.iter().any(|l| l == "line 0")); // The newest should be present assert!(recent.iter().any(|l| l == &format!("line {CAPACITY}"))); } #[test] fn filter_by_substring() { let buf = fresh_buffer(); buf.push("watcher started".into()); buf.push("mcp call received".into()); buf.push("watcher event".into()); let filtered = buf.get_recent(100, Some("watcher")); assert_eq!(filtered.len(), 2); assert_eq!(filtered[0], "watcher started"); assert_eq!(filtered[1], "watcher event"); } #[test] fn count_limits_results() { let buf = fresh_buffer(); for i in 0..10 { buf.push(format!("line {i}")); } let recent = buf.get_recent(3, None); assert_eq!(recent.len(), 3); // Most recent 3 assert_eq!(recent, vec!["line 7", "line 8", "line 9"]); } #[test] fn empty_buffer_returns_empty() { let buf = fresh_buffer(); assert!(buf.get_recent(10, None).is_empty()); } }