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}