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