use std::collections::VecDeque;
use std::sync::{Arc, Mutex};
use std::time::{SystemTime, UNIX_EPOCH};
#[derive(Debug, Clone, serde::Serialize)]
pub struct LogEntry {
pub ts: i64,
pub level: String,
pub source: String,
pub message: String,
}
impl LogEntry {
pub fn new(
level: impl Into<String>,
source: impl Into<String>,
message: impl Into<String>,
) -> Self {
let ts = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_millis() as i64;
Self {
ts,
level: level.into(),
source: source.into(),
message: message.into(),
}
}
}
#[derive(Clone, Debug)]
pub struct LogSink(Arc<Mutex<VecDeque<LogEntry>>>);
pub const LOG_SINK_CAP: usize = 20;
impl LogSink {
pub fn new() -> Self {
Self(Arc::new(Mutex::new(VecDeque::with_capacity(
LOG_SINK_CAP + 1,
))))
}
pub fn push(&self, entry: LogEntry) {
if let Ok(mut buf) = self.0.lock() {
buf.push_back(entry);
if buf.len() > LOG_SINK_CAP {
buf.pop_front();
}
}
}
pub fn to_json(&self) -> serde_json::Value {
if let Ok(buf) = self.0.lock() {
let entries: Vec<serde_json::Value> = buf
.iter()
.map(|e| {
serde_json::to_value(e).unwrap_or(serde_json::Value::Null)
})
.collect();
serde_json::Value::Array(entries)
} else {
serde_json::Value::Array(vec![])
}
}
pub fn entries(&self) -> Vec<LogEntry> {
if let Ok(buf) = self.0.lock() {
buf.iter().cloned().collect()
} else {
vec![]
}
}
}
impl Default for LogSink {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn log_sink_push_and_read() {
let sink = LogSink::new();
sink.push(LogEntry::new("info", "engine", "hello"));
sink.push(LogEntry::new("warn", "alc.log", "world"));
let entries = sink.entries();
assert_eq!(entries.len(), 2);
assert_eq!(entries[0].level, "info");
assert_eq!(entries[0].source, "engine");
assert_eq!(entries[0].message, "hello");
assert_eq!(entries[1].level, "warn");
assert_eq!(entries[1].message, "world");
}
#[test]
fn log_sink_cap_evicts_oldest() {
let sink = LogSink::new();
for i in 0..=20u32 {
sink.push(LogEntry::new("info", "engine", format!("msg-{i}")));
}
let entries = sink.entries();
assert_eq!(entries.len(), LOG_SINK_CAP);
assert_eq!(entries[0].message, "msg-1");
assert_eq!(entries[LOG_SINK_CAP - 1].message, "msg-20");
}
#[test]
fn log_sink_empty() {
let sink = LogSink::new();
assert!(sink.entries().is_empty());
let json = sink.to_json();
assert_eq!(json, serde_json::Value::Array(vec![]));
}
#[test]
fn log_sink_to_json_shape() {
let sink = LogSink::new();
sink.push(LogEntry::new("debug", "alc.lua.print", "test-msg"));
let json = sink.to_json();
let arr = json.as_array().unwrap();
assert_eq!(arr.len(), 1);
assert_eq!(arr[0]["level"], "debug");
assert_eq!(arr[0]["source"], "alc.lua.print");
assert_eq!(arr[0]["message"], "test-msg");
assert!(arr[0].get("ts").is_some());
}
#[test]
fn log_sink_clone_shares_buffer() {
let sink = LogSink::new();
let sink2 = sink.clone();
sink.push(LogEntry::new("info", "engine", "shared"));
assert_eq!(sink2.entries().len(), 1);
}
#[test]
fn log_sink_exactly_at_cap() {
let sink = LogSink::new();
for i in 0..20u32 {
sink.push(LogEntry::new("info", "engine", format!("msg-{i}")));
}
let entries = sink.entries();
assert_eq!(entries.len(), 20);
assert_eq!(entries[0].message, "msg-0");
assert_eq!(entries[19].message, "msg-19");
}
}