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