rootcause_tracing/
lib.rs

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