Using tracing and tracing_error

I very much like the idea of the tracing and tracing-error
However, these crates are not the complete solution for error handling.
You need to create your own error type and add tracing info there.

I'm used to using eyre, but it does not look like it would be easy to add tracing info there.

First of all, I am interested are there any crates that provide errors similar to anyhow or eyre but also use tracing-error.

I also tried to create my own very simple error type. And I want some improvements:

  • I want to know the exact file and line where the Error happened, not only the function.
  • Also when I use map_err, it would be great not to write an error message at all, but to automatically have it like "call to [function_name] failed".
  • Maybe error messages can have a syntax similar to tracing events syntax, where you can add variables without mentioning them in a format string.

Here is some example code.

use tracing_subscriber::prelude::*;

struct Error {
    message: String,
    cause: Option<Box<dyn ToString>>,
    span_trace: tracing_error::SpanTrace,
}

impl Error {
    fn new(message: impl ToString) -> Self {
        Self {
            message: message.to_string(),
            cause: None,
            span_trace: tracing_error::SpanTrace::capture(),
        }
    }

    fn with_cause(message: impl ToString, cause: impl ToString + 'static) -> Self {
        Self {
            message: message.to_string(),
            cause: Some(Box::new(cause)),
            span_trace: tracing_error::SpanTrace::capture(),
        }
    }
}

impl std::fmt::Debug for Error {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "Error: {}", self.message)?;
        if let Some(cause) = &self.cause {
            write!(f, "\nCause: {}", cause.to_string())?;
        }
        write!(f, "\n{}", self.span_trace)
    }
}

type Result<T> = std::result::Result<T, Error>;

#[tracing::instrument]
fn try_div(a: u32, b: u32) -> Result<u32> {
    if b == 0 {
        return Err(Error::new("division by zero"));
    }
    Ok(a / b)
}

#[tracing::instrument]
fn try_parse_to_i32(s: &str) -> Result<i32> {
    // just an example of using tracing event
    let s_len = s.len();
    tracing::info!(s_len, "string length");

    s.parse::<i32>()
        .map_err(|e| Error::with_cause("failed to parse i32", e))
}

fn init_tracing() {
    let subscriber = tracing_subscriber::FmtSubscriber::builder()
        .with_max_level(tracing::Level::TRACE)
        .finish()
        .with(tracing_error::ErrorLayer::default());
    tracing::subscriber::set_global_default(subscriber).unwrap();
}

fn main() {
    init_tracing();

    println!("try_div(1, 0): {:?}", try_div(1, 0));
    println!("try_parse_to_i32(\"abc\"): {:?}", try_parse_to_i32("abc"));
}

It will print:

try_div(1, 0): Err(Error: division by zero
   0: tmp::try_div
           with a=1 b=0
             at src/main.rs:39)

INFO try_parse_to_i32{s="abc"}: tmp: string length s_len=3

try_parse_to_i32("abc"): Err(Error: failed to parse i32
Cause: invalid digit found in string
   0: tmp::try_parse_to_i32
           with s="abc"
             at src/main.rs:47)

One of my ideas is to create a macro, that can be used on function calls and will create a local span, so that the error context will include all the information. But it can sometimes be not convenient to use.

macro_rules! map_err {
    ($e: expr) => {{
        let __span = tracing::span!(tracing::Level::INFO, stringify!($e));
        let __guard = __span.enter();
        $e.map_err(|e| Error::with_cause("", e))
    }};
}
...
    let i = map_err!(s.parse::<i32>())?;

For me, it looks like to have the tracing event not being logged, but saved with the error.