rootcause_backtrace/lib.rs
1#![deny(
2 elided_lifetimes_in_paths,
3 missing_docs,
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//! Stack backtrace attachment collector for rootcause error reports.
15//!
16//! This crate provides functionality to automatically capture and attach stack
17//! backtraces to error reports. This is useful for debugging to see the call
18//! stack that led to an error.
19//!
20//! # Quick Start
21//!
22//! ## Using Hooks (Automatic for All Errors)
23//!
24//! Register a backtrace collector as a hook to automatically capture backtraces
25//! for all errors:
26//!
27//! ```
28//! use rootcause::hooks::Hooks;
29//! use rootcause_backtrace::BacktraceCollector;
30//!
31//! // Capture backtraces for all errors
32//! Hooks::new()
33//! .report_creation_hook(BacktraceCollector::new_from_env())
34//! .install()
35//! .expect("failed to install hooks");
36//!
37//! // Now all errors automatically get backtraces!
38//! fn example() -> rootcause::Report {
39//! rootcause::report!("something went wrong")
40//! }
41//! println!("{}", example().context("additional context"));
42//! ```
43//!
44//! This will print a backtrace similar to the following:
45//! ```text
46//! ● additional context
47//! ├ src/main.rs:12
48//! ├ Backtrace
49//! │ │ main - /build/src/main.rs:12
50//! │ │ note: 39 frame(s) omitted. For a complete backtrace, set RUST_BACKTRACE=full.
51//! │ ╰─
52//! │
53//! ● something went wrong
54//! ├ src/main.rs:10
55//! ╰ Backtrace
56//! │ example - /build/src/main.rs:10
57//! │ main - /build/src/main.rs:12
58//! │ note: 40 frame(s) omitted. For a complete backtrace, set RUST_BACKTRACE=full.
59//! ╰─
60//! ```
61//!
62//! ## Manual Attachment (Per-Error)
63//!
64//! Attach backtraces to specific errors using the extension trait:
65//!
66//! ```
67//! use std::io;
68//!
69//! use rootcause::{Report, report};
70//! use rootcause_backtrace::BacktraceExt;
71//!
72//! fn operation() -> Result<(), Report> {
73//! Err(report!("operation failed"))
74//! }
75//!
76//! // Attach backtrace to the error in the Result
77//! let result = operation().attach_backtrace();
78//! ```
79//!
80//! # Environment Variables
81//!
82//! - `RUST_BACKTRACE=full` - Disables filtering and shows full paths
83//! - `ROOTCAUSE_BACKTRACE` - Comma-separated options:
84//! - `leafs` - Only capture backtraces for leaf errors (errors without
85//! children)
86//! - `full_paths` - Show full file paths in backtraces
87//!
88//! # Path privacy
89//!
90//! By default, backtrace paths are shortened paths for improved readability,
91//! but this may still expose private file system structure when a path is not
92//! recognized as belonging to a known prefix (e.g., RUST_SRC).
93//!
94//! If exposing private file system paths is a concern, then we recommend using
95//! the `--remap-path-prefix` option of `rustc` to remap source paths to
96//! generic placeholders.
97//!
98//! A good default way to handle this is to set the following environment
99//! variables when building your application for release:
100//!
101//! ```sh
102//! export RUSTFLAGS="--remap-path-prefix=$HOME=/home/user --remap-path-prefix=$PWD=/build"
103//! ```
104//!
105//! # Debugging symbols in release builds
106//!
107//! To ensure that backtraces contain useful symbol and source location
108//! information in release builds, make sure to enable debug symbols in your
109//! `Cargo.toml`:
110//!
111//! ```toml
112//! [profile.release]
113//! strip = false
114//! # You can also set this to "line-tables-only" for smaller binaries
115//! debug = true
116//! ```
117//!
118//! # Filtering
119//!
120//! Control which frames appear in backtraces:
121//!
122//! ```
123//! use rootcause_backtrace::{BacktraceCollector, BacktraceFilter};
124//!
125//! let collector = BacktraceCollector {
126//! filter: BacktraceFilter {
127//! skipped_initial_crates: &["rootcause", "rootcause-backtrace"], // Skip frames from rootcause at start
128//! skipped_middle_crates: &["tokio"], // Skip tokio frames in middle
129//! skipped_final_crates: &["std"], // Skip std frames at end
130//! max_entry_count: 15, // Limit to 15 frames
131//! show_full_path: false, // Show shortened paths
132//! },
133//! capture_backtrace_for_reports_with_children: false, // Only leaf errors
134//! };
135//! ```
136
137use std::{borrow::Cow, fmt, panic::Location, sync::OnceLock};
138
139use backtrace::BytesOrWideString;
140use rootcause::{
141 Report, ReportMut,
142 handlers::{
143 AttachmentFormattingPlacement, AttachmentFormattingStyle, AttachmentHandler,
144 FormattingFunction,
145 },
146 hooks::report_creation::ReportCreationHook,
147 markers::{self, Dynamic, ObjectMarkerFor},
148 report_attachment::ReportAttachment,
149};
150
151/// Stack backtrace information.
152///
153/// Contains a collection of stack frames representing the call stack
154/// at the point where a report was created.
155///
156/// # Examples
157///
158/// Capture a backtrace manually:
159///
160/// ```
161/// use rootcause_backtrace::{Backtrace, BacktraceFilter};
162///
163/// let backtrace = Backtrace::capture(&BacktraceFilter::DEFAULT);
164/// if let Some(bt) = backtrace {
165/// println!("Captured {} frames", bt.entries.len());
166/// }
167/// ```
168#[derive(Debug, Clone)]
169pub struct Backtrace {
170 /// The entries in the backtrace, ordered from most recent to oldest.
171 pub entries: Vec<BacktraceEntry>,
172 /// Total number of frames that were omitted due to filtering.
173 pub total_omitted_frames: usize,
174}
175
176/// A single entry in a stack backtrace.
177///
178/// # Examples
179///
180/// ```
181/// use rootcause_backtrace::{Backtrace, BacktraceEntry, BacktraceFilter};
182///
183/// if let Some(bt) = Backtrace::capture(&BacktraceFilter::DEFAULT) {
184/// for entry in &bt.entries {
185/// match entry {
186/// BacktraceEntry::Frame(_) => { /* a real call */ }
187/// BacktraceEntry::OmittedFrames { count, skipped_crate } => {
188/// println!("Omitted {count} frames from {skipped_crate}");
189/// }
190/// }
191/// }
192/// }
193/// ```
194#[derive(Debug, Clone)]
195pub enum BacktraceEntry {
196 /// A normal stack frame.
197 Frame(Frame),
198 /// A group of omitted frames from a specific crate.
199 OmittedFrames {
200 /// Number of omitted frames.
201 count: usize,
202 /// The name of the crate whose frames were omitted.
203 skipped_crate: &'static str,
204 },
205}
206
207/// A single stack frame in a backtrace.
208///
209/// Represents one function call in the call stack, including symbol information
210/// and source location if available.
211///
212/// # Examples
213///
214/// ```
215/// use rootcause_backtrace::{Backtrace, BacktraceEntry, BacktraceFilter};
216///
217/// if let Some(bt) = Backtrace::capture(&BacktraceFilter::DEFAULT) {
218/// for entry in &bt.entries {
219/// if let BacktraceEntry::Frame(frame) = entry {
220/// println!("{}", frame.sym_demangled);
221/// }
222/// }
223/// }
224/// ```
225#[derive(Debug, Clone)]
226pub struct Frame {
227 /// The demangled symbol name for this frame.
228 pub sym_demangled: String,
229 /// File path information for this frame, if available.
230 pub frame_path: Option<FramePath>,
231 /// Line number in the source file, if available.
232 pub lineno: Option<u32>,
233}
234
235/// File path information for a stack frame.
236///
237/// Contains the raw path and processed components for better display
238/// formatting.
239///
240/// # Examples
241///
242/// ```
243/// use rootcause_backtrace::{Backtrace, BacktraceEntry, BacktraceFilter};
244///
245/// if let Some(bt) = Backtrace::capture(&BacktraceFilter::DEFAULT) {
246/// for entry in &bt.entries {
247/// if let BacktraceEntry::Frame(frame) = entry {
248/// if let Some(path) = &frame.frame_path {
249/// println!("{}", path.raw_path);
250/// }
251/// }
252/// }
253/// }
254/// ```
255#[derive(Debug, Clone)]
256pub struct FramePath {
257 /// The raw file path from the debug information.
258 pub raw_path: String,
259 /// The crate name if detected from the path.
260 pub crate_name: Option<Cow<'static, str>>,
261 /// Common path prefix information for shortening display.
262 pub split_path: Option<FramePrefix>,
263}
264
265/// A common prefix for a frame path.
266///
267/// This struct represents a decomposed file path where a known prefix
268/// has been identified and separated from the rest of the path.
269///
270/// # Examples
271///
272/// ```
273/// use rootcause_backtrace::{Backtrace, BacktraceEntry, BacktraceFilter};
274///
275/// if let Some(bt) = Backtrace::capture(&BacktraceFilter::DEFAULT) {
276/// for entry in &bt.entries {
277/// if let BacktraceEntry::Frame(frame) = entry {
278/// if let Some(prefix) = frame
279/// .frame_path
280/// .as_ref()
281/// .and_then(|p| p.split_path.as_ref())
282/// {
283/// println!("[{}] {}", prefix.prefix_kind, prefix.suffix);
284/// }
285/// }
286/// }
287/// }
288/// ```
289#[derive(Debug, Clone)]
290pub struct FramePrefix {
291 /// The kind of prefix used to identify this prefix.
292 ///
293 /// Examples: `"RUST_SRC"` for Rust standard library paths,
294 /// `"CARGO"` for Cargo registry crate paths,
295 /// `"ROOTCAUSE"` for rootcause library paths.
296 pub prefix_kind: &'static str,
297 /// The full prefix path that was removed from the original path.
298 ///
299 /// Example: `"/home/user/.cargo/registry/src/index.crates.
300 /// io-1949cf8c6b5b557f"`
301 pub prefix: String,
302 /// The remaining path after the prefix was removed.
303 ///
304 /// Example: `"indexmap-2.12.1/src/map/core/entry.rs"`
305 pub suffix: String,
306}
307
308/// Handler for formatting [`Backtrace`] attachments.
309///
310/// The const generic `SHOW_FULL_PATH` controls whether file paths are shown
311/// in full or with common prefixes shortened.
312///
313/// # Examples
314///
315/// ```
316/// use rootcause::report_attachment::ReportAttachment;
317/// use rootcause_backtrace::{Backtrace, BacktraceFilter, BacktraceHandler};
318///
319/// let backtrace = Backtrace::capture(&BacktraceFilter::DEFAULT)
320/// .unwrap_or(Backtrace { entries: Vec::new(), total_omitted_frames: 0 });
321///
322/// // SHOW_FULL_PATH = false: shortened paths
323/// let _ = ReportAttachment::new_sendsync_custom::<BacktraceHandler<false>>(backtrace);
324/// ```
325#[derive(Copy, Clone)]
326pub struct BacktraceHandler<const SHOW_FULL_PATH: bool>;
327
328fn get_function_name(s: &str) -> &str {
329 let mut word_start = 0usize;
330 let mut word_end = 0usize;
331 let mut angle_nesting_level = 0u64;
332 let mut curly_nesting_level = 0u64;
333 let mut potential_function_arrow = false;
334 let mut inside_word = false;
335
336 for (i, c) in s.char_indices() {
337 if curly_nesting_level == 0 && angle_nesting_level == 0 {
338 if !inside_word && unicode_ident::is_xid_start(c) {
339 word_start = i;
340 inside_word = true;
341 } else if inside_word && !unicode_ident::is_xid_continue(c) {
342 word_end = i;
343 inside_word = false;
344 }
345 }
346
347 let was_potential_function_arrow = potential_function_arrow;
348 potential_function_arrow = c == '-';
349
350 if c == '<' {
351 angle_nesting_level = angle_nesting_level.saturating_add(1);
352 } else if c == '>' && !was_potential_function_arrow {
353 angle_nesting_level = angle_nesting_level.saturating_sub(1);
354 } else if c == '{' {
355 curly_nesting_level = curly_nesting_level.saturating_add(1);
356 if !inside_word && curly_nesting_level == 1 && angle_nesting_level == 0 {
357 word_start = i;
358 inside_word = true;
359 }
360 } else if c == '}' {
361 curly_nesting_level = curly_nesting_level.saturating_sub(1);
362 if inside_word && curly_nesting_level == 0 {
363 word_end = i + 1;
364 inside_word = false;
365 }
366 }
367 }
368
369 if word_start < word_end {
370 &s[word_start..word_end]
371 } else {
372 // We started at word start but never found an end; return rest of string
373 &s[word_start..]
374 }
375}
376
377impl<const SHOW_FULL_PATH: bool> AttachmentHandler<Backtrace> for BacktraceHandler<SHOW_FULL_PATH> {
378 fn display(value: &Backtrace, f: &mut fmt::Formatter<'_>) -> fmt::Result {
379 const MAX_UNWRAPPED_SYM_LENGTH: usize = 25;
380 let mut max_seen_length = 0;
381 for entry in &value.entries {
382 if let BacktraceEntry::Frame(frame) = entry {
383 let sym = get_function_name(&frame.sym_demangled);
384 if sym.len() <= MAX_UNWRAPPED_SYM_LENGTH && sym.len() > max_seen_length {
385 max_seen_length = sym.len();
386 }
387 }
388 }
389
390 for entry in &value.entries {
391 match entry {
392 BacktraceEntry::OmittedFrames {
393 count,
394 skipped_crate,
395 } => {
396 writeln!(
397 f,
398 "... omitted {count} frame(s) from crate '{skipped_crate}' ..."
399 )?;
400 continue;
401 }
402 BacktraceEntry::Frame(frame) => {
403 let sym = get_function_name(&frame.sym_demangled);
404
405 if sym.len() <= MAX_UNWRAPPED_SYM_LENGTH {
406 write!(f, "{:<max_seen_length$} - ", sym)?;
407 } else {
408 write!(f, "{sym}\n - ")?;
409 }
410
411 if let Some(path) = &frame.frame_path {
412 if SHOW_FULL_PATH {
413 write!(f, "{}", path.raw_path)?;
414 } else if let Some(split_path) = &path.split_path {
415 write!(f, "[..]/{}", split_path.suffix)?;
416 } else {
417 write!(f, "{}", path.raw_path)?;
418 }
419
420 if let Some(lineno) = frame.lineno {
421 write!(f, ":{lineno}")?;
422 }
423 }
424 writeln!(f)?;
425 }
426 }
427 }
428
429 if value.total_omitted_frames > 0 {
430 writeln!(
431 f,
432 "note: {} frame(s) omitted. For a complete backtrace, set RUST_BACKTRACE=full.",
433 value.total_omitted_frames
434 )?;
435 }
436
437 Ok(())
438 }
439
440 fn debug(value: &Backtrace, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
441 std::fmt::Debug::fmt(value, formatter)
442 }
443
444 fn preferred_formatting_style(
445 backtrace: &Backtrace,
446 _report_formatting_function: FormattingFunction,
447 ) -> AttachmentFormattingStyle {
448 AttachmentFormattingStyle {
449 placement: if backtrace.entries.is_empty() {
450 AttachmentFormattingPlacement::Hidden
451 } else {
452 AttachmentFormattingPlacement::InlineWithHeader {
453 header: "Backtrace",
454 }
455 },
456 // No reason to every print the Backtrace in the report
457 // as anything other than display.
458 function: FormattingFunction::Display,
459 priority: 10,
460 }
461 }
462}
463
464/// Attachment collector for capturing stack backtraces.
465///
466/// When registered as a report creation hook, this collector automatically
467/// captures the current stack backtrace and attaches it as a [`Backtrace`]
468/// attachment.
469///
470/// # Examples
471///
472/// Basic usage with default settings:
473///
474/// ```
475/// use rootcause::hooks::Hooks;
476/// use rootcause_backtrace::BacktraceCollector;
477///
478/// Hooks::new()
479/// .report_creation_hook(BacktraceCollector::new_from_env())
480/// .install()
481/// .expect("failed to install hooks");
482/// ```
483///
484/// Custom configuration:
485///
486/// ```
487/// use rootcause::hooks::Hooks;
488/// use rootcause_backtrace::{BacktraceCollector, BacktraceFilter};
489///
490/// let collector = BacktraceCollector {
491/// filter: BacktraceFilter {
492/// skipped_initial_crates: &[],
493/// skipped_middle_crates: &[],
494/// skipped_final_crates: &[],
495/// max_entry_count: 30,
496/// show_full_path: true,
497/// },
498/// capture_backtrace_for_reports_with_children: true,
499/// };
500///
501/// Hooks::new()
502/// .report_creation_hook(collector)
503/// .install()
504/// .expect("failed to install hooks");
505/// ```
506#[derive(Copy, Clone)]
507pub struct BacktraceCollector {
508 /// Configuration for filtering and formatting backtrace frames.
509 pub filter: BacktraceFilter,
510
511 /// If set to true, a backtrace is captured for every report creation,
512 /// including reports that have child reports (i.e., reports created with
513 /// existing children). If set to false, a backtrace is captured only
514 /// for reports created without any children. Reports created without
515 /// children always receive a backtrace regardless of this setting.
516 pub capture_backtrace_for_reports_with_children: bool,
517}
518
519/// Configuration for filtering frames from certain crates in a backtrace.
520///
521/// # Examples
522///
523/// Use default filtering:
524///
525/// ```
526/// use rootcause_backtrace::BacktraceFilter;
527///
528/// let filter = BacktraceFilter::DEFAULT;
529/// ```
530///
531/// Custom filtering to focus on application code:
532///
533/// ```
534/// use rootcause_backtrace::BacktraceFilter;
535///
536/// let filter = BacktraceFilter {
537/// // Hide rootcause crate frames at the start
538/// skipped_initial_crates: &["rootcause", "rootcause-backtrace"],
539/// // Hide framework frames in the middle
540/// skipped_middle_crates: &["tokio", "hyper", "tower"],
541/// // Hide runtime frames at the end
542/// skipped_final_crates: &["std", "tokio"],
543/// // Show only the most relevant 10 frames
544/// max_entry_count: 10,
545/// // Show shortened paths
546/// show_full_path: false,
547/// };
548/// ```
549#[derive(Copy, Clone, Debug)]
550pub struct BacktraceFilter {
551 /// Set of crate names whose frames should be hidden when they appear
552 /// at the beginning of a backtrace.
553 pub skipped_initial_crates: &'static [&'static str],
554 /// Set of crate names whose frames should be hidden when they appear
555 /// in the middle of a backtrace.
556 pub skipped_middle_crates: &'static [&'static str],
557 /// Set of crate names whose frames should be hidden when they appear
558 /// at the end of a backtrace.
559 pub skipped_final_crates: &'static [&'static str],
560 /// Maximum number of entries to include in the backtrace.
561 pub max_entry_count: usize,
562 /// Whether to show full file paths in the backtrace frames.
563 pub show_full_path: bool,
564}
565
566impl BacktraceFilter {
567 /// Default backtrace filter settings.
568 pub const DEFAULT: Self = Self {
569 skipped_initial_crates: &[
570 "backtrace",
571 "rootcause",
572 "rootcause-backtrace",
573 "core",
574 "std",
575 "alloc",
576 ],
577 skipped_middle_crates: &["std", "core", "alloc", "tokio"],
578 skipped_final_crates: &["std", "core", "alloc", "tokio"],
579 max_entry_count: 20,
580 show_full_path: false,
581 };
582}
583
584impl Default for BacktraceFilter {
585 fn default() -> Self {
586 Self::DEFAULT
587 }
588}
589
590#[derive(Debug)]
591struct RootcauseEnvOptions {
592 rust_backtrace_full: bool,
593 backtrace_leafs_only: bool,
594 show_full_path: bool,
595}
596
597impl RootcauseEnvOptions {
598 fn get() -> &'static Self {
599 static ROOTCAUSE_FLAGS: OnceLock<RootcauseEnvOptions> = OnceLock::new();
600
601 ROOTCAUSE_FLAGS.get_or_init(|| {
602 let rust_backtrace_full =
603 std::env::var_os("RUST_BACKTRACE").is_some_and(|var| var == "full");
604 let mut show_full_path = rust_backtrace_full;
605 let mut backtrace_leafs_only = false;
606 if let Some(var) = std::env::var_os("ROOTCAUSE_BACKTRACE") {
607 for v in var.to_string_lossy().split(',') {
608 if v.eq_ignore_ascii_case("leafs") {
609 backtrace_leafs_only = true;
610 } else if v.eq_ignore_ascii_case("full_paths") {
611 show_full_path = true;
612 }
613 }
614 }
615 RootcauseEnvOptions {
616 rust_backtrace_full,
617 backtrace_leafs_only,
618 show_full_path,
619 }
620 })
621 }
622}
623
624impl BacktraceCollector {
625 /// Creates a new [`BacktraceCollector`] with default settings.
626 ///
627 /// Configuration is controlled by environment variables. By default,
628 /// filtering is applied and backtraces are only captured for reports
629 /// without children.
630 ///
631 /// # Environment Variables
632 ///
633 /// - `RUST_BACKTRACE=full` - Disables all filtering and shows all frames
634 /// - `ROOTCAUSE_BACKTRACE` - Comma-separated options:
635 /// - `leafs` - Only capture backtraces for leaf errors (errors without
636 /// children)
637 /// - `full_paths` - Show full file paths instead of shortened paths
638 ///
639 /// The `RUST_BACKTRACE=full` setting implies `full_paths` unless explicitly
640 /// overridden by `ROOTCAUSE_BACKTRACE`.
641 ///
642 /// # Examples
643 ///
644 /// ```
645 /// use rootcause::hooks::Hooks;
646 /// use rootcause_backtrace::BacktraceCollector;
647 ///
648 /// // Respects RUST_BACKTRACE and ROOTCAUSE_BACKTRACE environment variables
649 /// Hooks::new()
650 /// .report_creation_hook(BacktraceCollector::new_from_env())
651 /// .install()
652 /// .expect("failed to install hooks");
653 /// ```
654 pub fn new_from_env() -> Self {
655 let env_options = RootcauseEnvOptions::get();
656 let capture_backtrace_for_reports_with_children = !env_options.backtrace_leafs_only;
657
658 Self {
659 filter: if env_options.rust_backtrace_full {
660 BacktraceFilter {
661 skipped_initial_crates: &[],
662 skipped_middle_crates: &[],
663 skipped_final_crates: &[],
664 max_entry_count: usize::MAX,
665 show_full_path: env_options.show_full_path,
666 }
667 } else {
668 BacktraceFilter {
669 show_full_path: env_options.show_full_path,
670 ..BacktraceFilter::DEFAULT
671 }
672 },
673 capture_backtrace_for_reports_with_children,
674 }
675 }
676}
677
678impl ReportCreationHook for BacktraceCollector {
679 fn on_local_creation(&self, mut report: ReportMut<'_, Dynamic, markers::Local>) {
680 let do_capture =
681 self.capture_backtrace_for_reports_with_children || report.children().is_empty();
682 if do_capture && let Some(backtrace) = Backtrace::capture(&self.filter) {
683 let attachment = if self.filter.show_full_path {
684 ReportAttachment::new_custom::<BacktraceHandler<true>>(backtrace)
685 } else {
686 ReportAttachment::new_custom::<BacktraceHandler<false>>(backtrace)
687 };
688 report.attachments_mut().push(attachment.into_dynamic());
689 }
690 }
691
692 fn on_sendsync_creation(&self, mut report: ReportMut<'_, Dynamic, markers::SendSync>) {
693 let do_capture =
694 self.capture_backtrace_for_reports_with_children || report.children().is_empty();
695 if do_capture && let Some(backtrace) = Backtrace::capture(&self.filter) {
696 let attachment = if self.filter.show_full_path {
697 ReportAttachment::new_custom::<BacktraceHandler<true>>(backtrace)
698 } else {
699 ReportAttachment::new_custom::<BacktraceHandler<false>>(backtrace)
700 };
701 report.attachments_mut().push(attachment.into_dynamic());
702 }
703 }
704}
705
706const fn get_rootcause_backtrace_matcher(
707 location: &'static Location<'static>,
708) -> Option<(&'static str, usize)> {
709 let file = location.file();
710
711 let Some(prefix_len) = file.len().checked_sub("/src/lib.rs".len()) else {
712 return None;
713 };
714
715 let (prefix, suffix) = file.split_at(prefix_len);
716 // Detect the path separator from the actual Location::caller() path rather
717 // than from a build-script-generated value. In cross-compilation environments
718 // the build script and compiler may run on different platforms, so the build
719 // script's idea of the host separator may not match what Location::caller()
720 // produces.
721 let sep = suffix.as_bytes()[0];
722 if sep == b'/' {
723 assert!(suffix.eq_ignore_ascii_case("/src/lib.rs"));
724 } else {
725 assert!(suffix.eq_ignore_ascii_case(r#"\src\lib.rs"#));
726 }
727
728 let (matcher_prefix, _) = file.split_at(prefix_len + 4);
729
730 let mut splitter_prefix = prefix;
731 while !splitter_prefix.is_empty() {
732 let (new_prefix, last_char) = splitter_prefix.split_at(splitter_prefix.len() - 1);
733 splitter_prefix = new_prefix;
734 if last_char.as_bytes()[0] == sep {
735 break;
736 }
737 }
738
739 Some((matcher_prefix, splitter_prefix.len()))
740}
741
742const ROOTCAUSE_BACKTRACE_MATCHER: Option<(&str, usize)> =
743 get_rootcause_backtrace_matcher(Location::caller());
744const ROOTCAUSE_MATCHER: Option<(&str, usize)> =
745 get_rootcause_backtrace_matcher(rootcause::__private::ROOTCAUSE_LOCATION);
746
747impl Backtrace {
748 /// Captures the current stack backtrace, applying optional filtering.
749 ///
750 /// Returns `None` if a backtrace could not be captured.
751 ///
752 /// # Examples
753 ///
754 /// ```
755 /// use rootcause_backtrace::{Backtrace, BacktraceFilter};
756 ///
757 /// if let Some(bt) = Backtrace::capture(&BacktraceFilter::DEFAULT) {
758 /// println!("Captured {} frames", bt.entries.len());
759 /// }
760 /// ```
761 pub fn capture(filter: &BacktraceFilter) -> Option<Self> {
762 let mut initial_filtering = !filter.skipped_initial_crates.is_empty();
763 let mut entries: Vec<BacktraceEntry> = Vec::new();
764 let mut total_omitted_frames = 0;
765
766 let mut delayed_omitted_frame: Option<Frame> = None;
767 let mut currently_omitted_crate_name: Option<&'static str> = None;
768 let mut currently_omitted_frames = 0;
769
770 backtrace::trace(|frame| {
771 backtrace::resolve_frame(frame, |symbol| {
772 // Don't consider frames without symbol names or filenames.
773 let (Some(sym), Some(filename_raw)) = (symbol.name(), symbol.filename_raw()) else {
774 return;
775 };
776
777 if entries.len() >= filter.max_entry_count {
778 total_omitted_frames += 1;
779 return;
780 }
781
782 let frame_path = FramePath::new(filename_raw);
783
784 if initial_filtering {
785 if let Some(cur_crate_name) = &frame_path.crate_name
786 && filter.skipped_initial_crates.contains(&&**cur_crate_name)
787 {
788 total_omitted_frames += 1;
789 return;
790 } else {
791 initial_filtering = false;
792 }
793 }
794
795 if let Some(cur_crate_name) = &frame_path.crate_name
796 && let Some(currently_omitted_crate_name) = ¤tly_omitted_crate_name
797 && cur_crate_name == currently_omitted_crate_name
798 {
799 delayed_omitted_frame = None;
800 currently_omitted_frames += 1;
801 total_omitted_frames += 1;
802 return;
803 }
804
805 if let Some(currently_omitted_crate_name) = currently_omitted_crate_name.take() {
806 if let Some(delayed_frame) = delayed_omitted_frame.take() {
807 entries.push(BacktraceEntry::Frame(delayed_frame));
808 } else {
809 entries.push(BacktraceEntry::OmittedFrames {
810 count: currently_omitted_frames,
811 skipped_crate: currently_omitted_crate_name,
812 });
813 }
814 currently_omitted_frames = 0;
815 }
816
817 if let Some(cur_crate_name) = &frame_path.crate_name
818 && let Some(skipped_crate) = filter
819 .skipped_middle_crates
820 .iter()
821 .find(|&crate_name| crate_name == cur_crate_name)
822 {
823 currently_omitted_crate_name = Some(skipped_crate);
824 currently_omitted_frames = 1;
825 total_omitted_frames += 1;
826 delayed_omitted_frame = Some(Frame {
827 sym_demangled: format!("{sym:#}"),
828 frame_path: Some(frame_path),
829 lineno: symbol.lineno(),
830 });
831 return;
832 }
833
834 entries.push(BacktraceEntry::Frame(Frame {
835 sym_demangled: format!("{sym:#}"),
836 frame_path: Some(frame_path),
837 lineno: symbol.lineno(),
838 }));
839 });
840
841 true
842 });
843
844 if let Some(currently_omitted_crate_name) = currently_omitted_crate_name.take() {
845 if let Some(delayed_frame) = delayed_omitted_frame.take() {
846 entries.push(BacktraceEntry::Frame(delayed_frame));
847 } else {
848 entries.push(BacktraceEntry::OmittedFrames {
849 count: currently_omitted_frames,
850 skipped_crate: currently_omitted_crate_name,
851 });
852 }
853 }
854
855 while let Some(last) = entries.last() {
856 match last {
857 BacktraceEntry::Frame(frame) => {
858 let mut skip = false;
859 if let Some(frame_path) = &frame.frame_path
860 && let Some(crate_name) = &frame_path.crate_name
861 && filter.skipped_final_crates.contains(&&**crate_name)
862 {
863 skip = true;
864 } else if frame.sym_demangled == "__libc_start_call_main"
865 || frame.sym_demangled == "__libc_start_main_impl"
866 {
867 skip = true;
868 } else if let Some(frame_path) = &frame.frame_path
869 && frame.sym_demangled == "_start"
870 && frame_path.raw_path.contains("zig/libc/glibc")
871 {
872 skip = true;
873 }
874
875 if skip {
876 total_omitted_frames += 1;
877 entries.pop();
878 } else {
879 break;
880 }
881 }
882 BacktraceEntry::OmittedFrames {
883 skipped_crate,
884 count,
885 } => {
886 if filter.skipped_final_crates.contains(skipped_crate) {
887 total_omitted_frames += count;
888 entries.pop();
889 } else {
890 break;
891 }
892 }
893 }
894 }
895
896 if entries.is_empty() && total_omitted_frames == 0 {
897 None
898 } else {
899 Some(Self {
900 entries,
901 total_omitted_frames,
902 })
903 }
904 }
905}
906
907/// Matches Rust standard library source paths and returns the crate name and byte offset.
908///
909/// Recognised paths:
910/// - `/lib/rustlib/src/rust/library/{std|core|alloc}/src/…`
911/// - `^/rustc/{40-hex-char hash}/library/{std|core|alloc}/src/…`
912fn match_std_library_path(path: &str) -> Option<(&'static str, usize)> {
913 const STD_CRATES: [&str; 3] = ["std", "core", "alloc"];
914
915 let (prefix, after_library) = path.split_once("/library/")?;
916
917 // The prefix must end with "/lib/rustlib/src/rust" or *be* "/rustc/{40 hex chars}"
918 let valid_prefix = prefix.ends_with("/lib/rustlib/src/rust")
919 || prefix
920 .strip_prefix("/rustc/")
921 .is_some_and(|hash| hash.len() == 40 && hash.bytes().all(|b| b.is_ascii_hexdigit()));
922
923 if !valid_prefix {
924 return None;
925 }
926
927 // after_library is "{std|core|alloc}/src/…"; split on "/src/" to isolate the crate name.
928 let (crate_name, _) = after_library.split_once("/src/")?;
929 let crate_name = STD_CRATES
930 .iter()
931 .copied()
932 .find(|&name| name == crate_name)?;
933
934 let crate_start = prefix.len() + "/library/".len();
935 Some((crate_name, crate_start))
936}
937
938/// Matches Cargo registry source paths and returns the crate name and its byte offset.
939///
940/// Recognised paths:
941/// - `/.cargo/registry/src/{index}-{16-hex-char hash}/{crate}-{version}/src/…`
942fn match_cargo_registry_path(path: &str) -> Option<(&str, usize)> {
943 let (before_registry, after_registry) = path.split_once("/registry/src/")?;
944
945 // Consume the "{index}-{16-hex-char}" directory component.
946 let (index_hash, after_index) = after_registry.split_once('/')?;
947 let (_, hash) = index_hash.rsplit_once('-')?;
948 if hash.len() != 16 || !hash.bytes().all(|b| b.is_ascii_hexdigit()) {
949 return None;
950 }
951
952 // after_index is "{crate}-{version}/src/…"; split on "/src/" to isolate "{crate}-{version}".
953 let (crate_version, _) = after_index.split_once("/src/")?;
954
955 // create names + version numbers can be really silly. However crate names can't contain dots
956 // So find the first dot and split (crate-name-0.1.0-alpha into crate-name-0)
957 // Then right split on dash (crate-name-0 to crate-name)
958 let (crate_name, _) = crate_version.split_once('.')?;
959 let (crate_name, _) = crate_name.rsplit_once('-')?;
960
961 let crate_start_abs = before_registry.len() + "/registry/src/".len() + index_hash.len() + 1;
962 Some((crate_name, crate_start_abs))
963}
964
965impl FramePath {
966 fn new(path: BytesOrWideString<'_>) -> Self {
967 let path_str = path.to_string();
968
969 if let Some((crate_name, crate_start)) = match_std_library_path(&path_str) {
970 let raw_path = path.to_str_lossy().into_owned();
971 let (prefix, suffix) = (&path_str[..crate_start - 1], &path_str[crate_start..]);
972
973 Self {
974 raw_path,
975 split_path: Some(FramePrefix {
976 prefix_kind: "RUST_SRC",
977 prefix: prefix.to_string(),
978 suffix: suffix.to_string(),
979 }),
980 crate_name: Some(crate_name.to_string().into()),
981 }
982 } else if let Some((crate_name, crate_start)) = match_cargo_registry_path(&path_str) {
983 let raw_path = path.to_str_lossy().into_owned();
984 let (prefix, suffix) = (&path_str[..crate_start - 1], &path_str[crate_start..]);
985
986 Self {
987 raw_path,
988 crate_name: Some(crate_name.to_string().into()),
989 split_path: Some(FramePrefix {
990 prefix_kind: "CARGO",
991 prefix: prefix.to_string(),
992 suffix: suffix.to_string(),
993 }),
994 }
995 } else if let Some((rootcause_matcher_prefix, rootcause_splitter_prefix_len)) =
996 ROOTCAUSE_MATCHER
997 && path_str.starts_with(rootcause_matcher_prefix)
998 {
999 let raw_path = path.to_str_lossy().into_owned();
1000 let (prefix, suffix) = (
1001 &path_str[..rootcause_splitter_prefix_len],
1002 &path_str[rootcause_splitter_prefix_len + 1..],
1003 );
1004 Self {
1005 raw_path,
1006 split_path: Some(FramePrefix {
1007 prefix_kind: "ROOTCAUSE",
1008 prefix: prefix.to_string(),
1009 suffix: suffix.to_string(),
1010 }),
1011 crate_name: Some(Cow::Borrowed("rootcause")),
1012 }
1013 } else if let Some((rootcause_matcher_prefix, rootcause_splitter_prefix_len)) =
1014 ROOTCAUSE_BACKTRACE_MATCHER
1015 && path_str.starts_with(rootcause_matcher_prefix)
1016 {
1017 let raw_path = path.to_str_lossy().into_owned();
1018 let (prefix, suffix) = (
1019 &path_str[..rootcause_splitter_prefix_len],
1020 &path_str[rootcause_splitter_prefix_len + 1..],
1021 );
1022 Self {
1023 raw_path,
1024 split_path: Some(FramePrefix {
1025 prefix_kind: "ROOTCAUSE",
1026 prefix: prefix.to_string(),
1027 suffix: suffix.to_string(),
1028 }),
1029 crate_name: Some(Cow::Borrowed("rootcause-backtrace")),
1030 }
1031 } else {
1032 let raw_path = path.to_str_lossy().into_owned();
1033 Self {
1034 raw_path,
1035 crate_name: None,
1036 split_path: None,
1037 }
1038 }
1039 }
1040}
1041
1042/// Extension trait for attaching backtraces to reports.
1043///
1044/// This trait provides methods to easily attach a captured backtrace to a
1045/// report or to the error contained within a `Result`.
1046///
1047/// # Examples
1048///
1049/// Attach backtrace to a report:
1050///
1051/// ```
1052/// use std::io;
1053///
1054/// use rootcause::report;
1055/// use rootcause_backtrace::BacktraceExt;
1056///
1057/// let report = report!(io::Error::other("An error occurred")).attach_backtrace();
1058/// ```
1059///
1060/// Attach backtrace to a `Result`:
1061///
1062/// ```
1063/// use std::io;
1064///
1065/// use rootcause::{Report, report};
1066/// use rootcause_backtrace::BacktraceExt;
1067///
1068/// fn might_fail() -> Result<(), Report> {
1069/// Err(report!(io::Error::other("operation failed")).into_dynamic())
1070/// }
1071///
1072/// let result = might_fail().attach_backtrace();
1073/// ```
1074///
1075/// Use a custom filter:
1076///
1077/// ```
1078/// use std::io;
1079///
1080/// use rootcause::report;
1081/// use rootcause_backtrace::{BacktraceExt, BacktraceFilter};
1082///
1083/// let filter = BacktraceFilter {
1084/// skipped_initial_crates: &[],
1085/// skipped_middle_crates: &[],
1086/// skipped_final_crates: &[],
1087/// max_entry_count: 50,
1088/// show_full_path: true,
1089/// };
1090///
1091/// let report = report!(io::Error::other("detailed error")).attach_backtrace_with_filter(&filter);
1092/// ```
1093pub trait BacktraceExt: Sized {
1094 /// Attaches a captured backtrace to the report using the default filter.
1095 ///
1096 /// # Examples
1097 ///
1098 /// ```
1099 /// use std::io;
1100 ///
1101 /// use rootcause::report;
1102 /// use rootcause_backtrace::BacktraceExt;
1103 ///
1104 /// let report = report!(io::Error::other("error")).attach_backtrace();
1105 /// ```
1106 fn attach_backtrace(self) -> Self {
1107 self.attach_backtrace_with_filter(&BacktraceFilter::DEFAULT)
1108 }
1109
1110 /// Attaches a captured backtrace to the report using the specified filter.
1111 ///
1112 /// # Examples
1113 ///
1114 /// ```
1115 /// use std::io;
1116 ///
1117 /// use rootcause::report;
1118 /// use rootcause_backtrace::{BacktraceExt, BacktraceFilter};
1119 ///
1120 /// let filter = BacktraceFilter {
1121 /// max_entry_count: 10,
1122 /// ..BacktraceFilter::DEFAULT
1123 /// };
1124 ///
1125 /// let report = report!(io::Error::other("error")).attach_backtrace_with_filter(&filter);
1126 /// ```
1127 fn attach_backtrace_with_filter(self, filter: &BacktraceFilter) -> Self;
1128}
1129
1130impl<C: ?Sized, T> BacktraceExt for Report<C, markers::Mutable, T>
1131where
1132 Backtrace: ObjectMarkerFor<T>,
1133{
1134 fn attach_backtrace_with_filter(mut self, filter: &BacktraceFilter) -> Self {
1135 if let Some(backtrace) = Backtrace::capture(filter) {
1136 if filter.show_full_path {
1137 self = self.attach_custom::<BacktraceHandler<true>, _>(backtrace);
1138 } else {
1139 self = self.attach_custom::<BacktraceHandler<false>, _>(backtrace);
1140 }
1141 }
1142 self
1143 }
1144}
1145
1146impl<C: ?Sized, V, T> BacktraceExt for Result<V, Report<C, markers::Mutable, T>>
1147where
1148 Backtrace: ObjectMarkerFor<T>,
1149{
1150 fn attach_backtrace_with_filter(self, filter: &BacktraceFilter) -> Self {
1151 match self {
1152 Ok(v) => Ok(v),
1153 Err(report) => Err(report.attach_backtrace_with_filter(filter)),
1154 }
1155 }
1156}
1157
1158#[cfg(test)]
1159mod tests {
1160 use super::*;
1161
1162 // ── match_std_library_path ────────────────────────────────────────────────
1163
1164 #[test]
1165 fn std_path_rustlib_std() {
1166 let path = "/home/user/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/std/src/io/mod.rs";
1167 let (name, pos) = match_std_library_path(path).expect("should match");
1168 assert_eq!(name, "std");
1169 assert_eq!(&path[pos..], "std/src/io/mod.rs");
1170 }
1171
1172 #[test]
1173 fn std_path_rustlib_core() {
1174 let path = "/home/user/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/option.rs";
1175 let (name, pos) = match_std_library_path(path).expect("should match");
1176 assert_eq!(name, "core");
1177 assert_eq!(&path[pos..], "core/src/option.rs");
1178 }
1179
1180 #[test]
1181 fn std_path_rustlib_alloc() {
1182 let path = "/home/user/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/alloc/src/vec/mod.rs";
1183 let (name, pos) = match_std_library_path(path).expect("should match");
1184 assert_eq!(name, "alloc");
1185 assert_eq!(&path[pos..], "alloc/src/vec/mod.rs");
1186 }
1187
1188 #[test]
1189 fn std_path_rustc_hash() {
1190 let hash = "a".repeat(40);
1191 let path = format!("/rustc/{hash}/library/std/src/panicking.rs");
1192 let (name, pos) = match_std_library_path(&path).expect("should match");
1193 assert_eq!(name, "std");
1194 assert_eq!(&path[pos..], "std/src/panicking.rs");
1195 }
1196
1197 #[test]
1198 fn std_path_no_match_unknown_crate() {
1199 let path = "/lib/rustlib/src/rust/library/unknown/src/lib.rs";
1200 assert!(match_std_library_path(path).is_none());
1201 }
1202
1203 #[test]
1204 fn std_path_no_match_rustc_hash_too_short() {
1205 let short_hash = "a".repeat(39);
1206 let path = format!("/rustc/{short_hash}/library/std/src/lib.rs");
1207 assert!(match_std_library_path(&path).is_none());
1208 }
1209
1210 #[test]
1211 fn std_path_no_match_rustc_hash_not_at_root() {
1212 let hash = "a".repeat(40);
1213 let path = format!("/prefix/rustc/{hash}/library/std/src/lib.rs");
1214 assert!(match_std_library_path(&path).is_none());
1215 }
1216
1217 #[test]
1218 fn std_path_no_match_rustc_hash_non_hex() {
1219 let hash = "z".repeat(40);
1220 let path = format!("/rustc/{hash}/library/std/src/lib.rs");
1221 assert!(match_std_library_path(&path).is_none());
1222 }
1223
1224 #[test]
1225 fn std_path_no_match_missing_src_segment() {
1226 let path = "/lib/rustlib/src/rust/library/std/nosrc/io.rs";
1227 assert!(match_std_library_path(path).is_none());
1228 }
1229
1230 #[test]
1231 fn std_path_no_match_cargo_registry_path() {
1232 // A cargo registry path should not match the std matcher.
1233 let path = "/home/user/.cargo/registry/src/index.crates.io-6f17d22bba15001f/serde-1.0.210/src/lib.rs";
1234 assert!(match_std_library_path(path).is_none());
1235 }
1236
1237 // ── match_cargo_registry_path ─────────────────────────────────────────────
1238
1239 #[test]
1240 fn cargo_path_simple_crate() {
1241 let path = "/home/user/.cargo/registry/src/index.crates.io-6f17d22bba15001f/serde-1.0.210/src/lib.rs";
1242 let (name, pos) = match_cargo_registry_path(path).expect("should match");
1243 assert_eq!(name, "serde");
1244 assert_eq!(&path[pos..], "serde-1.0.210/src/lib.rs");
1245 }
1246
1247 #[test]
1248 fn cargo_path_hyphenated_crate_name() {
1249 let path = "/home/user/.cargo/registry/src/index.crates.io-6f17d22bba15001f/my-cool-crate-0.3.1/src/lib.rs";
1250 let (name, pos) = match_cargo_registry_path(path).expect("should match");
1251 assert_eq!(name, "my-cool-crate");
1252 assert_eq!(&path[pos..], "my-cool-crate-0.3.1/src/lib.rs");
1253 }
1254
1255 #[test]
1256 fn cargo_path_prerelease_version() {
1257 let path = "/home/user/.cargo/registry/src/index.crates.io-6f17d22bba15001f/cratename-1.0.0-2beta/src/lib.rs";
1258 let (name, pos) = match_cargo_registry_path(path).expect("should match");
1259 assert_eq!(name, "cratename");
1260 assert_eq!(&path[pos..], "cratename-1.0.0-2beta/src/lib.rs");
1261 }
1262
1263 #[test]
1264 fn cargo_path_no_match_hash_too_short() {
1265 let path = "/home/user/.cargo/registry/src/index.crates.io-6f17d22bba1500/serde-1.0.210/src/lib.rs";
1266 assert!(match_cargo_registry_path(path).is_none());
1267 }
1268
1269 #[test]
1270 fn cargo_path_no_match_hash_non_hex() {
1271 let path = "/home/user/.cargo/registry/src/index.crates.io-zzzzzzzzzzzzzzzz/serde-1.0.210/src/lib.rs";
1272 assert!(match_cargo_registry_path(path).is_none());
1273 }
1274
1275 #[test]
1276 fn cargo_path_no_match_missing_src_segment() {
1277 let path = "/home/user/.cargo/registry/src/index.crates.io-6f17d22bba15001f/serde-1.0.210/nosrc/lib.rs";
1278 assert!(match_cargo_registry_path(path).is_none());
1279 }
1280
1281 #[test]
1282 fn cargo_path_no_match_no_version() {
1283 let path =
1284 "/home/user/.cargo/registry/src/index.crates.io-6f17d22bba15001f/serde/src/lib.rs";
1285 assert!(match_cargo_registry_path(path).is_none());
1286 }
1287
1288 #[test]
1289 fn cargo_path_no_match_std_library_path() {
1290 let path = "/lib/rustlib/src/rust/library/std/src/io/mod.rs";
1291 assert!(match_cargo_registry_path(path).is_none());
1292 }
1293}