use std::collections::VecDeque;
use std::fmt::Write as _;
use std::sync::{Arc, Mutex};
use tracing::field::{Field, Visit};
use tracing_subscriber::Layer;
use tracing_subscriber::layer::Context;
pub const DEFAULT_LOG_CAPACITY: usize = 1000;
#[derive(Clone, Debug)]
pub struct LogBuffer {
inner: Arc<Mutex<VecDeque<String>>>,
capacity: usize,
}
impl LogBuffer {
#[must_use]
pub fn new(capacity: usize) -> Self {
let capacity = capacity.max(1);
Self {
inner: Arc::new(Mutex::new(VecDeque::with_capacity(capacity))),
capacity,
}
}
pub fn push(&self, line: String) {
let mut guard = match self.inner.lock() {
Ok(g) => g,
Err(poisoned) => poisoned.into_inner(),
};
guard.push_back(line);
while guard.len() > self.capacity {
guard.pop_front();
}
}
#[must_use]
pub fn tail(&self, n: usize) -> Vec<String> {
let guard = match self.inner.lock() {
Ok(g) => g,
Err(poisoned) => poisoned.into_inner(),
};
let skip = guard.len().saturating_sub(n);
guard.iter().skip(skip).cloned().collect()
}
#[must_use]
pub fn len(&self) -> usize {
match self.inner.lock() {
Ok(g) => g.len(),
Err(poisoned) => poisoned.into_inner().len(),
}
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.len() == 0
}
}
pub struct LogBufferLayer {
buffer: LogBuffer,
}
impl LogBufferLayer {
#[must_use]
pub fn new(buffer: LogBuffer) -> Self {
Self { buffer }
}
}
struct LineVisitor {
message: String,
fields: String,
}
impl LineVisitor {
fn new() -> Self {
Self {
message: String::new(),
fields: String::new(),
}
}
}
impl Visit for LineVisitor {
fn record_debug(&mut self, field: &Field, value: &dyn std::fmt::Debug) {
if field.name() == "message" {
let _ = write!(self.message, "{value:?}");
} else {
let _ = write!(self.fields, " {}={value:?}", field.name());
}
}
fn record_str(&mut self, field: &Field, value: &str) {
if field.name() == "message" {
self.message.push_str(value);
} else {
let _ = write!(self.fields, " {}={value}", field.name());
}
}
}
impl<S: tracing::Subscriber> Layer<S> for LogBufferLayer {
fn on_event(&self, event: &tracing::Event<'_>, _ctx: Context<'_, S>) {
let meta = event.metadata();
let mut visitor = LineVisitor::new();
event.record(&mut visitor);
let message = visitor.message.trim_matches('"');
let line = format!(
"[{} {}] {}{}",
meta.level(),
meta.target(),
message,
visitor.fields
);
self.buffer.push(line);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn capacity_evicts_oldest() {
let buf = LogBuffer::new(3);
assert!(buf.is_empty());
for i in 0..5 {
buf.push(format!("line {i}"));
}
assert_eq!(buf.len(), 3);
assert_eq!(buf.tail(10), vec!["line 2", "line 3", "line 4"]);
}
#[test]
fn tail_returns_last_n() {
let buf = LogBuffer::new(100);
for i in 0..10 {
buf.push(format!("l{i}"));
}
assert_eq!(buf.len(), 10);
assert_eq!(buf.tail(3), vec!["l7", "l8", "l9"]);
}
#[test]
fn tail_all_when_n_exceeds_len() {
let buf = LogBuffer::new(100);
buf.push("only".to_string());
assert_eq!(buf.tail(50), vec!["only"]);
assert_eq!(buf.tail(0), Vec::<String>::new());
}
#[test]
fn zero_capacity_treated_as_one() {
let buf = LogBuffer::new(0);
buf.push("a".to_string());
buf.push("b".to_string());
assert_eq!(buf.tail(10), vec!["b"]);
}
#[test]
fn layer_captures_events() {
use tracing_subscriber::layer::SubscriberExt;
let buffer = LogBuffer::new(10);
let subscriber = tracing_subscriber::registry().with(LogBufferLayer::new(buffer.clone()));
tracing::subscriber::with_default(subscriber, || {
tracing::info!(answer = 42, "hello from test");
});
let lines = buffer.tail(10);
assert_eq!(lines.len(), 1, "expected one captured line, got {lines:?}");
let line = &lines[0];
assert!(line.contains("hello from test"), "line was: {line}");
assert!(line.contains("answer=42"), "line was: {line}");
assert!(line.contains("INFO"), "line was: {line}");
}
}