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}