Skip to main content

oopsie_core/
spantrace.rs

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