use std::collections::VecDeque;
use std::fmt::Write as _;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, Mutex, MutexGuard};
use std::time::{SystemTime, UNIX_EPOCH};
use tracing::field::{Field, Visit};
use tracing::{Event, Level, Subscriber};
use tracing_subscriber::layer::{Context, Layer};
pub(crate) const LOG_CAPACITY: usize = 1_000;
const MAX_LINE_CHARS: usize = 400;
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct LogLine {
pub(crate) time: String,
pub(crate) level: Level,
pub(crate) text: String,
}
struct Ring {
lines: VecDeque<LogLine>,
capacity: usize,
}
#[derive(Clone)]
pub(crate) struct LogRing {
ring: Arc<Mutex<Ring>>,
generation: Arc<AtomicU64>,
}
impl LogRing {
pub(crate) fn new(capacity: usize) -> Self {
Self {
ring: Arc::new(Mutex::new(Ring {
lines: VecDeque::with_capacity(capacity.min(64)),
capacity: capacity.max(1),
})),
generation: Arc::new(AtomicU64::new(0)),
}
}
fn lock(&self) -> MutexGuard<'_, Ring> {
self.ring
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
}
pub(crate) fn push(&self, line: LogLine) {
{
let mut ring = self.lock();
if ring.lines.len() == ring.capacity {
ring.lines.pop_front();
}
ring.lines.push_back(line);
}
self.generation.fetch_add(1, Ordering::Release);
}
pub(crate) fn info(&self, text: impl Into<String>) {
self.push(LogLine {
time: utc_hms(SystemTime::now()),
level: Level::INFO,
text: sanitize(&text.into()),
});
}
pub(crate) fn generation(&self) -> u64 {
self.generation.load(Ordering::Acquire)
}
#[cfg(test)]
pub(crate) fn len(&self) -> usize {
self.lock().lines.len()
}
pub(crate) fn tail(&self, n: usize) -> Vec<LogLine> {
let ring = self.lock();
let skip = ring.lines.len().saturating_sub(n);
ring.lines.iter().skip(skip).cloned().collect()
}
}
pub(crate) struct RingLayer {
ring: LogRing,
}
impl RingLayer {
pub(crate) fn new(ring: LogRing) -> Self {
Self { ring }
}
}
impl<S: Subscriber> Layer<S> for RingLayer {
fn on_event(&self, event: &Event<'_>, _ctx: Context<'_, S>) {
let mut visitor = LineVisitor::default();
event.record(&mut visitor);
let mut text = visitor.message;
if !visitor.fields.is_empty() {
if !text.is_empty() {
text.push(' ');
}
text.push_str(&visitor.fields);
}
self.ring.push(LogLine {
time: utc_hms(SystemTime::now()),
level: *event.metadata().level(),
text: sanitize(&text),
});
}
}
#[derive(Default)]
struct LineVisitor {
message: String,
fields: String,
}
impl Visit for LineVisitor {
fn record_str(&mut self, field: &Field, value: &str) {
if field.name() == "message" {
self.message.push_str(value);
} else {
self.push_field(field, format_args!("{value}"));
}
}
fn record_debug(&mut self, field: &Field, value: &dyn std::fmt::Debug) {
if field.name() == "message" {
let _ = write!(self.message, "{value:?}");
} else {
self.push_field(field, format_args!("{value:?}"));
}
}
}
impl LineVisitor {
fn push_field(&mut self, field: &Field, value: std::fmt::Arguments<'_>) {
if !self.fields.is_empty() {
self.fields.push(' ');
}
let _ = write!(self.fields, "{}={value}", field.name());
}
}
fn sanitize(text: &str) -> String {
let mut out = String::with_capacity(text.len().min(MAX_LINE_CHARS));
for (count, ch) in text.chars().enumerate() {
if count == MAX_LINE_CHARS {
out.push('…');
break;
}
out.push(if ch.is_control() { ' ' } else { ch });
}
out
}
fn utc_hms(now: SystemTime) -> String {
let secs = now
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
let day = secs % 86_400;
format!("{:02}:{:02}:{:02}", day / 3600, day % 3600 / 60, day % 60)
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::Duration;
use tracing_subscriber::layer::SubscriberExt;
#[test]
fn ring_is_bounded_and_keeps_the_newest_lines() {
let ring = LogRing::new(3);
let before = ring.generation();
for i in 0..5 {
ring.info(format!("line {i}"));
}
assert_eq!(ring.len(), 3);
assert_eq!(ring.generation(), before + 5);
let texts: Vec<_> = ring.tail(10).into_iter().map(|l| l.text).collect();
assert_eq!(texts, ["line 2", "line 3", "line 4"]);
assert_eq!(ring.tail(1)[0].text, "line 4");
}
#[test]
fn layer_records_message_fields_and_level_on_one_line() {
let ring = LogRing::new(10);
let subscriber = tracing_subscriber::registry().with(RingLayer::new(ring.clone()));
tracing::subscriber::with_default(subscriber, || {
tracing::warn!(device = 101, "late reply\nfrom peer");
});
let lines = ring.tail(10);
assert_eq!(lines.len(), 1);
assert_eq!(lines[0].level, Level::WARN);
assert_eq!(lines[0].text, "late reply from peer device=101");
}
#[test]
fn long_lines_are_truncated_and_times_are_utc() {
let long = "x".repeat(MAX_LINE_CHARS + 50);
assert_eq!(sanitize(&long).chars().count(), MAX_LINE_CHARS + 1);
let t = UNIX_EPOCH + Duration::from_secs(86_400 * 3 + 3_723);
assert_eq!(utc_hms(t), "01:02:03");
}
}