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
11//! Stack backtrace attachment collector for rootcause error reports.
12//!
13//! This crate provides functionality to automatically capture and attach stack
14//! backtraces to error reports. This is useful for debugging to see the call
15//! stack that led to an error.
16//!
17//! # Quick Start
18//!
19//! ## Using Hooks (Automatic for All Errors)
20//!
21//! Register a backtrace collector as a hook to automatically capture backtraces
22//! for all errors:
23//!
24//! ```
25//! use rootcause::hooks::Hooks;
26//! use rootcause_backtrace::BacktraceCollector;
27//!
28//! // Capture backtraces for all errors
29//! Hooks::new()
30//! .report_creation_hook(BacktraceCollector::new_from_env())
31//! .install()
32//! .expect("failed to install hooks");
33//!
34//! // Now all errors automatically get backtraces!
35//! fn example() -> rootcause::Report {
36//! rootcause::report!("something went wrong")
37//! }
38//! println!("{}", example().context("additional context"));
39//! ```
40//!
41//! This will print a backtrace similar to the following:
42//! ```text
43//! ● additional context
44//! ├ src/main.rs:12
45//! ├ Backtrace
46//! │ │ main - /build/src/main.rs:12
47//! │ │ note: 39 frame(s) omitted. For a complete backtrace, set RUST_BACKTRACE=full.
48//! │ ╰─
49//! │
50//! ● something went wrong
51//! ├ src/main.rs:10
52//! ╰ Backtrace
53//! │ example - /build/src/main.rs:10
54//! │ main - /build/src/main.rs:12
55//! │ note: 40 frame(s) omitted. For a complete backtrace, set RUST_BACKTRACE=full.
56//! ╰─
57//! ```
58//!
59//! ## Manual Attachment (Per-Error)
60//!
61//! Attach backtraces to specific errors using the extension trait:
62//!
63//! ```
64//! use std::io;
65//!
66//! use rootcause::{Report, report};
67//! use rootcause_backtrace::BacktraceExt;
68//!
69//! fn operation() -> Result<(), Report> {
70//! Err(report!("operation failed"))
71//! }
72//!
73//! // Attach backtrace to the error in the Result
74//! let result = operation().attach_backtrace();
75//! ```
76//!
77//! # Environment Variables
78//!
79//! - `RUST_BACKTRACE=full` - Disables filtering and shows full paths
80//! - `ROOTCAUSE_BACKTRACE` - Comma-separated options:
81//! - `leafs` - Only capture backtraces for leaf errors (errors without
82//! children)
83//! - `full_paths` - Show full file paths in backtraces
84//!
85//! # Path privacy
86//!
87//! By default, backtrace paths are shortened paths for improved readability,
88//! but this may still expose private file system structure when a path is not
89//! recognized as belonging to a known prefix (e.g., RUST_SRC).
90//!
91//! If exposing private file system paths is a concern, then we recommend using
92//! the `--remap-path-prefix` option of `rustc` to remap source paths to
93//! generic placeholders.
94//!
95//! A good default way to handle this is to set the following environment
96//! variables when building your application for release:
97//!
98//! ```sh
99//! export RUSTFLAGS="--remap-path-prefix=$HOME=/home/user --remap-path-prefix=$PWD=/build"
100//! ```
101//!
102//! # Debugging symbols in release builds
103//!
104//! To ensure that backtraces contain useful symbol and source location
105//! information in release builds, make sure to enable debug symbols in your
106//! `Cargo.toml`:
107//!
108//! ```toml
109//! [profile.release]
110//! strip = false
111//! # You can also set this to "line-tables-only" for smaller binaries
112//! debug = true
113//! ```
114//!
115//! # Filtering
116//!
117//! Control which frames appear in backtraces:
118//!
119//! ```
120//! use rootcause_backtrace::{BacktraceCollector, BacktraceFilter};
121//!
122//! let collector = BacktraceCollector {
123//! filter: BacktraceFilter {
124//! skipped_initial_crates: &["rootcause", "rootcause-backtrace"], // Skip frames from rootcause at start
125//! skipped_middle_crates: &["tokio"], // Skip tokio frames in middle
126//! skipped_final_crates: &["std"], // Skip std frames at end
127//! max_entry_count: 15, // Limit to 15 frames
128//! show_full_path: false, // Show shortened paths
129//! },
130//! capture_backtrace_for_reports_with_children: false, // Only leaf errors
131//! };
132//! ```
133
134use std::{borrow::Cow, fmt, panic::Location, sync::OnceLock};
135
136use backtrace::BytesOrWideString;
137use rootcause::{
138 Report, ReportMut,
139 handlers::{
140 AttachmentFormattingPlacement, AttachmentFormattingStyle, AttachmentHandler,
141 FormattingFunction,
142 },
143 hooks::report_creation::ReportCreationHook,
144 markers::{self, Dynamic, ObjectMarkerFor},
145 report_attachment::ReportAttachment,
146};
147
148/// Stack backtrace information.
149///
150/// Contains a collection of stack frames representing the call stack
151/// at the point where a report was created.
152///
153/// # Examples
154///
155/// Capture a backtrace manually:
156///
157/// ```
158/// use rootcause_backtrace::{Backtrace, BacktraceFilter};
159///
160/// let backtrace = Backtrace::capture(&BacktraceFilter::DEFAULT);
161/// if let Some(bt) = backtrace {
162/// println!("Captured {} frames", bt.entries.len());
163/// }
164/// ```
165#[derive(Debug, Clone)]
166pub struct Backtrace {
167 /// The entries in the backtrace, ordered from most recent to oldest.
168 pub entries: Vec<BacktraceEntry>,
169 /// Total number of frames that were omitted due to filtering.
170 pub total_omitted_frames: usize,
171}
172
173/// A single entry in a stack backtrace.
174#[derive(Debug, Clone)]
175pub enum BacktraceEntry {
176 /// A normal stack frame.
177 Frame(Frame),
178 /// A group of omitted frames from a specific crate.
179 OmittedFrames {
180 /// Number of omitted frames.
181 count: usize,
182 /// The name of the crate whose frames were omitted.
183 skipped_crate: &'static str,
184 },
185}
186
187/// A single stack frame in a backtrace.
188///
189/// Represents one function call in the call stack, including symbol information
190/// and source location if available.
191#[derive(Debug, Clone)]
192pub struct Frame {
193 /// The demangled symbol name for this frame.
194 pub sym_demangled: String,
195 /// File path information for this frame, if available.
196 pub frame_path: Option<FramePath>,
197 /// Line number in the source file, if available.
198 pub lineno: Option<u32>,
199}
200
201/// File path information for a stack frame.
202///
203/// Contains the raw path and processed components for better display
204/// formatting.
205#[derive(Debug, Clone)]
206pub struct FramePath {
207 /// The raw file path from the debug information.
208 pub raw_path: String,
209 /// The crate name if detected from the path.
210 pub crate_name: Option<Cow<'static, str>>,
211 /// Common path prefix information for shortening display.
212 pub split_path: Option<FramePrefix>,
213}
214
215/// A common prefix for a frame path.
216///
217/// This struct represents a decomposed file path where a known prefix
218/// has been identified and separated from the rest of the path.
219#[derive(Debug, Clone)]
220pub struct FramePrefix {
221 /// The kind of prefix used to identify this prefix.
222 ///
223 /// Examples: `"RUST_SRC"` for Rust standard library paths,
224 /// `"CARGO"` for Cargo registry crate paths,
225 /// `"ROOTCAUSE"` for rootcause library paths.
226 pub prefix_kind: &'static str,
227 /// The full prefix path that was removed from the original path.
228 ///
229 /// Example: `"/home/user/.cargo/registry/src/index.crates.
230 /// io-1949cf8c6b5b557f"`
231 pub prefix: String,
232 /// The remaining path after the prefix was removed.
233 ///
234 /// Example: `"indexmap-2.12.1/src/map/core/entry.rs"`
235 pub suffix: String,
236}
237
238/// Handler for formatting [`Backtrace`] attachments.
239#[derive(Copy, Clone)]
240pub struct BacktraceHandler<const SHOW_FULL_PATH: bool>;
241
242fn get_function_name(s: &str) -> &str {
243 let mut word_start = 0usize;
244 let mut word_end = 0usize;
245 let mut angle_nesting_level = 0u64;
246 let mut curly_nesting_level = 0u64;
247 let mut potential_function_arrow = false;
248 let mut inside_word = false;
249
250 for (i, c) in s.char_indices() {
251 if curly_nesting_level == 0 && angle_nesting_level == 0 {
252 if !inside_word && unicode_ident::is_xid_start(c) {
253 word_start = i;
254 inside_word = true;
255 } else if inside_word && !unicode_ident::is_xid_continue(c) {
256 word_end = i;
257 inside_word = false;
258 }
259 }
260
261 let was_potential_function_arrow = potential_function_arrow;
262 potential_function_arrow = c == '-';
263
264 if c == '<' {
265 angle_nesting_level = angle_nesting_level.saturating_add(1);
266 } else if c == '>' && !was_potential_function_arrow {
267 angle_nesting_level = angle_nesting_level.saturating_sub(1);
268 } else if c == '{' {
269 curly_nesting_level = curly_nesting_level.saturating_add(1);
270 if !inside_word && curly_nesting_level == 1 && angle_nesting_level == 0 {
271 word_start = i;
272 inside_word = true;
273 }
274 } else if c == '}' {
275 curly_nesting_level = curly_nesting_level.saturating_sub(1);
276 if inside_word && curly_nesting_level == 0 {
277 word_end = i + 1;
278 inside_word = false;
279 }
280 }
281 }
282
283 if word_start < word_end {
284 &s[word_start..word_end]
285 } else {
286 // We started at word start but never found an end; return rest of string
287 &s[word_start..]
288 }
289}
290
291impl<const SHOW_FULL_PATH: bool> AttachmentHandler<Backtrace> for BacktraceHandler<SHOW_FULL_PATH> {
292 fn display(value: &Backtrace, f: &mut fmt::Formatter<'_>) -> fmt::Result {
293 const MAX_UNWRAPPED_SYM_LENGTH: usize = 25;
294 let mut max_seen_length = 0;
295 for entry in &value.entries {
296 if let BacktraceEntry::Frame(frame) = entry {
297 let sym = get_function_name(&frame.sym_demangled);
298 if sym.len() <= MAX_UNWRAPPED_SYM_LENGTH && sym.len() > max_seen_length {
299 max_seen_length = sym.len();
300 }
301 }
302 }
303
304 for entry in &value.entries {
305 match entry {
306 BacktraceEntry::OmittedFrames {
307 count,
308 skipped_crate,
309 } => {
310 writeln!(
311 f,
312 "... omitted {count} frame(s) from crate '{skipped_crate}' ..."
313 )?;
314 continue;
315 }
316 BacktraceEntry::Frame(frame) => {
317 let sym = get_function_name(&frame.sym_demangled);
318
319 if sym.len() <= MAX_UNWRAPPED_SYM_LENGTH {
320 write!(f, "{:<max_seen_length$} - ", sym)?;
321 } else {
322 write!(f, "{sym}\n - ")?;
323 }
324
325 if let Some(path) = &frame.frame_path {
326 if SHOW_FULL_PATH {
327 write!(f, "{}", path.raw_path)?;
328 } else if let Some(split_path) = &path.split_path {
329 write!(f, "[..]/{}", split_path.suffix)?;
330 } else {
331 write!(f, "{}", path.raw_path)?;
332 }
333
334 if let Some(lineno) = frame.lineno {
335 write!(f, ":{lineno}")?;
336 }
337 }
338 writeln!(f)?;
339 }
340 }
341 }
342
343 if value.total_omitted_frames > 0 {
344 writeln!(
345 f,
346 "note: {} frame(s) omitted. For a complete backtrace, set RUST_BACKTRACE=full.",
347 value.total_omitted_frames
348 )?;
349 }
350
351 Ok(())
352 }
353
354 fn debug(value: &Backtrace, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
355 std::fmt::Debug::fmt(value, formatter)
356 }
357
358 fn preferred_formatting_style(
359 backtrace: &Backtrace,
360 _report_formatting_function: FormattingFunction,
361 ) -> AttachmentFormattingStyle {
362 AttachmentFormattingStyle {
363 placement: if backtrace.entries.is_empty() {
364 AttachmentFormattingPlacement::Hidden
365 } else {
366 AttachmentFormattingPlacement::InlineWithHeader {
367 header: "Backtrace",
368 }
369 },
370 // No reason to every print the Backtrace in the report
371 // as anything other than display.
372 function: FormattingFunction::Display,
373 priority: 10,
374 }
375 }
376}
377
378/// Attachment collector for capturing stack backtraces.
379///
380/// When registered as a report creation hook, this collector automatically
381/// captures the current stack backtrace and attaches it as a [`Backtrace`]
382/// attachment.
383///
384/// # Examples
385///
386/// Basic usage with default settings:
387///
388/// ```
389/// use rootcause::hooks::Hooks;
390/// use rootcause_backtrace::BacktraceCollector;
391///
392/// Hooks::new()
393/// .report_creation_hook(BacktraceCollector::new_from_env())
394/// .install()
395/// .expect("failed to install hooks");
396/// ```
397///
398/// Custom configuration:
399///
400/// ```
401/// use rootcause::hooks::Hooks;
402/// use rootcause_backtrace::{BacktraceCollector, BacktraceFilter};
403///
404/// let collector = BacktraceCollector {
405/// filter: BacktraceFilter {
406/// skipped_initial_crates: &[],
407/// skipped_middle_crates: &[],
408/// skipped_final_crates: &[],
409/// max_entry_count: 30,
410/// show_full_path: true,
411/// },
412/// capture_backtrace_for_reports_with_children: true,
413/// };
414///
415/// Hooks::new()
416/// .report_creation_hook(collector)
417/// .install()
418/// .expect("failed to install hooks");
419/// ```
420#[derive(Copy, Clone)]
421pub struct BacktraceCollector {
422 /// Configuration for filtering and formatting backtrace frames.
423 pub filter: BacktraceFilter,
424
425 /// If set to true, a backtrace is captured for every report creation,
426 /// including reports that have child reports (i.e., reports created with
427 /// existing children). If set to false, a backtrace is captured only
428 /// for reports created without any children. Reports created without
429 /// children always receive a backtrace regardless of this setting.
430 pub capture_backtrace_for_reports_with_children: bool,
431}
432
433/// Configuration for filtering frames from certain crates in a backtrace.
434///
435/// # Examples
436///
437/// Use default filtering:
438///
439/// ```
440/// use rootcause_backtrace::BacktraceFilter;
441///
442/// let filter = BacktraceFilter::DEFAULT;
443/// ```
444///
445/// Custom filtering to focus on application code:
446///
447/// ```
448/// use rootcause_backtrace::BacktraceFilter;
449///
450/// let filter = BacktraceFilter {
451/// // Hide rootcause crate frames at the start
452/// skipped_initial_crates: &["rootcause", "rootcause-backtrace"],
453/// // Hide framework frames in the middle
454/// skipped_middle_crates: &["tokio", "hyper", "tower"],
455/// // Hide runtime frames at the end
456/// skipped_final_crates: &["std", "tokio"],
457/// // Show only the most relevant 10 frames
458/// max_entry_count: 10,
459/// // Show shortened paths
460/// show_full_path: false,
461/// };
462/// ```
463#[derive(Copy, Clone, Debug)]
464pub struct BacktraceFilter {
465 /// Set of crate names whose frames should be hidden when they appear
466 /// at the beginning of a backtrace.
467 pub skipped_initial_crates: &'static [&'static str],
468 /// Set of crate names whose frames should be hidden when they appear
469 /// in the middle of a backtrace.
470 pub skipped_middle_crates: &'static [&'static str],
471 /// Set of crate names whose frames should be hidden when they appear
472 /// at the end of a backtrace.
473 pub skipped_final_crates: &'static [&'static str],
474 /// Maximum number of entries to include in the backtrace.
475 pub max_entry_count: usize,
476 /// Whether to show full file paths in the backtrace frames.
477 pub show_full_path: bool,
478}
479
480impl BacktraceFilter {
481 /// Default backtrace filter settings.
482 pub const DEFAULT: Self = Self {
483 skipped_initial_crates: &[
484 "backtrace",
485 "rootcause",
486 "rootcause-backtrace",
487 "core",
488 "std",
489 "alloc",
490 ],
491 skipped_middle_crates: &["std", "core", "alloc", "tokio"],
492 skipped_final_crates: &["std", "core", "alloc", "tokio"],
493 max_entry_count: 20,
494 show_full_path: false,
495 };
496}
497
498impl Default for BacktraceFilter {
499 fn default() -> Self {
500 Self::DEFAULT
501 }
502}
503
504#[derive(Debug)]
505struct RootcauseEnvOptions {
506 rust_backtrace_full: bool,
507 backtrace_leafs_only: bool,
508 show_full_path: bool,
509}
510
511impl RootcauseEnvOptions {
512 fn get() -> &'static Self {
513 static ROOTCAUSE_FLAGS: OnceLock<RootcauseEnvOptions> = OnceLock::new();
514
515 ROOTCAUSE_FLAGS.get_or_init(|| {
516 let rust_backtrace_full =
517 std::env::var_os("RUST_BACKTRACE").is_some_and(|var| var == "full");
518 let mut show_full_path = rust_backtrace_full;
519 let mut backtrace_leafs_only = false;
520 if let Some(var) = std::env::var_os("ROOTCAUSE_BACKTRACE") {
521 for v in var.to_string_lossy().split(',') {
522 if v.eq_ignore_ascii_case("leafs") {
523 backtrace_leafs_only = true;
524 } else if v.eq_ignore_ascii_case("full_paths") {
525 show_full_path = true;
526 }
527 }
528 }
529 RootcauseEnvOptions {
530 rust_backtrace_full,
531 backtrace_leafs_only,
532 show_full_path,
533 }
534 })
535 }
536}
537
538impl BacktraceCollector {
539 /// Creates a new [`BacktraceCollector`] with default settings.
540 ///
541 /// Configuration is controlled by environment variables. By default,
542 /// filtering is applied and backtraces are only captured for reports
543 /// without children.
544 ///
545 /// # Environment Variables
546 ///
547 /// - `RUST_BACKTRACE=full` - Disables all filtering and shows all frames
548 /// - `ROOTCAUSE_BACKTRACE` - Comma-separated options:
549 /// - `leafs` - Only capture backtraces for leaf errors (errors without
550 /// children)
551 /// - `full_paths` - Show full file paths instead of shortened paths
552 ///
553 /// The `RUST_BACKTRACE=full` setting implies `full_paths` unless explicitly
554 /// overridden by `ROOTCAUSE_BACKTRACE`.
555 ///
556 /// # Examples
557 ///
558 /// ```
559 /// use rootcause::hooks::Hooks;
560 /// use rootcause_backtrace::BacktraceCollector;
561 ///
562 /// // Respects RUST_BACKTRACE and ROOTCAUSE_BACKTRACE environment variables
563 /// Hooks::new()
564 /// .report_creation_hook(BacktraceCollector::new_from_env())
565 /// .install()
566 /// .expect("failed to install hooks");
567 /// ```
568 pub fn new_from_env() -> Self {
569 let env_options = RootcauseEnvOptions::get();
570 let capture_backtrace_for_reports_with_children = !env_options.backtrace_leafs_only;
571
572 Self {
573 filter: if env_options.rust_backtrace_full {
574 BacktraceFilter {
575 skipped_initial_crates: &[],
576 skipped_middle_crates: &[],
577 skipped_final_crates: &[],
578 max_entry_count: usize::MAX,
579 show_full_path: env_options.show_full_path,
580 }
581 } else {
582 BacktraceFilter {
583 show_full_path: env_options.show_full_path,
584 ..BacktraceFilter::DEFAULT
585 }
586 },
587 capture_backtrace_for_reports_with_children,
588 }
589 }
590}
591
592impl ReportCreationHook for BacktraceCollector {
593 fn on_local_creation(&self, mut report: ReportMut<'_, Dynamic, markers::Local>) {
594 let do_capture =
595 self.capture_backtrace_for_reports_with_children || report.children().is_empty();
596 if do_capture && let Some(backtrace) = Backtrace::capture(&self.filter) {
597 let attachment = if self.filter.show_full_path {
598 ReportAttachment::new_custom::<BacktraceHandler<true>>(backtrace)
599 } else {
600 ReportAttachment::new_custom::<BacktraceHandler<false>>(backtrace)
601 };
602 report.attachments_mut().push(attachment.into_dynamic());
603 }
604 }
605
606 fn on_sendsync_creation(&self, mut report: ReportMut<'_, Dynamic, markers::SendSync>) {
607 let do_capture =
608 self.capture_backtrace_for_reports_with_children || report.children().is_empty();
609 if do_capture && let Some(backtrace) = Backtrace::capture(&self.filter) {
610 let attachment = if self.filter.show_full_path {
611 ReportAttachment::new_custom::<BacktraceHandler<true>>(backtrace)
612 } else {
613 ReportAttachment::new_custom::<BacktraceHandler<false>>(backtrace)
614 };
615 report.attachments_mut().push(attachment.into_dynamic());
616 }
617 }
618}
619
620const fn get_rootcause_backtrace_matcher(
621 location: &'static Location<'static>,
622) -> Option<(&'static str, usize)> {
623 let file = location.file();
624
625 let Some(prefix_len) = file.len().checked_sub("/src/lib.rs".len()) else {
626 return None;
627 };
628
629 let (prefix, suffix) = file.split_at(prefix_len);
630 // Assert the suffix is /src/lib.rs (or \src\lib.rs on Windows)
631 // This is a compile-time check that the caller location is valid
632 if HOST_PATH_SEPARATOR == '/' {
633 assert!(suffix.eq_ignore_ascii_case("/src/lib.rs"));
634 } else {
635 assert!(suffix.eq_ignore_ascii_case(r#"\src\lib.rs"#));
636 }
637
638 let (matcher_prefix, _) = file.split_at(prefix_len + 4);
639
640 let mut splitter_prefix = prefix;
641 while !splitter_prefix.is_empty() {
642 let (new_prefix, last_char) = splitter_prefix.split_at(splitter_prefix.len() - 1);
643 splitter_prefix = new_prefix;
644 if last_char.eq_ignore_ascii_case(HOST_PATH_SEPARATOR.encode_utf8(&mut [0])) {
645 break;
646 }
647 }
648
649 Some((matcher_prefix, splitter_prefix.len()))
650}
651
652const HOST_PATH_SEPARATOR: char = include!(concat!(env!("OUT_DIR"), "/host_path_separator"));
653const ROOTCAUSE_BACKTRACE_MATCHER: Option<(&str, usize)> =
654 get_rootcause_backtrace_matcher(Location::caller());
655const ROOTCAUSE_MATCHER: Option<(&str, usize)> =
656 get_rootcause_backtrace_matcher(rootcause::__private::ROOTCAUSE_LOCATION);
657
658impl Backtrace {
659 /// Captures the current stack backtrace, applying optional filtering.
660 pub fn capture(filter: &BacktraceFilter) -> Option<Self> {
661 let mut initial_filtering = !filter.skipped_initial_crates.is_empty();
662 let mut entries: Vec<BacktraceEntry> = Vec::new();
663 let mut total_omitted_frames = 0;
664
665 let mut delayed_omitted_frame: Option<Frame> = None;
666 let mut currently_omitted_crate_name: Option<&'static str> = None;
667 let mut currently_omitted_frames = 0;
668
669 backtrace::trace(|frame| {
670 backtrace::resolve_frame(frame, |symbol| {
671 // Don't consider frames without symbol names or filenames.
672 let (Some(sym), Some(filename_raw)) = (symbol.name(), symbol.filename_raw()) else {
673 return;
674 };
675
676 if entries.len() >= filter.max_entry_count {
677 total_omitted_frames += 1;
678 return;
679 }
680
681 let frame_path = FramePath::new(filename_raw);
682
683 if initial_filtering {
684 if let Some(cur_crate_name) = &frame_path.crate_name
685 && filter.skipped_initial_crates.contains(&&**cur_crate_name)
686 {
687 total_omitted_frames += 1;
688 return;
689 } else {
690 initial_filtering = false;
691 }
692 }
693
694 if let Some(cur_crate_name) = &frame_path.crate_name
695 && let Some(currently_omitted_crate_name) = ¤tly_omitted_crate_name
696 && cur_crate_name == currently_omitted_crate_name
697 {
698 delayed_omitted_frame = None;
699 currently_omitted_frames += 1;
700 total_omitted_frames += 1;
701 return;
702 }
703
704 if let Some(currently_omitted_crate_name) = currently_omitted_crate_name.take() {
705 if let Some(delayed_frame) = delayed_omitted_frame.take() {
706 entries.push(BacktraceEntry::Frame(delayed_frame));
707 } else {
708 entries.push(BacktraceEntry::OmittedFrames {
709 count: currently_omitted_frames,
710 skipped_crate: currently_omitted_crate_name,
711 });
712 }
713 currently_omitted_frames = 0;
714 }
715
716 if let Some(cur_crate_name) = &frame_path.crate_name
717 && let Some(skipped_crate) = filter
718 .skipped_middle_crates
719 .iter()
720 .find(|&crate_name| crate_name == cur_crate_name)
721 {
722 currently_omitted_crate_name = Some(skipped_crate);
723 currently_omitted_frames = 1;
724 total_omitted_frames += 1;
725 delayed_omitted_frame = Some(Frame {
726 sym_demangled: format!("{sym:#}"),
727 frame_path: Some(frame_path),
728 lineno: symbol.lineno(),
729 });
730 return;
731 }
732
733 entries.push(BacktraceEntry::Frame(Frame {
734 sym_demangled: format!("{sym:#}"),
735 frame_path: Some(frame_path),
736 lineno: symbol.lineno(),
737 }));
738 });
739
740 true
741 });
742
743 if let Some(currently_omitted_crate_name) = currently_omitted_crate_name.take() {
744 if let Some(delayed_frame) = delayed_omitted_frame.take() {
745 entries.push(BacktraceEntry::Frame(delayed_frame));
746 } else {
747 entries.push(BacktraceEntry::OmittedFrames {
748 count: currently_omitted_frames,
749 skipped_crate: currently_omitted_crate_name,
750 });
751 }
752 }
753
754 while let Some(last) = entries.last() {
755 match last {
756 BacktraceEntry::Frame(frame) => {
757 let mut skip = false;
758 if let Some(frame_path) = &frame.frame_path
759 && let Some(crate_name) = &frame_path.crate_name
760 && filter.skipped_final_crates.contains(&&**crate_name)
761 {
762 skip = true;
763 } else if frame.sym_demangled == "__libc_start_call_main"
764 || frame.sym_demangled == "__libc_start_main_impl"
765 {
766 skip = true;
767 } else if let Some(frame_path) = &frame.frame_path
768 && frame.sym_demangled == "_start"
769 && frame_path.raw_path.contains("zig/libc/glibc")
770 {
771 skip = true;
772 }
773
774 if skip {
775 total_omitted_frames += 1;
776 entries.pop();
777 } else {
778 break;
779 }
780 }
781 BacktraceEntry::OmittedFrames {
782 skipped_crate,
783 count,
784 } => {
785 if filter.skipped_final_crates.contains(skipped_crate) {
786 total_omitted_frames += count;
787 entries.pop();
788 } else {
789 break;
790 }
791 }
792 }
793 }
794
795 if entries.is_empty() && total_omitted_frames == 0 {
796 None
797 } else {
798 Some(Self {
799 entries,
800 total_omitted_frames,
801 })
802 }
803 }
804}
805
806impl FramePath {
807 fn new(path: BytesOrWideString<'_>) -> Self {
808 static REGEXES: OnceLock<[regex::Regex; 2]> = OnceLock::new();
809 let [std_regex, registry_regex] = REGEXES.get_or_init(|| {
810 [
811 // Matches Rust standard library paths:
812 // - /lib/rustlib/src/rust/library/{std|core|alloc}/src/...
813 // - /rustc/{40-char-hash}/library/{std|core|alloc}/src/...
814 regex::Regex::new(
815 r"(?:/lib/rustlib/src/rust|^/rustc/[0-9a-f]{40})/library/(std|core|alloc)/src/.*$",
816 )
817 .expect("built-in regex pattern for std library paths should be valid"),
818 // Matches Cargo registry paths:
819 // - /.cargo/registry/src/{index}-{16-char-hash}/{crate}-{version}/src/...
820 regex::Regex::new(
821 r"/\.cargo/registry/src/[^/]+-[0-9a-f]{16}/([^./]+)-[0-9]+\.[^/]*/src/.*$",
822 )
823 .expect("built-in regex pattern for cargo registry paths should be valid"),
824 ]
825 });
826
827 let path_str = path.to_string();
828
829 if let Some(captures) = std_regex.captures(&path_str) {
830 let raw_path = path.to_str_lossy().into_owned();
831 let crate_capture = captures
832 .get(1)
833 .expect("regex capture group 1 should exist for std library paths");
834 let split = crate_capture.start();
835 let (prefix, suffix) = (&path_str[..split - 1], &path_str[split..]);
836
837 Self {
838 raw_path,
839 split_path: Some(FramePrefix {
840 prefix_kind: "RUST_SRC",
841 prefix: prefix.to_string(),
842 suffix: suffix.to_string(),
843 }),
844 crate_name: Some(crate_capture.as_str().to_string().into()),
845 }
846 } else if let Some(captures) = registry_regex.captures(&path_str) {
847 let raw_path = path.to_str_lossy().into_owned();
848 let crate_capture = captures
849 .get(1)
850 .expect("regex capture group 1 should exist for cargo registry paths");
851 let split = crate_capture.start();
852 let (prefix, suffix) = (&path_str[..split - 1], &path_str[split..]);
853
854 Self {
855 raw_path,
856 crate_name: Some(crate_capture.as_str().to_string().into()),
857 split_path: Some(FramePrefix {
858 prefix_kind: "CARGO",
859 prefix: prefix.to_string(),
860 suffix: suffix.to_string(),
861 }),
862 }
863 } else if let Some((rootcause_matcher_prefix, rootcause_splitter_prefix_len)) =
864 ROOTCAUSE_MATCHER
865 && path_str.starts_with(rootcause_matcher_prefix)
866 {
867 let raw_path = path.to_str_lossy().into_owned();
868 let (prefix, suffix) = (
869 &path_str[..rootcause_splitter_prefix_len],
870 &path_str[rootcause_splitter_prefix_len + 1..],
871 );
872 Self {
873 raw_path,
874 split_path: Some(FramePrefix {
875 prefix_kind: "ROOTCAUSE",
876 prefix: prefix.to_string(),
877 suffix: suffix.to_string(),
878 }),
879 crate_name: Some(Cow::Borrowed("rootcause")),
880 }
881 } else if let Some((rootcause_matcher_prefix, rootcause_splitter_prefix_len)) =
882 ROOTCAUSE_BACKTRACE_MATCHER
883 && path_str.starts_with(rootcause_matcher_prefix)
884 {
885 let raw_path = path.to_str_lossy().into_owned();
886 let (prefix, suffix) = (
887 &path_str[..rootcause_splitter_prefix_len],
888 &path_str[rootcause_splitter_prefix_len + 1..],
889 );
890 Self {
891 raw_path,
892 split_path: Some(FramePrefix {
893 prefix_kind: "ROOTCAUSE",
894 prefix: prefix.to_string(),
895 suffix: suffix.to_string(),
896 }),
897 crate_name: Some(Cow::Borrowed("rootcause-backtrace")),
898 }
899 } else {
900 let raw_path = path.to_str_lossy().into_owned();
901 Self {
902 raw_path,
903 crate_name: None,
904 split_path: None,
905 }
906 }
907 }
908}
909
910/// Extension trait for attaching backtraces to reports.
911///
912/// This trait provides methods to easily attach a captured backtrace to a
913/// report or to the error contained within a `Result`.
914///
915/// # Examples
916///
917/// Attach backtrace to a report:
918///
919/// ```
920/// use std::io;
921///
922/// use rootcause::report;
923/// use rootcause_backtrace::BacktraceExt;
924///
925/// let report = report!(io::Error::other("An error occurred")).attach_backtrace();
926/// ```
927///
928/// Attach backtrace to a `Result`:
929///
930/// ```
931/// use std::io;
932///
933/// use rootcause::{Report, report};
934/// use rootcause_backtrace::BacktraceExt;
935///
936/// fn might_fail() -> Result<(), Report> {
937/// Err(report!(io::Error::other("operation failed")).into_dynamic())
938/// }
939///
940/// let result = might_fail().attach_backtrace();
941/// ```
942///
943/// Use a custom filter:
944///
945/// ```
946/// use std::io;
947///
948/// use rootcause::report;
949/// use rootcause_backtrace::{BacktraceExt, BacktraceFilter};
950///
951/// let filter = BacktraceFilter {
952/// skipped_initial_crates: &[],
953/// skipped_middle_crates: &[],
954/// skipped_final_crates: &[],
955/// max_entry_count: 50,
956/// show_full_path: true,
957/// };
958///
959/// let report = report!(io::Error::other("detailed error")).attach_backtrace_with_filter(&filter);
960/// ```
961pub trait BacktraceExt: Sized {
962 /// Attaches a captured backtrace to the report using the default filter.
963 ///
964 /// # Examples
965 ///
966 /// ```
967 /// use std::io;
968 ///
969 /// use rootcause::report;
970 /// use rootcause_backtrace::BacktraceExt;
971 ///
972 /// let report = report!(io::Error::other("error")).attach_backtrace();
973 /// ```
974 fn attach_backtrace(self) -> Self {
975 self.attach_backtrace_with_filter(&BacktraceFilter::DEFAULT)
976 }
977
978 /// Attaches a captured backtrace to the report using the specified filter.
979 ///
980 /// # Examples
981 ///
982 /// ```
983 /// use std::io;
984 ///
985 /// use rootcause::report;
986 /// use rootcause_backtrace::{BacktraceExt, BacktraceFilter};
987 ///
988 /// let filter = BacktraceFilter {
989 /// max_entry_count: 10,
990 /// ..BacktraceFilter::DEFAULT
991 /// };
992 ///
993 /// let report = report!(io::Error::other("error")).attach_backtrace_with_filter(&filter);
994 /// ```
995 fn attach_backtrace_with_filter(self, filter: &BacktraceFilter) -> Self;
996}
997
998impl<C: ?Sized, T> BacktraceExt for Report<C, markers::Mutable, T>
999where
1000 Backtrace: ObjectMarkerFor<T>,
1001{
1002 fn attach_backtrace_with_filter(mut self, filter: &BacktraceFilter) -> Self {
1003 if let Some(backtrace) = Backtrace::capture(filter) {
1004 if filter.show_full_path {
1005 self = self.attach_custom::<BacktraceHandler<true>, _>(backtrace);
1006 } else {
1007 self = self.attach_custom::<BacktraceHandler<false>, _>(backtrace);
1008 }
1009 }
1010 self
1011 }
1012}
1013
1014impl<C: ?Sized, V, T> BacktraceExt for Result<V, Report<C, markers::Mutable, T>>
1015where
1016 Backtrace: ObjectMarkerFor<T>,
1017{
1018 fn attach_backtrace_with_filter(self, filter: &BacktraceFilter) -> Self {
1019 match self {
1020 Ok(v) => Ok(v),
1021 Err(report) => Err(report.attach_backtrace_with_filter(filter)),
1022 }
1023 }
1024}