Skip to content

Commit

Permalink
Optionally include backtraces in errors
Browse files Browse the repository at this point in the history
Seeing error and their call chains can be tremendously useful and most
of the time that is sufficient to get a good understanding of an issue.
However, occasionally errors don't contain enough context and messages
are reused on multiple call paths.
With this change we enhance our Error type with the capability to
capture a backtrace at the time the error was created. This backtrace is
then contained in the Debug representation. Note that because capturing
a backtrace is a costly operation, backtrace capture is controlled by
environment variables in the same way that Rust proper behaves.

Signed-off-by: Daniel Müller <[email protected]>
  • Loading branch information
d-e-s-o committed Oct 30, 2023
1 parent 424ac90 commit bc4fca4
Show file tree
Hide file tree
Showing 6 changed files with 161 additions and 17 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ Unreleased
- Added caching for APK related symbolization data structures
- Added caching logic to `inspect::Inspector`
- Made `inspect::SymInfo::file_offset` member optional
- Added ability to contain backtraces in `Error` objects
- Added support for symbolizing Gsym addresses to `blazecli`
- Fixed bogus inlined function reporting for Gsym
- Bumped minimum supported Rust version to `1.65`
Expand Down
6 changes: 5 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,11 @@ name = "blazesym"
crate-type = ["lib", "cdylib", "staticlib"]

[features]
default = ["demangle", "dwarf"]
default = ["backtrace", "demangle", "dwarf"]
# Enable this feature to compile in support for capturing backtraces in errors.
# Note that by default backtraces will not be collected unless opted in with
# environment variables.
backtrace = []
# Enable this feature to enable DWARF support.
dwarf = ["gimli"]
# Enable this feature to get transparent symbol demangling.
Expand Down
80 changes: 64 additions & 16 deletions src/error.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use std::backtrace::Backtrace;
use std::backtrace::BacktraceStatus;
use std::borrow::Borrow;
use std::borrow::Cow;
use std::error;
Expand Down Expand Up @@ -91,16 +93,17 @@ impl IntoCowStr for String {
}
}


