Skip to main content

oopsie_core/
spantrace.rs

1//! SpanTrace wrapper with `Capturable` support.
2
3use std::fmt;
4
5/// A wrapper around `tracing_error::SpanTrace`.
6#[cfg(feature = "tracing")]
7#[derive(Debug, Clone)]
8pub struct SpanTrace {
9    inner: tracing_error::SpanTrace,
10}
11
12#[cfg(feature = "tracing")]
13impl SpanTrace {
14    /// Captures the currently active span stack from the installed subscriber.
15    ///
16    /// Yields an empty trace when no span is active or the subscriber does not
17    /// support span traces; check [`is_captured`](Self::is_captured) to tell the
18    /// two apart.
19    #[must_use]
20    #[inline]
21    pub fn capture() -> Self {
22        Self {
23            inner: tracing_error::SpanTrace::capture(),
24        }
25    }
26
27    /// Wraps an existing `tracing_error::SpanTrace`.
28    #[must_use]
29    #[inline]
30    pub const fn new(inner: tracing_error::SpanTrace) -> Self {
31        Self { inner }
32    }
33
34    /// Returns whether this trace was captured, empty, or unsupported.
35    #[must_use]
36    #[inline]
37    pub fn status(&self) -> tracing_error::SpanTraceStatus {
38        self.inner.status()
39    }
40
41    /// `true` if a span trace was actually captured (an active span existed and
42    /// the subscriber supports `SpanTrace`). An empty/unsupported trace yields
43    /// no frames and should not be rendered as a `SPANTRACE` section.
44    #[must_use]
45    #[inline]
46    pub fn is_captured(&self) -> bool {
47        matches!(self.status(), tracing_error::SpanTraceStatus::CAPTURED)
48    }
49
50    /// Consumes the wrapper and returns the underlying `tracing_error::SpanTrace`.
51    #[must_use]
52    #[inline]
53    pub fn into_span_trace(self) -> tracing_error::SpanTrace {
54        self.inner
55    }
56
57    /// Borrows the underlying `tracing_error::SpanTrace`.
58    #[inline]
59    #[must_use]
60    pub const fn as_span_trace(&self) -> &tracing_error::SpanTrace {
61        &self.inner
62    }
63}
64
65// Span fields carry no stable identity, so only debug builds keep them around to
66// tighten equality; release builds drop them and compare callsites alone.
67#[cfg(all(feature = "tracing", debug_assertions))]
68#[inline]
69fn capture_fields(fields: &str) -> String {
70    fields.to_owned()
71}
72
73#[cfg(all(feature = "tracing", not(debug_assertions)))]
74#[inline]
75fn capture_fields(_fields: &str) {}
76
77#[cfg(all(feature = "tracing", debug_assertions))]
78#[inline]
79fn fields_eq(stored: &str, current: &str) -> bool {
80    stored == current
81}
82
83#[cfg(all(feature = "tracing", not(debug_assertions)))]
84#[inline]
85fn fields_eq(_stored: &(), _current: &str) -> bool {
86    true
87}
88
89#[cfg(feature = "tracing")]
90impl PartialEq for SpanTrace {
91    fn eq(&self, other: &Self) -> bool {
92        use std::collections::VecDeque;
93
94        let a = &self.inner;
95        let b = &other.inner;
96
97        let mut a_frames = VecDeque::with_capacity(2);
98        a.with_spans(|a_md, a_fields| {
99            a_frames.push_back((a_md.callsite(), capture_fields(a_fields)));
100            true
101        });
102        let mut equal = true;
103        b.with_spans(|b_md, b_fields| {
104            equal = match a_frames.pop_front() {
105                Some((a_callsite, a_fields)) => {
106                    a_callsite == b_md.callsite() && fields_eq(&a_fields, b_fields)
107                }
108                None => false,
109            };
110            equal
111        });
112        equal && a_frames.is_empty()
113    }
114}
115
116#[cfg(feature = "tracing")]
117impl fmt::Display for SpanTrace {
118    #[inline]
119    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
120        fmt::Display::fmt(&self.inner, f)
121    }
122}
123
124/// A no-op span trace stub: the type stays present without the `tracing`
125/// feature so the captured-spantrace surface is unconditional, but capture is
126/// inert and the trace is never considered present.
127#[cfg(not(feature = "tracing"))]
128#[derive(Clone, Debug)]
129pub struct SpanTrace;
130
131#[cfg(not(feature = "tracing"))]
132impl SpanTrace {
133    /// Returns the inert stub; capture is a no-op without the `tracing` feature.
134    #[must_use]
135    #[inline]
136    pub const fn capture() -> Self {
137        Self
138    }
139
140    /// Always `false`: the stub never holds a captured trace.
141    #[must_use]
142    #[inline]
143    pub const fn is_captured(&self) -> bool {
144        false
145    }
146}
147
148#[cfg(not(feature = "tracing"))]
149impl PartialEq for SpanTrace {
150    #[inline]
151    fn eq(&self, _other: &Self) -> bool {
152        true
153    }
154}
155
156#[cfg(not(feature = "tracing"))]
157impl fmt::Display for SpanTrace {
158    #[inline]
159    fn fmt(&self, _f: &mut fmt::Formatter<'_>) -> fmt::Result {
160        Ok(())
161    }
162}
163
164impl crate::Capturable for SpanTrace {
165    #[inline]
166    fn capture() -> Self {
167        Self::capture()
168    }
169
170    #[inline]
171    fn capture_or_extract(source: &dyn crate::Diagnostic) -> Self {
172        match source.oopsie_spantrace() {
173            // Keep the source's trace only if capture actually succeeded; an
174            // EMPTY/UNSUPPORTED trace carries nothing worth preserving over a
175            // fresh capture at the wrap site.
176            Some(trace) if trace.is_captured() => trace.clone(),
177            _ => Self::capture(),
178        }
179    }
180}
181
182/// A wrapper around `Option<SpanTrace>` that implements [`Capturable`](crate::Capturable).
183///
184/// [`is_some`](Self::is_some) / [`is_none`](Self::is_none) reflect whether a
185/// `SpanTrace` value is present, not whether it was successfully captured: the
186/// [`some`](Self::some) and `From` constructors wrap unconditionally, so a
187/// present trace may still be empty/unsupported. Only the
188/// [`Capturable`](crate::Capturable) construction path enforces
189/// `is_some() == captured`.
190#[derive(Clone, Debug, Default)]
191pub struct OptionalSpanTrace(Option<SpanTrace>);
192
193impl OptionalSpanTrace {
194    /// Create an empty `OptionalSpanTrace`.
195    #[must_use]
196    #[inline]
197    pub const fn none() -> Self {
198        Self(None)
199    }
200
201    /// Create an `OptionalSpanTrace` from an existing `SpanTrace`.
202    #[must_use]
203    #[inline]
204    pub const fn some(trace: SpanTrace) -> Self {
205        Self(Some(trace))
206    }
207
208    /// Returns the inner `Option<SpanTrace>`.
209    // The real `SpanTrace`'s destructor isn't const-evaluable, so only the
210    // featureless stub variant can be a `const fn`.
211    #[cfg(feature = "tracing")]
212    #[must_use]
213    #[inline]
214    pub fn into_inner(self) -> Option<SpanTrace> {
215        self.0
216    }
217
218    /// Returns the inner `Option<SpanTrace>`.
219    #[cfg(not(feature = "tracing"))]
220    #[must_use]
221    #[inline]
222    pub const fn into_inner(self) -> Option<SpanTrace> {
223        self.0
224    }
225
226    /// Returns a reference to the inner `SpanTrace` if present.
227    #[must_use]
228    #[inline]
229    pub const fn as_ref(&self) -> Option<&SpanTrace> {
230        self.0.as_ref()
231    }
232
233    /// Returns true if this contains a span trace.
234    #[must_use]
235    #[inline]
236    pub const fn is_some(&self) -> bool {
237        self.0.is_some()
238    }
239
240    /// Returns true if this does not contain a span trace.
241    #[must_use]
242    #[inline]
243    pub const fn is_none(&self) -> bool {
244        self.0.is_none()
245    }
246}
247
248impl fmt::Display for OptionalSpanTrace {
249    #[inline]
250    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
251        match &self.0 {
252            Some(trace) => fmt::Display::fmt(trace, f),
253            None => Ok(()),
254        }
255    }
256}
257
258impl crate::Capturable for OptionalSpanTrace {
259    #[inline]
260    fn capture() -> Self {
261        let trace = SpanTrace::capture();
262        if trace.is_captured() {
263            Self(Some(trace))
264        } else {
265            Self(None)
266        }
267    }
268
269    #[inline]
270    fn capture_or_extract(source: &dyn crate::Diagnostic) -> Self {
271        match source.oopsie_spantrace() {
272            // Keep the source's trace only if capture actually succeeded; an
273            // Empty/Unsupported trace must not flip `is_some()` to true (the
274            // type's documented invariant) — fall back to a fresh capture.
275            Some(trace) if trace.is_captured() => Self::some(trace.clone()),
276            _ => <Self as crate::Capturable>::capture(),
277        }
278    }
279}
280
281impl From<SpanTrace> for OptionalSpanTrace {
282    #[inline]
283    fn from(trace: SpanTrace) -> Self {
284        Self(Some(trace))
285    }
286}
287
288impl From<Option<SpanTrace>> for OptionalSpanTrace {
289    #[inline]
290    fn from(opt: Option<SpanTrace>) -> Self {
291        Self(opt)
292    }
293}
294
295#[cfg(test)]
296mod tests {
297    use super::*;
298    use crate::Capturable;
299
300    #[test]
301    fn test_span_trace_capture() {
302        let trace = SpanTrace::capture();
303        let _ = trace.is_captured();
304    }
305
306    #[test]
307    fn test_generate_implicit_data() {
308        let trace: SpanTrace = Capturable::capture();
309        let _ = trace.is_captured();
310    }
311
312    #[test]
313    fn test_optional_span_trace() {
314        let trace: OptionalSpanTrace = Capturable::capture();
315        let _ = trace;
316    }
317
318    #[cfg(feature = "tracing")]
319    #[test]
320    fn test_into_span_trace_tracing_returns_some() {
321        let tracing = SpanTrace::capture();
322        let _ = tracing.into_span_trace();
323    }
324
325    #[test]
326    fn test_optional_span_trace_none_into_inner() {
327        let opt = OptionalSpanTrace::none();
328        assert!(opt.into_inner().is_none());
329    }
330
331    #[test]
332    fn test_optional_span_trace_as_ref_none() {
333        let opt = OptionalSpanTrace::none();
334        assert!(opt.as_ref().is_none());
335    }
336
337    #[test]
338    fn test_optional_span_trace_is_none() {
339        let opt = OptionalSpanTrace::none();
340        assert!(opt.is_none());
341        assert!(!opt.is_some());
342    }
343
344    #[test]
345    fn test_optional_span_trace_display_none() {
346        let opt = OptionalSpanTrace::none();
347        let display = opt.to_string();
348        assert!(display.is_empty());
349    }
350
351    #[test]
352    fn test_optional_span_trace_from_option_none() {
353        let opt: OptionalSpanTrace = None::<SpanTrace>.into();
354        assert!(opt.is_none());
355    }
356
357    #[test]
358    fn test_optional_span_trace_generate_without_subscriber() {
359        let opt: OptionalSpanTrace = Capturable::capture();
360        assert!(opt.is_none());
361    }
362
363    // Tests that need a live SpanTrace instance.
364    #[test]
365    fn test_optional_span_trace_some_into_inner() {
366        let trace = SpanTrace::capture();
367        let opt = OptionalSpanTrace::some(trace);
368        assert!(opt.into_inner().is_some());
369    }
370
371    #[test]
372    fn test_optional_span_trace_as_ref_some() {
373        let trace = SpanTrace::capture();
374        let opt = OptionalSpanTrace::some(trace);
375        assert!(opt.as_ref().is_some());
376    }
377
378    #[test]
379    fn test_optional_span_trace_is_some() {
380        let trace = SpanTrace::capture();
381        let opt = OptionalSpanTrace::some(trace);
382        assert!(opt.is_some());
383        assert!(!opt.is_none());
384    }
385
386    #[test]
387    fn test_optional_span_trace_display_some() {
388        let trace = SpanTrace::capture();
389        let opt = OptionalSpanTrace::some(trace);
390        let _ = opt.to_string();
391    }
392
393    #[test]
394    fn test_optional_span_trace_from_spantrace() {
395        let trace = SpanTrace::capture();
396        let opt: OptionalSpanTrace = trace.into();
397        assert!(opt.is_some());
398    }
399
400    #[test]
401    fn test_optional_span_trace_from_option_some() {
402        let trace = SpanTrace::capture();
403        let opt: OptionalSpanTrace = Some(trace).into();
404        assert!(opt.is_some());
405    }
406
407    #[cfg(feature = "tracing")]
408    fn with_error_subscriber<R>(f: impl FnOnce() -> R) -> R {
409        use tracing_subscriber::prelude::*;
410        let subscriber =
411            tracing_subscriber::Registry::default().with(tracing_error::ErrorLayer::default());
412        tracing::subscriber::with_default(subscriber, f)
413    }
414
415    // Fixed callsites shared across captures: a depth-3 stack leaf -> mid -> root.
416    // Same source location => same `&'static Metadata`, so two captures through
417    // the same functions yield identical stacks.
418    #[cfg(feature = "tracing")]
419    fn leaf() -> SpanTrace {
420        let _g = tracing::info_span!("leaf").entered();
421        SpanTrace::capture()
422    }
423    #[cfg(feature = "tracing")]
424    fn mid() -> SpanTrace {
425        let _g = tracing::info_span!("mid").entered();
426        leaf()
427    }
428    #[cfg(feature = "tracing")]
429    fn via_root_a() -> SpanTrace {
430        let _g = tracing::info_span!("root_a").entered();
431        mid()
432    }
433    #[cfg(feature = "tracing")]
434    fn via_root_b() -> SpanTrace {
435        let _g = tracing::info_span!("root_b").entered();
436        mid()
437    }
438
439    // A single fixed callsite that records a field value. Two captures through
440    // this function share the same `&'static Metadata` (same source location),
441    // so they differ ONLY in the recorded value of `x`.
442    #[cfg(feature = "tracing")]
443    fn via_field(x: u32) -> SpanTrace {
444        let _g = tracing::info_span!("field_span", x).entered();
445        SpanTrace::capture()
446    }
447
448    #[cfg(feature = "tracing")]
449    #[test]
450    fn identical_depth3_stacks_are_equal() {
451        let (a, b) = with_error_subscriber(|| (via_root_a(), via_root_a()));
452        assert_eq!(a.status(), tracing_error::SpanTraceStatus::CAPTURED);
453        assert_eq!(a, b);
454        assert_eq!(a, a.clone());
455    }
456
457    #[cfg(feature = "tracing")]
458    #[test]
459    fn depth3_stacks_differing_only_at_root_are_unequal() {
460        // Shared leaf+mid callsites, divergent root: the case a leaf-only or
461        // out-of-order comparison gets wrong.
462        let (a, b) = with_error_subscriber(|| (via_root_a(), via_root_b()));
463        assert_ne!(a, b);
464    }
465
466    #[cfg(feature = "tracing")]
467    #[test]
468    fn shorter_stack_is_not_equal_to_deeper_one() {
469        let (deep, shallow) = with_error_subscriber(|| (via_root_a(), leaf()));
470        assert_ne!(deep, shallow);
471        // Reverse direction exercises the b-longer-than-a frame-walk branch.
472        assert_ne!(shallow, deep);
473    }
474
475    #[cfg(feature = "tracing")]
476    #[test]
477    fn empty_span_traces_are_equal() {
478        // Without a subscriber, captures are uncaptured/empty; equality must be
479        // reflexive rather than reporting two empty traces as unequal.
480        let a = SpanTrace::capture();
481        let b = SpanTrace::capture();
482        assert_eq!(a, b);
483        assert_eq!(a, a.clone());
484    }
485
486    #[cfg(feature = "tracing")]
487    #[test]
488    fn same_callsite_differing_field_values() {
489        // Both captures go through the *same* `info_span!` callsite, so the
490        // callsite comparison in `eq` can't tell them apart — only the recorded
491        // value of `x` differs. This is the one shape that reaches the
492        // field-value branch of `eq` with a non-equal result; every other
493        // inequality test diverges by span name (i.e. by callsite).
494        let (a, b) = with_error_subscriber(|| (via_field(1), via_field(2)));
495        assert_eq!(a.status(), tracing_error::SpanTraceStatus::CAPTURED);
496
497        // Control: identical callsite *and* identical field value compare equal
498        // in either build profile, proving the divergence below comes from the
499        // field values and nothing else.
500        let (c, d) = with_error_subscriber(|| (via_field(1), via_field(1)));
501        assert_eq!(c, d);
502
503        // Debug builds compare recorded field values; release builds compare
504        // callsites only (see `capture_fields`/`fields_eq`).
505        #[cfg(debug_assertions)]
506        assert_ne!(a, b, "debug builds must distinguish differing field values");
507        #[cfg(not(debug_assertions))]
508        assert_eq!(
509            a, b,
510            "release builds compare callsites only, ignoring fields"
511        );
512    }
513
514    #[derive(Debug)]
515    struct DiagSource;
516
517    impl fmt::Display for DiagSource {
518        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
519            f.write_str("diag source")
520        }
521    }
522
523    impl std::error::Error for DiagSource {}
524    impl crate::Diagnostic for DiagSource {}
525
526    #[test]
527    fn optional_span_trace_captures_from_diagnostic_source() {
528        let opt = <OptionalSpanTrace as crate::Capturable>::capture_or_extract(&DiagSource);
529        assert!(opt.is_none());
530    }
531
532    #[derive(Debug)]
533    struct DiagSourceWithEmptyTrace(SpanTrace);
534
535    impl fmt::Display for DiagSourceWithEmptyTrace {
536        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
537            f.write_str("diag source")
538        }
539    }
540
541    impl std::error::Error for DiagSourceWithEmptyTrace {}
542    impl crate::Diagnostic for DiagSourceWithEmptyTrace {
543        fn oopsie_spantrace(&self) -> Option<&SpanTrace> {
544            Some(&self.0)
545        }
546    }
547
548    #[test]
549    fn capture_or_extract_drops_empty_source_trace() {
550        // With no active subscriber the captured trace is empty/unsupported.
551        // Extracting it must not flip `is_some()` to true (the type invariant).
552        let src = DiagSourceWithEmptyTrace(SpanTrace::capture());
553        assert!(!src.0.is_captured(), "precondition: source trace is empty");
554        let opt = <OptionalSpanTrace as crate::Capturable>::capture_or_extract(&src);
555        assert!(opt.is_none());
556    }
557
558    #[cfg(feature = "tracing")]
559    #[test]
560    fn span_trace_capture_or_extract_recaptures_over_empty_source_trace() {
561        // Source captured with no subscriber → empty trace.
562        let src = DiagSourceWithEmptyTrace(SpanTrace::capture());
563        assert!(!src.0.is_captured(), "precondition: source trace is empty");
564        // Wrap site has a live subscriber inside an active span → fresh capture
565        // must win over the source's empty trace.
566        let extracted = with_error_subscriber(|| {
567            let _g = tracing::info_span!("wrap_site").entered();
568            <SpanTrace as crate::Capturable>::capture_or_extract(&src)
569        });
570        assert!(extracted.is_captured());
571    }
572
573    #[cfg(feature = "tracing")]
574    #[test]
575    fn span_trace_capture_or_extract_keeps_captured_source_trace() {
576        let (src_trace, extracted) = with_error_subscriber(|| {
577            let src = DiagSourceWithEmptyTrace(leaf());
578            let t = <SpanTrace as crate::Capturable>::capture_or_extract(&src);
579            (src.0, t)
580        });
581        assert!(extracted.is_captured());
582        assert_eq!(extracted, src_trace);
583    }
584
585    #[cfg(not(feature = "tracing"))]
586    mod stub {
587        use super::*;
588
589        #[test]
590        fn capture_is_inert() {
591            assert!(!SpanTrace::capture().is_captured());
592        }
593
594        #[test]
595        fn display_is_empty() {
596            assert!(SpanTrace::capture().to_string().is_empty());
597        }
598
599        #[test]
600        fn equality_is_reflexive() {
601            let a = SpanTrace::capture();
602            assert_eq!(a, a.clone());
603            assert_eq!(SpanTrace::capture(), SpanTrace::capture());
604        }
605
606        #[test]
607        fn capture_or_extract_yields_stub() {
608            let extracted = <SpanTrace as crate::Capturable>::capture_or_extract(
609                &DiagSourceWithEmptyTrace(SpanTrace::capture()),
610            );
611            assert!(!extracted.is_captured());
612        }
613
614        #[test]
615        fn optional_capture_is_none() {
616            let opt: OptionalSpanTrace = Capturable::capture();
617            assert!(opt.is_none());
618        }
619    }
620}