use serde::{Deserialize, Serialize};
use std::collections::VecDeque;
use std::sync::RwLock;
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct TraceSpan {
pub name: String,
pub kind: String,
pub duration_us: u64,
pub timestamp: u64,
}
pub struct SpanCollector {
spans: RwLock<VecDeque<TraceSpan>>,
capacity: usize,
}
impl SpanCollector {
pub fn new(capacity: usize) -> Self {
Self {
spans: RwLock::new(VecDeque::with_capacity(capacity)),
capacity,
}
}
pub fn record(&self, span: TraceSpan) {
if let Ok(mut lock) = self.spans.write() {
if lock.len() >= self.capacity {
lock.pop_front();
}
lock.push_back(span);
}
}
pub fn snapshot(&self) -> Vec<TraceSpan> {
self.spans
.read()
.map(|lock| lock.iter().cloned().collect())
.unwrap_or_default()
}
}
static GLOBAL_SPAN_COLLECTOR: std::sync::OnceLock<SpanCollector> = std::sync::OnceLock::new();
pub fn global_span_collector() -> &'static SpanCollector {
GLOBAL_SPAN_COLLECTOR.get_or_init(|| SpanCollector::new(500))
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
#[test]
fn test_span_collector_capacity_and_eviction() {
let collector = SpanCollector::new(3);
assert_eq!(collector.snapshot().len(), 0);
for i in 1..=5 {
collector.record(TraceSpan {
name: format!("operation_{}", i),
kind: "sql".to_string(),
duration_us: i * 100,
timestamp: 1700000000 + i,
});
}
let snapshot = collector.snapshot();
assert_eq!(snapshot.len(), 3);
assert_eq!(snapshot[0].name, "operation_3");
assert_eq!(snapshot[1].name, "operation_4");
assert_eq!(snapshot[2].name, "operation_5");
}
#[test]
fn test_global_span_collector_and_serde() {
let global = global_span_collector();
global.record(TraceSpan {
name: "GET /api/v1/users".to_string(),
kind: "http".to_string(),
duration_us: 1500,
timestamp: 1700000000,
});
let snapshot = global.snapshot();
assert!(!snapshot.is_empty());
let span = &snapshot[snapshot.len() - 1];
let json = serde_json::to_string(span).unwrap();
assert!(json.contains("GET /api/v1/users"));
let deserialized: TraceSpan = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.name, span.name);
assert_eq!(deserialized.kind, span.kind);
assert_eq!(deserialized.duration_us, span.duration_us);
}
}