use std::sync::Mutex;
use std::time::{SystemTime, UNIX_EPOCH};
use serde_json::{Value, json};
#[derive(Debug, Clone, serde::Serialize)]
pub struct TraceRecord(pub String, pub Option<String>, pub Value, pub i64);
impl TraceRecord {
pub fn tag(&self) -> &str {
&self.0
}
}
pub const DEFAULT_MAX_BUFFER: usize = 2500;
#[derive(Debug)]
pub struct Tracer {
id: Option<String>,
max_buffer: usize,
buffer: Mutex<Vec<TraceRecord>>,
}
impl Tracer {
pub fn new(id: Option<String>) -> Self {
Self::with_capacity(id, DEFAULT_MAX_BUFFER)
}
pub fn with_capacity(id: Option<String>, max_buffer: usize) -> Self {
Self {
id,
max_buffer: max_buffer.max(1),
buffer: Mutex::new(Vec::new()),
}
}
pub fn id(&self) -> Option<&str> {
self.id.as_deref()
}
pub fn trace(&self, tag: impl Into<String>, data: Value) {
let record = TraceRecord(tag.into(), self.id.clone(), data, now_ms());
let mut buffer = self.buffer.lock().unwrap_or_else(|e| e.into_inner());
buffer.push(record);
cap_buffer(&mut buffer, self.max_buffer, &self.id);
}
pub fn take(&self) -> Vec<TraceRecord> {
let mut buffer = self.buffer.lock().unwrap_or_else(|e| e.into_inner());
std::mem::take(&mut *buffer)
}
pub fn restore(&self, mut records: Vec<TraceRecord>) {
if records.is_empty() {
return;
}
let mut buffer = self.buffer.lock().unwrap_or_else(|e| e.into_inner());
records.append(&mut buffer);
*buffer = records;
cap_buffer(&mut buffer, self.max_buffer, &self.id);
}
pub fn len(&self) -> usize {
self.buffer.lock().unwrap_or_else(|e| e.into_inner()).len()
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
}
fn cap_buffer(buffer: &mut Vec<TraceRecord>, max: usize, id: &Option<String>) {
let overflow = buffer.len().saturating_sub(max);
if overflow == 0 {
return;
}
buffer.drain(0..overflow);
if let Some(front) = buffer.first_mut() {
*front = TraceRecord(
"traceBufferOverflow".to_owned(),
id.clone(),
json!({ "dropped": overflow }),
now_ms(),
);
}
}
pub(crate) fn now_ms() -> i64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_millis() as i64)
.unwrap_or(0)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn trace_records_carry_tag_id_and_data() {
let tracer = Tracer::new(Some("subscriber".to_owned()));
tracer.trace("iceconnectionstatechange", json!("connected"));
let drained = tracer.take();
assert_eq!(drained.len(), 1);
assert_eq!(drained[0].tag(), "iceconnectionstatechange");
assert_eq!(drained[0].1.as_deref(), Some("subscriber"));
assert_eq!(drained[0].2, json!("connected"));
assert!(drained[0].3 > 0, "timestamp should be a real epoch ms");
}
#[test]
fn take_drains_the_buffer() {
let tracer = Tracer::new(None);
tracer.trace("a", Value::Null);
tracer.trace("b", Value::Null);
assert_eq!(tracer.len(), 2);
let first = tracer.take();
assert_eq!(first.len(), 2);
assert!(tracer.is_empty(), "buffer empty after take");
assert!(tracer.take().is_empty(), "second take yields nothing");
}
#[test]
fn serializes_as_a_four_element_array() {
let tracer = Tracer::new(Some("publisher".to_owned()));
tracer.trace("ontrack", json!("audio:abc stream:xyz"));
let drained = tracer.take();
let value = serde_json::to_value(&drained).expect("serialize");
let arr = value.as_array().expect("array of records");
let record = arr[0].as_array().expect("record is an array");
assert_eq!(record.len(), 4);
assert_eq!(record[0], json!("ontrack"));
assert_eq!(record[1], json!("publisher"));
assert_eq!(record[2], json!("audio:abc stream:xyz"));
assert!(record[3].is_i64());
}
#[test]
fn buffer_is_bounded_and_leaves_overflow_breadcrumb() {
let tracer = Tracer::with_capacity(Some("publisher".to_owned()), 4);
for i in 0..10 {
tracer.trace(format!("evt-{i}"), json!(i));
}
let drained = tracer.take();
assert_eq!(drained.len(), 4, "buffer capped at max");
assert_eq!(drained[0].tag(), "traceBufferOverflow");
assert_eq!(drained[0].2, json!({ "dropped": 1 }));
assert_eq!(drained[1].tag(), "evt-7");
assert_eq!(drained[3].tag(), "evt-9");
}
#[test]
fn restore_beyond_capacity_drops_and_breadcrumbs() {
let tracer = Tracer::with_capacity(None, 3);
let restored: Vec<TraceRecord> = (0..5)
.map(|i| TraceRecord(format!("r-{i}"), None, json!(i), 1))
.collect();
tracer.trace("live", json!("x"));
tracer.restore(restored);
let all = tracer.take();
assert_eq!(all.len(), 3);
assert_eq!(all[0].tag(), "traceBufferOverflow");
assert_eq!(all[0].2, json!({ "dropped": 3 }));
}
#[test]
fn restore_re_prepends_a_drained_slice() {
let tracer = Tracer::new(None);
tracer.trace("first", json!(1));
tracer.trace("second", json!(2));
let drained = tracer.take();
tracer.trace("third", json!(3));
tracer.restore(drained);
let all = tracer.take();
let tags: Vec<&str> = all.iter().map(|r| r.tag()).collect();
assert_eq!(tags, vec!["first", "second", "third"]);
}
#[test]
fn restore_of_empty_is_a_noop() {
let tracer = Tracer::new(None);
tracer.trace("x", Value::Null);
tracer.restore(Vec::new());
assert_eq!(tracer.len(), 1);
}
}