Skip to main content

obs_core/
span_trace.rs

1//! `obs::SpanTrace` — capture the active scope/span ancestry for
2//! attaching to error types.
3//!
4//! Spec 13 § 9.
5
6use std::fmt;
7
8use crate::scope::{ScopeField, ScopeFrame, with_frames_innermost_first};
9
10/// One captured frame's name + fields, decoupled from the live
11/// `ScopeFrame` so the `SpanTrace` can outlive the scope guard.
12#[derive(Debug, Clone)]
13struct CapturedFrame {
14    name: Option<String>,
15    target: Option<String>,
16    fields: Vec<(String, String)>,
17}
18
19/// Captured `obs::scope!` ancestry. Like `tracing-error::SpanTrace`.
20#[derive(Debug, Clone, Default)]
21pub struct SpanTrace {
22    frames: Vec<CapturedFrame>,
23}
24
25impl SpanTrace {
26    /// Walk the active task's `obs::scope!` stack and capture frames
27    /// outermost-first. Cheap: zero allocation when the stack is
28    /// empty, linear in stack depth otherwise.
29    #[must_use]
30    pub fn capture() -> Self {
31        let mut frames: Vec<CapturedFrame> =
32            with_frames_innermost_first(|stack| stack.iter().map(capture_frame).collect());
33        // `with_frames_innermost_first` returns the stack as-is
34        // (outermost-first by index because Vec::push appends), so
35        // emit Display in that natural order.
36        frames.shrink_to_fit();
37        Self { frames }
38    }
39
40    /// `true` when no scope was active at capture time.
41    #[must_use]
42    pub fn is_empty(&self) -> bool {
43        self.frames.is_empty()
44    }
45
46    /// Number of captured frames.
47    #[must_use]
48    pub fn len(&self) -> usize {
49        self.frames.len()
50    }
51}
52
53fn capture_frame(frame: &ScopeFrame) -> CapturedFrame {
54    let span = frame.as_span_frame();
55    CapturedFrame {
56        name: span.map(|s| s.name.to_string()),
57        target: span.map(|s| s.target.to_string()),
58        fields: frame
59            .fields()
60            .iter()
61            .map(|f| match f {
62                ScopeField::TraceId(v) => ("trace_id".to_string(), v.clone()),
63                ScopeField::SpanId(v) => ("span_id".to_string(), v.clone()),
64                ScopeField::ParentSpanId(v) => ("parent_span_id".to_string(), v.clone()),
65                ScopeField::Label(k, v) => ((*k).to_string(), v.clone()),
66            })
67            .collect(),
68    }
69}
70
71impl fmt::Display for SpanTrace {
72    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
73        if self.frames.is_empty() {
74            return f.write_str("(no obs scope active)");
75        }
76        // Print innermost first so error chain reads "the immediate
77        // context, then its parent, …".
78        for (i, frame) in self.frames.iter().enumerate().rev() {
79            let depth = self.frames.len() - 1 - i;
80            write!(
81                f,
82                "  {depth}: {}",
83                frame.name.as_deref().unwrap_or("<scope>")
84            )?;
85            if let Some(t) = frame.target.as_deref() {
86                write!(f, " @ {t}")?;
87            }
88            if !frame.fields.is_empty() {
89                write!(f, " [")?;
90                for (j, (k, v)) in frame.fields.iter().enumerate() {
91                    if j > 0 {
92                        write!(f, ", ")?;
93                    }
94                    write!(f, "{k}={v}")?;
95                }
96                write!(f, "]")?;
97            }
98            writeln!(f)?;
99        }
100        Ok(())
101    }
102}