Skip to content

Commit

Permalink
error: add error instrumentation type and traits (#574)
Browse files Browse the repository at this point in the history
## Motivation

The tracing_error crate should provide some basic functionality for
instrumenting Errors with `SpanTraces` generically. The goal is to make
it easy for users to instrument their errors without needing to add a
`SpanTrace` to every error they construct.

## Solution

Critically this trait provides 3 traits for instrumenting errors,
`InstrumentResult`, `InstrumentError`, and `ExtractSpanTrace`. The first
two traits expose a `in_current_span()` method that can work on a
`Result<T, E: InstrumentError>` and an `Error`, respectively, that wraps
the error type in a `TracedError` that stores the inner error and a
SpanTrace. The third trait, `ExtractSpanTrace`, is used to retrieve the
span_trace member of a `TracedError` from a `&dyn Error` trait object.

This is done by downcasting the error to a `TracedError` and returning
the inner `span_trace` if it is successful. By default this only works
because the inner error is stored in a Box as a trait object, which
prevents it from being part of the type signature for `TracedError`.

However, this crate also exposes a second version of `TracedError` that
does not box the inner error, thus avoiding the need for heap
allocation, which it accomplishes by type erasing the `TracedError<E>`
into a `TracedError<()>` while iterating through the error `source()`
chain.
  • Loading branch information
yaahc authored Feb 25, 2020
1 parent a6a2434 commit 503d936
Show file tree
Hide file tree
Showing 6 changed files with 516 additions and 11 deletions.
91 changes: 91 additions & 0 deletions examples/examples/instrumented_error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
//! This example demonstrates using the `tracing-error` crate's `SpanTrace` type
//! to attach a trace context to a custom error type.
#![deny(rust_2018_idioms)]
use std::error::Error;
use std::fmt;
use tracing_error::{prelude::*, ErrorLayer};
use tracing_subscriber::{prelude::*, registry::Registry};

#[derive(Debug)]
struct FooError {
message: &'static str,
}

// Arbitrary user defined error type for demonstration purposes only
impl FooError {
fn new(message: &'static str) -> Self {
Self { message }
}
}

impl Error for FooError {}

impl fmt::Display for FooError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.pad(self.message)
}
}

#[tracing::instrument]
fn do_something(foo: &str) -> Result<&'static str, impl Error + Send + Sync + 'static> {
// Results can be instrumented with a `SpanTrace` via the `InstrumentResult` trait
do_another_thing(42, false).in_current_span()
}

#[tracing::instrument]
fn do_another_thing(
answer: usize,
will_succeed: bool,
) -> Result<&'static str, impl Error + Send + Sync + 'static> {
// Errors can also be instrumented directly via the `InstrumentError` trait
Err(FooError::new("something broke, lol").in_current_span())
}

#[tracing::instrument]
fn main() {
let subscriber = Registry::default()
.with(tracing_subscriber::fmt::Layer::default())
// The `ErrorLayer` subscriber layer enables the use of `SpanTrace`.
.with(ErrorLayer::default());
tracing::subscriber::set_global_default(subscriber).expect("Could not set global default");
match do_something("hello world") {
Ok(result) => println!("did something successfully: {}", result),
Err(e) => {
eprintln!("printing error chain naively");
print_naive_spantraces(&e);

eprintln!("\nprinting error with extract method");
print_extracted_spantraces(&e);
}
};
}

// Iterate through the source errors and check if each error is actually the attached `SpanTrace`
// before printing it
fn print_extracted_spantraces(error: &(dyn Error + 'static)) {
let mut error = Some(error);
let mut ind = 0;

while let Some(err) = error {
if let Some(spantrace) = err.span_trace() {
eprintln!("Span Backtrace:\n{}", spantrace);
} else {
eprintln!("Error {}: {}", ind, err);
}

error = err.source();
ind += 1;
}
}

// Iterate through source errors and print all sources as errors uniformly, regardless of whether
// or not that error has a `SpanTrace` attached or not.
fn print_naive_spantraces(error: &(dyn Error + 'static)) {
let mut error = Some(error);
let mut ind = 0;
while let Some(err) = error {
eprintln!("Error {}: {}", ind, err);
error = err.source();
ind += 1;
}
}
4 changes: 4 additions & 0 deletions tracing-error/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ keywords = [
]
edition = "2018"

[features]

stack-error = []

[dependencies]
tracing-subscriber = { path = "../tracing-subscriber", version = "0.2.1", default-features = false, features = ["registry", "fmt"] }
tracing = { path = "../tracing", version = "0.1"}
Expand Down
1 change: 0 additions & 1 deletion tracing-error/src/backtrace.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@ use tracing::{Metadata, Span};
/// similarly to how Rust formats panics. For example:
///
/// ```text
/// span backtrace:
/// 0: custom_error::do_another_thing
/// with answer=42 will_succeed=false
/// at examples/examples/custom_error.rs:42
Expand Down
83 changes: 83 additions & 0 deletions tracing-error/src/heap_error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
use crate::SpanTrace;
use crate::{ExtractSpanTrace, InstrumentError};
use std::error::Error;
use std::fmt::{self, Debug, Display};

/// A wrapper type for Errors that bundles a `SpanTrace` with an inner `Error` type.
pub struct TracedError {
inner: ErrorImpl,
}

struct ErrorImpl {
span_trace: SpanTrace,
error: Box<dyn Error + Send + Sync + 'static>,
}

impl TracedError {
fn new<E>(error: E) -> Self
where
E: Error + Send + Sync + 'static,
{
Self {
inner: ErrorImpl {
span_trace: SpanTrace::capture(),
error: Box::new(error),
},
}
}
}

impl Error for TracedError {
fn source<'a>(&'a self) -> Option<&'a (dyn Error + 'static)> {
Some(&self.inner)
}
}

impl Debug for TracedError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
Debug::fmt(&self.inner.error, f)
}
}

impl Display for TracedError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
Display::fmt(&self.inner.error, f)
}
}

impl Error for ErrorImpl {
fn source<'a>(&'a self) -> Option<&'a (dyn Error + 'static)> {
self.error.source()
}
}

impl Debug for ErrorImpl {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.pad("span backtrace:\n")?;
Debug::fmt(&self.span_trace, f)
}
}

impl Display for ErrorImpl {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.pad("span backtrace:\n")?;
Display::fmt(&self.span_trace, f)
}
}

impl<E> InstrumentError for E
where
E: Error + Send + Sync + 'static,
{
type Instrumented = TracedError;

fn in_current_span(self) -> Self::Instrumented {
TracedError::new(self)
}
}

impl ExtractSpanTrace for &(dyn Error + 'static) {
fn span_trace(&self) -> Option<&SpanTrace> {
self.downcast_ref::<ErrorImpl>().map(|e| &e.span_trace)
}
}
Loading

0 comments on commit 503d936

Please sign in to comment.