use std::cell::RefCell;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SpanRecord {
pub operation: String,
pub target: String,
}
thread_local! {
static SPAN_LOG: RefCell<Vec<SpanRecord>> = const { RefCell::new(Vec::new()) };
}
fn record_span(operation: &str, target: &str) {
let record = SpanRecord {
operation: operation.to_string(),
target: target.to_string(),
};
SPAN_LOG.with(|log| {
log.borrow_mut().push(record.clone());
});
if let Ok(sink_path) = std::env::var("AFFI_TRACE_SINK") {
if let Ok(mut file) = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(sink_path)
{
let line = format!("{}|{}\n", record.operation, record.target);
let _ = std::io::Write::write_all(&mut file, line.as_bytes());
}
}
}
pub fn captured_spans() -> Vec<SpanRecord> {
SPAN_LOG.with(|log| log.borrow().clone())
}
pub fn clear_spans() {
SPAN_LOG.with(|log| log.borrow_mut().clear());
}
pub fn trace_emit<F, T>(event_type: &str, _object_count: usize, f: F) -> T
where
F: FnOnce() -> T,
{
record_span("emit", event_type);
f()
}
pub fn trace_assemble<F, T>(event_count: usize, f: F) -> T
where
F: FnOnce() -> T,
{
record_span("assemble", &event_count.to_string());
f()
}
pub fn trace_verify<F, T>(receipt_path: &str, f: F) -> T
where
F: FnOnce() -> T,
{
record_span("verify", receipt_path);
f()
}
pub fn trace_show<F, T>(receipt_path: &str, f: F) -> T
where
F: FnOnce() -> T,
{
record_span("show", receipt_path);
f()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn trace_verify_emits_an_observable_span() {
clear_spans();
let out = trace_verify("receipt.json", || 42);
assert_eq!(out, 42, "trace wrapper must return the inner result");
let spans = captured_spans();
assert!(
spans
.iter()
.any(|s| s.operation == "verify" && s.target == "receipt.json"),
"verify must emit an observable span; got {spans:?}"
);
}
#[test]
fn no_span_recorded_without_a_traced_operation() {
clear_spans();
assert!(captured_spans().is_empty(), "no spans before any traced op");
}
}