use std::io::Write;
use std::sync::{Mutex, OnceLock};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Phase {
In,
Out,
End,
Event,
}
impl Phase {
fn as_str(self) -> &'static str {
match self {
Phase::In => "in",
Phase::Out => "out",
Phase::End => "end",
Phase::Event => "event",
}
}
}
struct Sink {
inner: Mutex<Inner>,
path: String,
}
struct Inner {
seq: u64,
file: Option<std::fs::File>,
}
static SINK: OnceLock<Sink> = OnceLock::new();
fn sink() -> &'static Sink {
SINK.get_or_init(|| {
let path =
std::env::var("FACETT_TRACE").unwrap_or_else(|_| "/tmp/facett_trace.jsonl".to_string());
let file = std::fs::File::create(&path).ok();
let s = Sink { inner: Mutex::new(Inner { seq: 0, file }), path };
emit_into(&s, "trace.session", Phase::Event, &serde_json::json!({ "file": s.path }));
s
})
}
pub fn emit(span: &str, phase: Phase, data: &serde_json::Value) {
emit_into(sink(), span, phase, data);
}
pub fn emit_in(span: &str, data: &serde_json::Value) {
emit(span, Phase::In, data);
}
pub fn emit_out(span: &str, data: &serde_json::Value) {
emit(span, Phase::Out, data);
}
pub fn emit_end(span: &str, data: &serde_json::Value) {
emit(span, Phase::End, data);
}
pub fn emit_event(span: &str, data: &serde_json::Value) {
emit(span, Phase::Event, data);
}
fn emit_into(s: &Sink, span: &str, phase: Phase, data: &serde_json::Value) {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default();
let ts_ms = now.as_millis() as u64;
let stamp = stamp_from_ms(ts_ms);
let Ok(mut g) = s.inner.lock() else { return };
g.seq += 1;
let line = serde_json::json!({
"seq": g.seq,
"ts_ms": ts_ms,
"stamp": stamp,
"span": span,
"phase": phase.as_str(),
"data": data,
});
if let Some(f) = g.file.as_mut() {
let _ = writeln!(f, "{line}");
let _ = f.flush();
}
}
fn stamp_from_ms(total_ms: u64) -> String {
let ms = total_ms % 1000;
let secs = total_ms / 1000;
let h = (secs / 3600) % 24;
let m = (secs / 60) % 60;
let s = secs % 60;
format!("{h:02}:{m:02}:{s:02}.{ms:03}")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn writes_jsonl_lines_with_real_payload() {
let path = "/tmp/facett_trace_test.jsonl";
unsafe { std::env::set_var("FACETT_TRACE", path) };
emit_in("facet.render", &serde_json::json!({ "title": "graph", "size": [800.0, 600.0] }));
emit_out(
"facet.render",
&serde_json::json!({ "title": "graph", "vertices": 412, "drew": true }),
);
let body = std::fs::read_to_string(path).unwrap();
let lines: Vec<&str> = body.lines().collect();
assert!(lines.len() >= 3, "expected >=3 lines, got {}", lines.len());
for l in &lines {
let v: serde_json::Value = serde_json::from_str(l).unwrap();
assert!(v.get("seq").is_some());
assert!(v.get("span").is_some());
assert!(v.get("phase").is_some());
assert!(v.get("data").is_some());
}
let last: serde_json::Value = serde_json::from_str(lines.last().unwrap()).unwrap();
assert_eq!(last["span"], "facet.render");
assert_eq!(last["phase"], "out");
assert_eq!(last["data"]["vertices"], 412);
}
#[test]
fn stamp_shape_is_hms_millis() {
let s = stamp_from_ms(3_661_123); assert_eq!(s, "01:01:01.123");
}
}