Skip to main content

jigs_trace/
lib.rs

1#![warn(missing_docs)]
2//! Per-jig execution tracing.
3//!
4//! Wires into `#[jig]` when the `trace` feature is enabled on the `jigs`
5//! umbrella crate (or on `jigs-macros` directly). Each instrumented jig
6//! records its metadata, depth, wall-clock duration and outcome into a
7//! thread-local buffer that callers can drain with [`take`].
8
9use std::cell::{Cell, RefCell};
10use std::time::Duration;
11
12pub use jigs_core::JigMeta;
13pub use jigs_core::Status;
14
15/// One recorded jig invocation.
16pub struct Entry {
17    /// Compile-time metadata for the jig that ran.
18    pub meta: &'static JigMeta,
19    /// Nesting depth at the time of entry (top-level jigs are depth 0).
20    pub depth: usize,
21    /// Wall-clock time spent inside the jig.
22    pub duration: Duration,
23    /// `true` if the jig produced a successful outcome.
24    pub ok: bool,
25    /// Error message captured from the jig's output, if any.
26    pub error: Option<String>,
27}
28
29impl Entry {
30    /// Function name of the jig.
31    pub fn name(&self) -> &'static str {
32        self.meta.name
33    }
34}
35
36thread_local! {
37    static DEPTH: Cell<usize> = const { Cell::new(0) };
38    static BUFFER: RefCell<Vec<Entry>> = const { RefCell::new(Vec::new()) };
39}
40
41/// Record the start of a jig invocation. Returns an index used by [`exit`]
42/// to close the same entry. Called by code generated from `#[jig]`.
43pub fn enter(meta: &'static JigMeta) -> usize {
44    let depth = DEPTH.with(|d| {
45        let v = d.get();
46        d.set(v + 1);
47        v
48    });
49    BUFFER.with(|b| {
50        let mut buf = b.borrow_mut();
51        let idx = buf.len();
52        buf.push(Entry {
53            meta,
54            depth,
55            duration: Duration::ZERO,
56            ok: true,
57            error: None,
58        });
59        idx
60    })
61}
62
63/// Close the entry at `idx` with its measured duration and outcome.
64/// Called by code generated from `#[jig]`.
65pub fn exit(idx: usize, duration: Duration, ok: bool, error: Option<String>) {
66    DEPTH.with(|d| d.set(d.get().saturating_sub(1)));
67    BUFFER.with(|b| {
68        let mut buf = b.borrow_mut();
69        buf[idx].duration = duration;
70        buf[idx].ok = ok;
71        buf[idx].error = error;
72    });
73}
74
75/// Drain the current thread's trace buffer and reset depth tracking.
76/// Call once per request after the pipeline finishes.
77pub fn take() -> Vec<Entry> {
78    DEPTH.with(|d| d.set(0));
79    BUFFER.with(|b| std::mem::take(&mut *b.borrow_mut()))
80}
81
82#[cfg(test)]
83mod tests {
84    use super::*;
85
86    fn meta(name: &'static str) -> &'static JigMeta {
87        Box::leak(Box::new(JigMeta {
88            name,
89            file: "",
90            line: 0,
91            kind: "Response",
92            input: "Request",
93            input_type: "",
94            output_type: "",
95            is_async: false,
96            module: "",
97            chain: &[],
98        }))
99    }
100
101    #[test]
102    fn enter_exit_records_one_entry() {
103        let m = meta("step");
104        let idx = enter(m);
105        exit(idx, Duration::from_micros(42), true, None);
106        let entries = take();
107        assert_eq!(entries.len(), 1);
108        assert_eq!(entries[0].name(), "step");
109        assert_eq!(entries[0].depth, 0);
110        assert!(entries[0].ok);
111        assert_eq!(entries[0].duration, Duration::from_micros(42));
112        assert!(entries[0].error.is_none());
113    }
114
115    #[test]
116    fn depth_increases_for_nested_calls() {
117        let outer = meta("outer");
118        let inner = meta("inner");
119        let i0 = enter(outer);
120        let i1 = enter(inner);
121        exit(i1, Duration::ZERO, true, None);
122        exit(i0, Duration::ZERO, true, None);
123        let entries = take();
124        assert_eq!(entries[0].depth, 0);
125        assert_eq!(entries[1].depth, 1);
126    }
127
128    #[test]
129    fn error_is_recorded() {
130        let m = meta("fail");
131        let idx = enter(m);
132        exit(idx, Duration::ZERO, false, Some("boom".into()));
133        let entries = take();
134        assert!(!entries[0].ok);
135        assert_eq!(entries[0].error.as_deref(), Some("boom"));
136    }
137
138    #[test]
139    fn take_drains_buffer() {
140        let m = meta("x");
141        let idx = enter(m);
142        exit(idx, Duration::ZERO, true, None);
143        let first = take();
144        let second = take();
145        assert_eq!(first.len(), 1);
146        assert!(second.is_empty());
147    }
148
149    #[test]
150    fn take_resets_depth() {
151        let m = meta("a");
152        let idx = enter(m);
153        exit(idx, Duration::ZERO, true, None);
154        let _ = take();
155        let idx2 = enter(m);
156        exit(idx2, Duration::ZERO, true, None);
157        let entries = take();
158        assert_eq!(entries[0].depth, 0);
159    }
160
161    #[test]
162    fn entry_exposes_full_meta() {
163        let m = meta("with_meta");
164        let idx = enter(m);
165        exit(idx, Duration::ZERO, true, None);
166        let entries = take();
167        assert!(std::ptr::eq(entries[0].meta, m));
168    }
169}