use std::collections::BTreeMap;
use std::sync::{Arc, Mutex};
use tracing::Level;
use tracing::field::{Field, Visit};
use tracing_subscriber::Layer;
use tracing_subscriber::layer::Context;
#[derive(Clone, Debug)]
pub struct CapturedEvent {
pub level: Level,
pub target: String,
pub message: String,
pub fields: BTreeMap<String, String>,
}
#[derive(Default)]
struct FieldVisitor {
message: String,
fields: BTreeMap<String, String>,
}
impl Visit for FieldVisitor {
fn record_str(&mut self, field: &Field, value: &str) {
self.store(field.name(), value.to_owned());
}
fn record_debug(&mut self, field: &Field, value: &dyn std::fmt::Debug) {
self.store(field.name(), format!("{value:?}"));
}
fn record_u64(&mut self, field: &Field, value: u64) {
self.store(field.name(), value.to_string());
}
fn record_i64(&mut self, field: &Field, value: i64) {
self.store(field.name(), value.to_string());
}
}
impl FieldVisitor {
fn store(&mut self, name: &str, value: String) {
if name == "message" {
self.message = value;
} else {
self.fields.insert(name.to_owned(), value);
}
}
}
pub struct CaptureLayer {
pub events: Arc<Mutex<Vec<CapturedEvent>>>,
}
impl<S: tracing::Subscriber> Layer<S> for CaptureLayer {
fn on_event(&self, event: &tracing::Event<'_>, _context: Context<'_, S>) {
let mut visitor = FieldVisitor::default();
event.record(&mut visitor);
let captured = CapturedEvent {
level: *event.metadata().level(),
target: event.metadata().target().to_owned(),
message: visitor.message,
fields: visitor.fields,
};
if let Ok(mut events) = self.events.lock() {
events.push(captured);
}
}
}
pub fn capture_log() -> Arc<Mutex<Vec<CapturedEvent>>> {
Arc::new(Mutex::new(Vec::new()))
}
pub fn render(events: &[CapturedEvent]) -> String {
if events.is_empty() {
return "<NO EVENTS CAPTURED AT ALL — the subscriber never received \
anything, so this proves nothing about the subject>"
.to_owned();
}
let (subject, other): (Vec<_>, Vec<_>) = events
.iter()
.partition(|event| event.target.starts_with("aion"));
let mut lines = vec![format!(
"{} events captured in total ({} from aion targets, {} from elsewhere \
— the subscriber was demonstrably live)",
events.len(),
subject.len(),
other.len()
)];
if subject.is_empty() {
lines.push("NO aion-target events at all.".to_owned());
}
lines.extend(subject.iter().map(|event| {
format!(
"[{}] {}: {} {:?}",
event.level, event.target, event.message, event.fields
)
}));
let mut counts: BTreeMap<&str, usize> = BTreeMap::new();
for event in &other {
*counts.entry(event.target.as_str()).or_default() += 1;
}
lines.extend(
counts
.into_iter()
.map(|(target, count)| format!("({count}× {target}, not shown)")),
);
format!("\n {}", lines.join("\n "))
}