Skip to main content

relux_runtime/observe/structured/builder/
lifecycle.rs

1//! Span and shell lifecycle emitters.
2//!
3//! Methods that open/close spans, mutate the spans glossary, and record
4//! shell spawn/terminate timestamps. Also home to the effect-expose
5//! emitters, which announce the rebinding of an effect output into the
6//! consumer's scope.
7
8use relux_core::diagnostics::IrSpan;
9
10use super::SpanGuard;
11use super::StructuredLogBuilder;
12use crate::observe::progress::ProgressEvent;
13use crate::observe::structured::event::EventKind;
14use crate::observe::structured::event::EventSeq;
15use crate::observe::structured::failure::StackFrame;
16use crate::observe::structured::shell::ShellRecord;
17use crate::observe::structured::span::Span;
18use crate::observe::structured::span::SpanId;
19use crate::observe::structured::span::SpanKind;
20
21impl StructuredLogBuilder {
22    // --- Span lifecycle ----------------------------------
23
24    /// Open a span and return a guard that closes it on drop. The caller
25    /// must keep the guard alive for the span's lifetime; passing the id
26    /// (`guard.id()`) to children is fine. Drop on `?` propagation closes
27    /// cleanly; for a tighter `end_ts`, use `SpanGuard::close()` explicitly.
28    pub fn open_span(
29        &self,
30        kind: SpanKind,
31        parent: Option<SpanId>,
32        location: Option<&IrSpan>,
33    ) -> SpanGuard {
34        let location = location.and_then(|s| self.resolve_location(s));
35        let start_ts = self.now();
36        let id = {
37            let mut inner = self.inner.lock().unwrap();
38            let id = inner.next_span_id;
39            inner.next_span_id += 1;
40            inner.spans.insert(
41                id,
42                Span {
43                    id,
44                    kind,
45                    parent,
46                    start_ts,
47                    end_ts: None,
48                    location,
49                },
50            );
51            id
52        };
53        SpanGuard::new(id, self.clone())
54    }
55
56    /// First close wins. The `SpanGuard`'s `Drop` always calls into here
57    /// (so `?` early-returns still get an `end_ts`), but call sites may
58    /// also close a span explicitly via id when its semantic end happens
59    /// well before the guard would naturally drop - e.g., a failing
60    /// effect's setup span needs to close before its `try_guards!`
61    /// awaits `run_effect_cleanup`, or the guard would sit on the stack
62    /// through the entire cleanup phase and end up with a misleading
63    /// `end_ts` near test-end. Making this idempotent lets the guard
64    /// drop later as a no-op.
65    pub(super) fn close_span_inner(&self, id: SpanId) {
66        let end_ts = self.now();
67        let mut inner = self.inner.lock().unwrap();
68        if let Some(span) = inner.spans.get_mut(&id)
69            && span.end_ts.is_none()
70        {
71            span.end_ts = Some(end_ts);
72        }
73    }
74
75    /// Close a span by id. Idempotent - see `close_span_inner`. Used to
76    /// pin a span's `end_ts` to its actual semantic boundary when the
77    /// owning `SpanGuard`'s drop point would otherwise be deferred (the
78    /// failing-effect path through `try_guards!` is the canonical case).
79    pub fn close_span(&self, id: SpanId) {
80        self.close_span_inner(id);
81    }
82
83    /// Attach a return value to an in-flight `FnCall` span. Called from
84    /// `exec_call` on the success path before the span closes; failed calls
85    /// leave `result` as `None` so the row title falls back to `name/arity`.
86    pub fn set_fn_call_result(&self, id: SpanId, result: &str) {
87        let mut inner = self.inner.lock().unwrap();
88        if let Some(span) = inner.spans.get_mut(&id)
89            && let SpanKind::FnCall { result: slot, .. } = &mut span.kind
90        {
91            *slot = Some(result.to_string());
92        }
93    }
94
95    /// Walk parent pointers from `leaf` back to a root span and return the
96    /// frames in root-to-leaf order. Used at failure-construction time to
97    /// snapshot the active call chain.
98    pub fn resolve_stack(&self, leaf: SpanId) -> Vec<StackFrame> {
99        let inner = self.inner.lock().unwrap();
100        let mut chain: Vec<StackFrame> = Vec::new();
101        let mut next = Some(leaf);
102        while let Some(id) = next {
103            let Some(span) = inner.spans.get(&id) else {
104                break;
105            };
106            let (name, args) = span.kind.frame_data();
107            // A pure-fn call renders as `pure-fn-call` so the failure
108            // report distinguishes the compile-time pure chain from an
109            // impure `fn` call; both are `SpanKind::FnCall` spans.
110            let kind = match &span.kind {
111                SpanKind::FnCall { is_pure: true, .. } => "pure-fn-call",
112                other => other.kind_str(),
113            };
114            chain.push(StackFrame {
115                span: id,
116                kind: kind.to_string(),
117                name,
118                args,
119                alias: span.kind.frame_alias(),
120                location: span.location.clone(),
121            });
122            next = span.parent;
123        }
124        chain.reverse();
125        chain
126    }
127
128    /// Open the synthetic `markers` root span. Always opened (per
129    /// design); viewer filters out empty markers roots.
130    pub fn open_markers_span(&self, location: Option<&IrSpan>) -> SpanGuard {
131        self.open_span(SpanKind::Markers, None, location)
132    }
133
134    /// Open a `multi-match` span as a child of `parent`. Carries the
135    /// owning shell's display name; the viewer keys on this span kind
136    /// to apply the observation-vs-drain rule to inner `Matched` buffer
137    /// events.
138    pub fn open_multimatch_span(
139        &self,
140        parent: SpanId,
141        shell: &str,
142        location: Option<&IrSpan>,
143    ) -> SpanGuard {
144        self.open_span(
145            SpanKind::MultiMatch {
146                shell: shell.to_string(),
147            },
148            Some(parent),
149            location,
150        )
151    }
152
153    /// Open a `marker-eval` span as a child of a `markers` root.
154    pub fn open_marker_eval_span(
155        &self,
156        parent: SpanId,
157        marker_kind: super::super::span::MarkerEvalKind,
158        modifier: super::super::span::MarkerEvalModifier,
159        decision: super::super::span::MarkerEvalDecision,
160        location: Option<&IrSpan>,
161    ) -> SpanGuard {
162        self.open_span(
163            SpanKind::MarkerEval {
164                marker_kind,
165                modifier,
166                decision,
167            },
168            Some(parent),
169            location,
170        )
171    }
172
173    /// Emit the final truthy/falsy outcome event inside a marker-eval
174    /// span. Mirrors the shape stored on `MarkerRecording.evaluation`.
175    /// Returns the emitted event's `EventSeq` so callers (e.g. `replay_markers`)
176    /// can use it as a focus pointer.
177    pub fn emit_bool_check(
178        &self,
179        span: SpanId,
180        evaluation: super::super::span::MarkerEvalDetail,
181        location: Option<&IrSpan>,
182    ) -> EventSeq {
183        self.push_event(
184            span,
185            None,
186            None,
187            location,
188            EventKind::BoolCheck { evaluation },
189        )
190    }
191
192    // --- Shells glossary ---------------------------------
193
194    pub fn record_shell_spawn(&self, marker: &str, name: &str, command: &str) {
195        let spawn_ts = self.now();
196        let mut inner = self.inner.lock().unwrap();
197        inner.shells.insert(
198            marker.to_string(),
199            ShellRecord {
200                marker: marker.to_string(),
201                name: name.to_string(),
202                spawn_ts,
203                terminate_ts: None,
204                command: command.to_string(),
205            },
206        );
207    }
208
209    pub fn record_shell_terminate(&self, marker: &str) {
210        let terminate_ts = self.now();
211        let mut inner = self.inner.lock().unwrap();
212        if let Some(rec) = inner.shells.get_mut(marker) {
213            rec.terminate_ts = Some(terminate_ts);
214        }
215    }
216
217    // --- Shell lifecycle emitters ------------------------
218
219    pub fn emit_shell_spawn(
220        &self,
221        span: SpanId,
222        shell: &str,
223        marker: &str,
224        command: &str,
225        location: Option<&IrSpan>,
226    ) {
227        self.record_shell_spawn(marker, shell, command);
228        self.push_event(
229            span,
230            Some(shell),
231            Some(marker),
232            location,
233            EventKind::ShellSpawn {
234                name: shell.to_string(),
235                command: command.to_string(),
236            },
237        );
238        self.push_progress(ProgressEvent::ShellSpawn);
239    }
240
241    pub fn emit_shell_ready(
242        &self,
243        span: SpanId,
244        shell: &str,
245        marker: &str,
246        location: Option<&IrSpan>,
247    ) {
248        self.push_event(
249            span,
250            Some(shell),
251            Some(marker),
252            location,
253            EventKind::ShellReady {
254                name: shell.to_string(),
255            },
256        );
257    }
258
259    pub fn emit_shell_switch(
260        &self,
261        span: SpanId,
262        shell: &str,
263        marker: &str,
264        location: Option<&IrSpan>,
265    ) {
266        self.push_event(
267            span,
268            Some(shell),
269            Some(marker),
270            location,
271            EventKind::ShellSwitch {
272                name: shell.to_string(),
273            },
274        );
275        self.push_progress(ProgressEvent::ShellSwitch(shell.to_string()));
276    }
277
278    pub fn emit_shell_terminate(
279        &self,
280        span: SpanId,
281        shell: &str,
282        marker: &str,
283        location: Option<&IrSpan>,
284    ) {
285        self.record_shell_terminate(marker);
286        self.push_event(
287            span,
288            Some(shell),
289            Some(marker),
290            location,
291            EventKind::ShellTerminate {
292                name: shell.to_string(),
293            },
294        );
295        self.push_progress(ProgressEvent::ShellTerminate);
296    }
297
298    // --- Progress-only emitters (no structured event; the surrounding
299    // span already carries the full information). Used to surface
300    // lifecycle brackets on the live progress line.
301
302    pub fn push_fn_enter(&self, name: &str) {
303        self.push_progress(ProgressEvent::FnEnter(name.to_string()));
304    }
305
306    pub fn push_fn_exit(&self) {
307        self.push_progress(ProgressEvent::FnExit);
308    }
309
310    pub fn push_effect_setup(&self, name: &str) {
311        self.push_progress(ProgressEvent::EffectSetup(name.to_string()));
312    }
313
314    pub fn push_effect_teardown(&self) {
315        self.push_progress(ProgressEvent::EffectTeardown);
316    }
317
318    // --- Effect exposes ----------------------------------
319
320    pub fn emit_effect_expose_shell(
321        &self,
322        span: SpanId,
323        name: &str,
324        target: &str,
325        qualifier: Option<&str>,
326        location: Option<&IrSpan>,
327    ) {
328        self.push_event(
329            span,
330            None,
331            None,
332            location,
333            EventKind::EffectExposeShell {
334                name: name.to_string(),
335                target: target.to_string(),
336                qualifier: qualifier.map(String::from),
337            },
338        );
339    }
340
341    pub fn emit_effect_expose_var(
342        &self,
343        span: SpanId,
344        name: &str,
345        target: &str,
346        qualifier: Option<&str>,
347        value: &str,
348        location: Option<&IrSpan>,
349    ) {
350        self.push_event(
351            span,
352            None,
353            None,
354            location,
355            EventKind::EffectExposeVar {
356                name: name.to_string(),
357                target: target.to_string(),
358                qualifier: qualifier.map(String::from),
359                value: value.to_string(),
360            },
361        );
362    }
363}