// TODO: We may want to support optionally storing a backtrace in
// terminal variants.
enum ErrorImpl {
#[cfg(feature = "dwarf")]
Dwarf {
error: gimli::Error,
#[cfg(feature = "backtrace")]
backtrace: Backtrace,
},
Io {
error: io::Error,
#[cfg(feature = "backtrace")]
backtrace: Backtrace,
},
// Unfortunately, if we just had a single `Context` variant that
// contains a `Cow`, this inner `Cow` would cause an overall enum
Expand All @@ -123,7 +126,7 @@ impl ErrorImpl {
match self {
#[cfg(feature = "dwarf")]
Self::Dwarf { .. } => ErrorKind::InvalidDwarf,
Self::Io { error } => match error.kind() {
Self::Io { error, .. } => match error.kind() {
io::ErrorKind::NotFound => ErrorKind::NotFound,
io::ErrorKind::PermissionDenied => ErrorKind::PermissionDenied,
io::ErrorKind::AlreadyExists => ErrorKind::AlreadyExists,
Expand All @@ -143,6 +146,24 @@ impl ErrorImpl {
}
}

/// Retrieve the object's associated backtrace, if any.
#[cfg(feature = "backtrace")]
fn backtrace(&self) -> Option<&Backtrace> {
match self {
#[cfg(feature = "dwarf")]
Self::Dwarf { backtrace, .. } => Some(backtrace),
Self::Io { backtrace, .. } => Some(backtrace),
Self::ContextOwned { .. } => None,
Self::ContextStatic { .. } => None,
}
}

/// Stub for retrieving no backtrace, as support is compiled out.
#[cfg(not(feature = "backtrace"))]
fn backtrace(&self) -> Option<&Backtrace> {
None
}

#[cfg(test)]
fn is_owned(&self) -> Option<bool> {
match self {
Expand All @@ -162,11 +183,11 @@ impl Debug for ErrorImpl {

match self {
#[cfg(feature = "dwarf")]
Self::Dwarf { error } => {
Self::Dwarf { error, .. } => {
dbg = f.debug_tuple(stringify!(Dwarf));
dbg.field(error)
}
Self::Io { error } => {
Self::Io { error, .. } => {
dbg = f.debug_tuple(stringify!(Io));
dbg.field(error)
}
Expand All @@ -183,8 +204,8 @@ impl Debug for ErrorImpl {
} else {
let () = match self {
#[cfg(feature = "dwarf")]
Self::Dwarf { error } => write!(f, "Error: {error}")?,
Self::Io { error } => write!(f, "Error: {error}")?,
Self::Dwarf { error, .. } => write!(f, "Error: {error}")?,
Self::Io { error, .. } => write!(f, "Error: {error}")?,
Self::ContextOwned { context, .. } => write!(f, "Error: {context}")?,
Self::ContextStatic { context, .. } => write!(f, "Error: {context}")?,
};
Expand All @@ -198,6 +219,13 @@ impl Debug for ErrorImpl {
error = err.source();
}
}

match self.backtrace() {
Some(backtrace) if backtrace.status() == BacktraceStatus::Captured => {
let () = write!(f, "\n\nStack backtrace:\n{backtrace}")?;
}
_ => (),
}
Ok(())
}
}
Expand All @@ -207,8 +235,8 @@ impl Display for ErrorImpl {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
let () = match self {
#[cfg(feature = "dwarf")]
Self::Dwarf { error } => Display::fmt(error, f)?,
Self::Io { error } => Display::fmt(error, f)?,
Self::Dwarf { error, .. } => Display::fmt(error, f)?,
Self::Io { error, .. } => Display::fmt(error, f)?,
Self::ContextOwned { context, .. } => Display::fmt(context, f)?,
Self::ContextStatic { context, .. } => Display::fmt(context, f)?,
};
Expand All @@ -228,8 +256,8 @@ impl error::Error for ErrorImpl {
fn source(&self) -> Option<&(dyn error::Error + 'static)> {
match self {
#[cfg(feature = "dwarf")]
Self::Dwarf { error } => error.source(),
Self::Io { error } => error.source(),
Self::Dwarf { error, .. } => error.source(),
Self::Io { error, .. } => error.source(),
Self::ContextOwned { source, .. } | Self::ContextStatic { source, .. } => Some(source),
}
}
Expand Down Expand Up @@ -333,6 +361,19 @@ pub enum ErrorKind {
/// // > No such file or directory (os error 2)
/// println!("{err:?}");
/// ```
///
/// On top of that, if the `backtrace` feature is enabled, errors may also
/// contain an optional backtrace. Backtrace capturing behavior follows the
/// exact rules set forth by the [`std::backtrace`] module. That is, it is
/// controlled by the `RUST_BACKTRACE` and `RUST_LIB_BACKTRACE` environment
/// variables. Please refer to this module's documentation for precise
/// semantics, but in short:
/// - If you want panics and errors to both have backtraces, set
/// `RUST_BACKTRACE=1`
/// - If you want only errors to have backtraces, set
/// `RUST_LIB_BACKTRACE=1`
/// - If you want only panics to have backtraces, set `RUST_BACKTRACE=1`
/// and `RUST_LIB_BACKTRACE=0`
// Representation is optimized for fast copying (a single machine word),
// not so much for fast creation (as it is heap allocated). We generally
// expect errors to be exceptional, though a lot of functionality is
Expand Down Expand Up @@ -429,15 +470,23 @@ impl error::Error for Error {
impl From<gimli::Error> for Error {
fn from(other: gimli::Error) -> Self {
Self {
error: Box::new(ErrorImpl::Dwarf { error: other }),
error: Box::new(ErrorImpl::Dwarf {
error: other,
#[cfg(feature = "backtrace")]
backtrace: Backtrace::capture(),
}),
}
}
}

impl From<io::Error> for Error {
fn from(other: io::Error) -> Self {
Self {
error: Box::new(ErrorImpl::Io { error: other }),
error: Box::new(ErrorImpl::Io {
error: other,
#[cfg(feature = "backtrace")]
backtrace: Backtrace::capture(),
}),
}
}
}
Expand Down Expand Up @@ -633,7 +682,6 @@ mod tests {
#[test]
fn error_size() {
assert_eq!(size_of::<Error>(), size_of::<usize>());
assert_eq!(size_of::<ErrorImpl>(), 4 * size_of::<usize>());
}

/// Check that we can format errors as expected.
Expand All @@ -648,7 +696,7 @@ mod tests {
assert_eq!(err.kind(), ErrorKind::InvalidData);
assert_eq!(format!("{err}"), "some invalid data");
assert_eq!(format!("{err:#}"), "some invalid data");
assert_eq!(format!("{err:?}"), "Error: some invalid data");
assert!(format!("{err:?}").starts_with("Error: some invalid data"));
// TODO: The inner format may not actually be all that stable.
let expected = r#"Io(
Custom {
Expand Down
40 changes: 40 additions & 0 deletions tests/error_backtrace.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
//! This test relies on environment variable modification for its
//! workings and, hence, relies on being run in a dedicated process. Do
//! not add additional test cases unless they are guaranteed to not
//! interfere.

#![allow(clippy::let_and_return, clippy::let_unit_value)]

use std::env;
use std::io;

use blazesym::Error;


/// Make sure that we can capture backtraces in errors.
///
/// # Notes
/// This test requires sufficient debug information to be present so
/// that the file name is contained in the backtrace. For that reason we
/// only run it on debug builds (represented by the `debug_assertions`
/// proxy cfg).
#[test]
fn error_backtrace() {
if !cfg!(debug_assertions) {
return
}

// Ensure that we capture a backtrace.
let () = env::set_var("RUST_LIB_BACKTRACE", "1");

let err = io::Error::new(io::ErrorKind::InvalidData, "some invalid data");
let err = Error::from(err);
let debug = format!("{err:?}");

let start_idx = debug.find("Stack backtrace").unwrap();
let backtrace = &debug[start_idx..];
assert!(
backtrace.contains("tests/error_backtrace.rs"),
"{backtrace}"
);
}
26 changes: 26 additions & 0 deletions tests/error_no_backtrace1.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
//! This test relies on environment variable modification for its
//! workings and, hence, relies on being run in a dedicated process. Do
//! not add additional test cases unless they are guaranteed to not
//! interfere.

#![allow(clippy::let_and_return, clippy::let_unit_value)]

use std::env;
use std::io;

use blazesym::Error;


/// Make sure that we do not emit backtraces in errors when
/// the `RUST_LIB_BACKTRACE` environment variable is not present.
#[test]
fn error_no_backtrace1() {
let () = env::remove_var("RUST_BACKTRACE");
let () = env::remove_var("RUST_LIB_BACKTRACE");

let err = io::Error::new(io::ErrorKind::InvalidData, "some invalid data");
let err = Error::from(err);
let debug = format!("{err:?}");

assert_eq!(debug.find("Stack backtrace"), None);
}
25 changes: 25 additions & 0 deletions tests/error_no_backtrace2.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
//! This test relies on environment variable modification for its
//! workings and, hence, relies on being run in a dedicated process. Do
//! not add additional test cases unless they are guaranteed to not
//! interfere.

#![allow(clippy::let_and_return, clippy::let_unit_value)]

use std::env;
use std::io;

use blazesym::Error;


/// Make sure that we do not emit backtraces in errors when
/// the `RUST_LIB_BACKTRACE` environment variable is "0".
#[test]
fn error_no_backtrace2() {
let () = env::set_var("RUST_LIB_BACKTRACE", "0");

let err = io::Error::new(io::ErrorKind::InvalidData, "some invalid data");
let err = Error::from(err);
let debug = format!("{err:?}");

assert_eq!(debug.find("Stack backtrace"), None);
}

0 comments on commit bc4fca4

Please sign in to comment.