Skip to main content

rootcause_tracing/
lib.rs

1#![deny(
2    missing_docs,
3    elided_lifetimes_in_paths,
4    unsafe_code,
5    rustdoc::invalid_rust_codeblocks,
6    rustdoc::broken_intra_doc_links,
7    missing_copy_implementations,
8    unused_doc_comments
9)]
10// Extra checks on nightly
11#![cfg_attr(nightly_extra_checks, feature(rustdoc_missing_doc_code_examples))]
12#![cfg_attr(nightly_extra_checks, forbid(rustdoc::missing_doc_code_examples))]
13
14//! Tracing span capture for rootcause error reports.
15//!
16//! This crate automatically captures tracing span context when errors occur,
17//! helping you understand which operation was being performed.
18//!
19//! # How It Works
20//!
21//! You add [`RootcauseLayer`] to your tracing subscriber alongside your
22//! existing layers (formatting, filtering, log forwarding, etc.). While your
23//! other layers do their work, [`RootcauseLayer`] quietly captures span field
24//! values in the background for use in error reports.
25//!
26//! # Quick Start
27//!
28//! ```
29//! use rootcause::hooks::Hooks;
30//! use rootcause_tracing::{RootcauseLayer, SpanCollector};
31//! use tracing_subscriber::{Registry, layer::SubscriberExt};
32//!
33//! // 1. Set up tracing with RootcauseLayer (required)
34//! let subscriber = Registry::default()
35//!     .with(RootcauseLayer) // Captures span field values for error reports
36//!     .with(tracing_subscriber::fmt::layer()); // Your normal console output
37//! tracing::subscriber::set_global_default(subscriber).expect("failed to set subscriber");
38//!
39//! // 2. Install hook to capture spans for all errors (optional)
40//! Hooks::new()
41//!     .report_creation_hook(SpanCollector::new())
42//!     .install()
43//!     .expect("failed to install hooks");
44//!
45//! // 3. Use normally - spans are captured automatically
46//! #[tracing::instrument(fields(user_id = 42))]
47//! fn example() -> rootcause::Report {
48//!     rootcause::report!("something went wrong")
49//! }
50//! println!("{}", example());
51//! ```
52//!
53//! Output:
54//! ```text
55//!  ● something went wrong
56//!  ├ src/main.rs:10
57//!  ╰ Tracing spans
58//!    │ example{user_id=42}
59//!    ╰─
60//! ```
61//!
62//! ## Manual Attachment
63//!
64//! To attach spans selectively instead of automatically:
65//!
66//! ```
67//! use rootcause::{Report, report};
68//! use rootcause_tracing::SpanExt;
69//!
70//! #[tracing::instrument]
71//! fn operation() -> Result<(), Report> {
72//!     Err(report!("operation failed"))
73//! }
74//!
75//! let result = operation().attach_span();
76//! ```
77//!
78//! **Note:** [`RootcauseLayer`] must be in your subscriber setup either way.
79//!
80//! # Environment Variables
81//!
82//! - `ROOTCAUSE_TRACING` - Comma-separated options:
83//!   - `leafs` - Only capture tracing spans for leaf errors (errors without
84//!     children)
85
86use std::{fmt, sync::OnceLock};
87
88use rootcause::{
89    Report, ReportMut,
90    handlers::{
91        AttachmentFormattingPlacement, AttachmentFormattingStyle, AttachmentHandler,
92        FormattingFunction,
93    },
94    hooks::report_creation::ReportCreationHook,
95    markers::{self, Dynamic, ObjectMarkerFor},
96    report_attachment::ReportAttachment,
97};
98use tracing::{
99    Span,
100    field::{Field, Visit},
101};
102use tracing_subscriber::{
103    Registry,
104    registry::{LookupSpan, SpanRef},
105};
106
107/// Handler for formatting [`Span`] attachments.
108///
109/// # Examples
110///
111/// ```
112/// use rootcause::report_attachment::ReportAttachment;
113/// use rootcause_tracing::SpanHandler;
114/// use tracing::Span;
115///
116/// let span = Span::current();
117/// let _ = ReportAttachment::new_sendsync_custom::<SpanHandler>(span);
118/// ```
119#[derive(Copy, Clone)]
120pub struct SpanHandler;
121
122/// Captured field values for a span.
123struct CapturedFields(String);
124
125impl AttachmentHandler<Span> for SpanHandler {
126    fn display(value: &Span, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
127        match value
128            .with_subscriber(|(span_id, dispatch)| display_span_chain(span_id, dispatch, formatter))
129        {
130            Some(Ok(())) => Ok(()),
131            Some(Err(e)) => Err(e),
132            None => write!(formatter, "No tracing subscriber available"),
133        }
134    }
135
136    fn debug(value: &Span, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
137        std::fmt::Debug::fmt(value, formatter)
138    }
139
140    fn preferred_formatting_style(
141        span: &Span,
142        _report_formatting_function: FormattingFunction,
143    ) -> AttachmentFormattingStyle {
144        AttachmentFormattingStyle {
145            placement: if span.is_none() {
146                AttachmentFormattingPlacement::Hidden
147            } else {
148                AttachmentFormattingPlacement::InlineWithHeader {
149                    header: "Tracing spans:",
150                }
151            },
152            function: FormattingFunction::Display,
153            priority: 9, // Slightly lower priority than backtraces (10)
154        }
155    }
156}
157
158fn display_span_chain(
159    span_id: &tracing::span::Id,
160    dispatch: &tracing::Dispatch,
161    formatter: &mut fmt::Formatter<'_>,
162) -> fmt::Result {
163    let Some(registry) = dispatch.downcast_ref::<Registry>() else {
164        write!(formatter, "No tracing registry subscriber found")?;
165        return Ok(());
166    };
167
168    let Some(span) = registry.span(span_id) else {
169        write!(formatter, "No span found for ID")?;
170        return Ok(());
171    };
172
173    let mut first_span = true;
174
175    for ancestor_span in span.scope() {
176        if first_span {
177            first_span = false;
178        } else {
179            writeln!(formatter)?;
180        }
181        display_span(ancestor_span, formatter)?;
182    }
183
184    Ok(())
185}
186
187fn display_span(
188    span: SpanRef<'_, Registry>,
189    formatter: &mut fmt::Formatter<'_>,
190) -> Result<(), fmt::Error> {
191    write!(formatter, "{}", span.name())?;
192
193    let extensions = span.extensions();
194    let Some(captured_fields) = extensions.get::<CapturedFields>() else {
195        write!(
196            formatter,
197            "{{ Span values missing. Was the RootcauseLayer installed correctly? }}"
198        )?;
199        return Ok(());
200    };
201
202    if captured_fields.0.is_empty() {
203        Ok(())
204    } else {
205        write!(formatter, "{{{}}}", captured_fields.0)
206    }
207}
208
209/// A tracing layer that captures span field values for error reports.
210///
211/// **Required for rootcause-tracing.** Add this to your subscriber alongside
212/// your other layers (formatting, filtering, log forwarding, etc.). It runs in
213/// the background, capturing span field values without affecting your other
214/// layers.
215///
216/// # Examples
217///
218/// ```
219/// use rootcause_tracing::RootcauseLayer;
220/// use tracing_subscriber::{Registry, layer::SubscriberExt};
221///
222/// let subscriber = Registry::default()
223///     .with(RootcauseLayer) // Captures span data for error reports
224///     .with(tracing_subscriber::fmt::layer()); // Example: console output
225///
226/// tracing::subscriber::set_global_default(subscriber).expect("failed to set subscriber");
227/// ```
228#[derive(Copy, Clone, Debug, Default)]
229pub struct RootcauseLayer;
230
231impl<S> tracing_subscriber::Layer<S> for RootcauseLayer
232where
233    S: tracing::Subscriber + for<'a> tracing_subscriber::registry::LookupSpan<'a>,
234{
235    fn on_new_span(
236        &self,
237        attrs: &tracing::span::Attributes<'_>,
238        id: &tracing::span::Id,
239        ctx: tracing_subscriber::layer::Context<'_, S>,
240    ) {
241        let span = ctx.span(id).expect("span not found");
242        let mut extensions = span.extensions_mut();
243
244        struct Visitor(String);
245
246        impl Visit for Visitor {
247            fn record_debug(&mut self, field: &Field, value: &dyn fmt::Debug) {
248                use std::fmt::Write;
249                if self.0.is_empty() {
250                    let _ = write!(self.0, "{}={value:?}", field.name());
251                } else {
252                    let _ = write!(self.0, " {}={value:?}", field.name());
253                }
254            }
255        }
256
257        let mut visitor = Visitor(String::new());
258        attrs.record(&mut visitor);
259        extensions.insert(CapturedFields(visitor.0));
260    }
261}
262
263/// Attachment collector for capturing tracing spans.
264///
265/// When registered as a report creation hook, this collector automatically
266/// captures the current tracing span and attaches it as a [`Span`] attachment.
267///
268/// # Examples
269///
270/// Basic usage with default settings:
271///
272/// ```
273/// use rootcause::hooks::Hooks;
274/// use rootcause_tracing::SpanCollector;
275///
276/// Hooks::new()
277///     .report_creation_hook(SpanCollector::new())
278///     .install()
279///     .expect("failed to install hooks");
280/// ```
281///
282/// Custom configuration:
283///
284/// ```
285/// use rootcause::hooks::Hooks;
286/// use rootcause_tracing::SpanCollector;
287///
288/// let collector = SpanCollector {
289///     capture_span_for_reports_with_children: true,
290/// };
291///
292/// Hooks::new()
293///     .report_creation_hook(collector)
294///     .install()
295///     .expect("failed to install hooks");
296/// ```
297#[derive(Copy, Clone)]
298pub struct SpanCollector {
299    /// Whether to capture spans for all reports or only leaf reports (those
300    /// without children).
301    ///
302    /// When `true`, all reports get span attachments. When `false`, only leaf
303    /// reports do.
304    pub capture_span_for_reports_with_children: bool,
305}
306
307#[derive(Debug)]
308struct RootcauseTracingEnvOptions {
309    span_leafs_only: bool,
310}
311
312impl RootcauseTracingEnvOptions {
313    fn get() -> &'static Self {
314        static ROOTCAUSE_TRACING_FLAGS: OnceLock<RootcauseTracingEnvOptions> = OnceLock::new();
315
316        ROOTCAUSE_TRACING_FLAGS.get_or_init(|| {
317            let mut span_leafs_only = false;
318
319            if let Some(var) = std::env::var_os("ROOTCAUSE_TRACING") {
320                for v in var.to_string_lossy().split(',') {
321                    if v.eq_ignore_ascii_case("leafs") {
322                        span_leafs_only = true;
323                    }
324                }
325            }
326
327            RootcauseTracingEnvOptions { span_leafs_only }
328        })
329    }
330}
331
332impl SpanCollector {
333    /// Creates a new [`SpanCollector`] with default settings.
334    ///
335    /// Configuration is controlled by environment variables.
336    ///
337    /// # Environment Variables
338    ///
339    /// - `ROOTCAUSE_TRACING` - Comma-separated options:
340    ///   - `leafs` - Only capture tracing spans for leaf errors (errors without
341    ///     children)
342    ///
343    /// # Examples
344    ///
345    /// ```
346    /// use rootcause::hooks::Hooks;
347    /// use rootcause_tracing::SpanCollector;
348    ///
349    /// // Respects ROOTCAUSE_TRACING environment variable
350    /// Hooks::new()
351    ///     .report_creation_hook(SpanCollector::new())
352    ///     .install()
353    ///     .expect("failed to install hooks");
354    /// ```
355    pub fn new() -> Self {
356        let env_options = RootcauseTracingEnvOptions::get();
357        let capture_span_for_reports_with_children = !env_options.span_leafs_only;
358
359        Self {
360            capture_span_for_reports_with_children,
361        }
362    }
363}
364
365impl Default for SpanCollector {
366    fn default() -> Self {
367        Self::new()
368    }
369}
370
371impl ReportCreationHook for SpanCollector {
372    fn on_local_creation(&self, mut report: ReportMut<'_, Dynamic, markers::Local>) {
373        let do_capture =
374            self.capture_span_for_reports_with_children || report.children().is_empty();
375        if do_capture {
376            let span = Span::current();
377            if !span.is_none() {
378                let attachment = ReportAttachment::new_custom::<SpanHandler>(span);
379                report.attachments_mut().push(attachment.into_dynamic());
380            }
381        }
382    }
383
384    fn on_sendsync_creation(&self, mut report: ReportMut<'_, Dynamic, markers::SendSync>) {
385        let do_capture =
386            self.capture_span_for_reports_with_children || report.children().is_empty();
387        if do_capture {
388            let span = Span::current();
389            if !span.is_none() {
390                let attachment = ReportAttachment::new_custom::<SpanHandler>(span);
391                report.attachments_mut().push(attachment.into_dynamic());
392            }
393        }
394    }
395}
396
397/// Extension trait for attaching tracing spans to reports.
398///
399/// This trait provides methods to easily attach the current tracing span
400/// to a report or to the error contained within a `Result`.
401///
402/// # Examples
403///
404/// Attach tracing span to a report:
405///
406/// ```
407/// use rootcause::report;
408/// use rootcause_tracing::SpanExt;
409///
410/// #[tracing::instrument]
411/// fn example() {
412///     let report = report!("An error occurred").attach_span();
413/// }
414/// ```
415///
416/// Attach tracing span to a `Result`:
417///
418/// ```
419/// use rootcause::{Report, report};
420/// use rootcause_tracing::SpanExt;
421///
422/// #[tracing::instrument]
423/// fn might_fail() -> Result<(), Report> {
424///     Err(report!("operation failed").into_dynamic())
425/// }
426///
427/// let result = might_fail().attach_span();
428/// ```
429pub trait SpanExt: Sized {
430    /// Attaches the current tracing span to the report.
431    ///
432    /// # Examples
433    ///
434    /// ```
435    /// use rootcause::report;
436    /// use rootcause_tracing::SpanExt;
437    ///
438    /// #[tracing::instrument]
439    /// fn example() {
440    ///     let report = report!("error").attach_span();
441    /// }
442    /// ```
443    fn attach_span(self) -> Self;
444}
445
446impl<C: ?Sized, T> SpanExt for Report<C, markers::Mutable, T>
447where
448    Span: ObjectMarkerFor<T>,
449{
450    fn attach_span(mut self) -> Self {
451        let span = Span::current();
452        if !span.is_disabled() {
453            self = self.attach_custom::<SpanHandler, _>(span);
454        }
455        self
456    }
457}
458
459impl<C: ?Sized, V, T> SpanExt for Result<V, Report<C, markers::Mutable, T>>
460where
461    Span: ObjectMarkerFor<T>,
462{
463    fn attach_span(self) -> Self {
464        match self {
465            Ok(v) => Ok(v),
466            Err(report) => Err(report.attach_span()),
467        }
468    }
469}