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}