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