API design: `TracingResult`

So I've finally had enough of typing

.map_err(|error| {
    tracing::warn!("uncooperative yak!", %error);
    error
})?;

and decided to do something about it. I currently have the following (nightly only), but would really like ideas and challenges on the overall API at an early stage:

.or_warn("uncooperative yak!")?;

this relies on

  • a trait extension to Result
  • a custom type which stores a tracing event alongside the Ok(value) or Err(err) variants of a result
  • emitting the event on ?

In particular I'd like opinions on:

  1. the extension methods naming:
    • or_warn() / or_debug() etc emit on Err
    • and_...() emit on Ok
    • should there be something that always emits? currently .and_info().or_warn()? only emits one of the two possible events.
  2. ability to chain:
    • supporting .and_info().or_warn()?; reads logically
    • is it worth the extra work and complexity to avoid allowing .or_warn().and_info()? which would be confusing?
  3. ability to add fields
    • currently the methods require a single name: &'static str
    • any extra fields need to be included in a wrapping Span first
    • this keeps the method call chain simple and provides a clear intent "I'm going to do some stuff and any warnings should include this info about it"
    • the alternative would be to accept a struct of fields with an empty Default, but that feels ugly to me
  4. because of the way tracing uses statics to allow for self-referential structs, it's not possible to create a DefaultCallsite with the location of the ? as the module-path & line number.[1]
    • How much of a killer would it be to have origin_module_path and origin_line as fields on the event, while .metadata().module_path() & .metadata().line() return the location inside tracing_result?
    • I may be able to construct a custom type to work around this, but it's a lot of extra complexity and my gut says that tracing went the macros + static route for multiple reasons, not just performance.

Thanks in advance!


  1. The location is available at runtime, but runtime values can't be used to construct MetaData. To have this done at compile time would mean switching to a macro-based approach which isn't the API I want. ↩︎

.inspect_err(|error| tracing::warn!("uncooperative yak!", %error))?;

Or:

use std::fmt::Display;

fn trace<T>(message: &str) -> impl FnOnce(&T) + '_
where
    T: Display,
{
    move |error| tracing::warn!(%error, "{message}")
}

fn main() {
    let e: Result<String, String> = Err("meow!".into());
    e.inspect_err(trace("uncooperative yak!"));
}

You already got pretty good suggestion to your question, but I just wanted to pay respect to the tracing message you choose for your example "uncooperative yak!". Very witty. :wink:

i wonder if all possible errors can be mapped to yaks,

permission denied -> uncooperative yak!
file not found -> could not locate yak!
segfault -> the yaks are flying out of my nostrils!
sigtrap -> the yak was a lie!
eagain -> stubborn yak!

I've decided to continue with tracing_result with a very simple interface:

  • only takes a name, any other fields need to be in a previously entered span
  • chaining emits the relevant event (so to always emit you need to .and_info("").or_info("")?) (TBD)
  • I'll "fix" the callsite location with fields first and maybe adjust to a custom CallSite implementation later (TBD)
  • Generically implemented on all Try types, not just Result (so also works with Option) (TBD)

I'm leaving .inspect_err(|e| ...) as the solution to this, I just decided even that was annoying enough to finish what I'd started :slight_smile: