Skip to content

Commit

Permalink
Support ErrorRef in # error formatter
Browse files Browse the repository at this point in the history
This commit adds support for error references for the `#` error formatter from the `kv` macro. If the passed error is an owned error value it uses `ErrorValue`; otherwise if it can be dereferenced into an error then it uses `ErrorRef`.

The way to detect if the value implements `Error` directly or if it's an error reference is based on auto-deref coercion as described in the following articles:
- <https://lukaskalbertodt.github.io/2019/12/05/generalized-autoref-based-specialization.html>
- <https://github.com/dtolnay/case-studies/tree/master/autoref-specialization>

I believe it to be fully backwards compatible with existing uses of `#`. This provides a short-hand for the `ErrorRef` wrapper in most common cases. However, it does not support temporary lifetime extension.

This means that the following example needs to introduce a temporary binding to use the shorthand syntax:
```rust
// explicit ref with lifetime extension
info!(logger, "not found"; "error" => ErrorRef(&std::io::Error::from(std::io::ErrorKind::NotFound)));
// `#` shorthand with temporary binding
let error = std::io::Error::from(std::io::ErrorKind::NotFound);
info!(logger, "not found"; "error" => #&error);
```

In practice, this use case is not an issue. Such a temporary can instead be passed by value (the reference is useless) through `ErrorValue`. There are [discussions in Zulip](https://rust-lang.zulipchat.com/#narrow/stream/403629-t-lang.2Ftemporary-lifetimes-2024) to support explicit lifetime extensions and support this use case in the future.
  • Loading branch information
demurgos committed Dec 14, 2023
1 parent 90889a7 commit e2a25e9
Show file tree
Hide file tree
Showing 3 changed files with 153 additions and 17 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
## [Unreleased]

* Add `ErrorRef` wrapper to enable logging error references.
* The `#` error formatter in macros was updated to automatically select `ErrorValue` or `ErrorRef`.

### 2.8.0-beta.1 - 2023-09-09

Expand Down
122 changes: 116 additions & 6 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -405,10 +405,10 @@ macro_rules! kv(
kv!(@ ($crate::SingleKV::from(($k, __slog_builtin!(@format_args "{:#?}", $v))), $args_ready); $($args)* )
};
(@ $args_ready:expr; $k:expr => #$v:expr) => {
kv!(@ ($crate::SingleKV::from(($k, $crate::ErrorValue($v))), $args_ready); )
kv!(@ ($crate::SingleKV::from(($k, { use $crate::{ErrorRefKind, ErrorValueKind}; let v = $v; let w = $crate::ErrorTagWrapper(v); (&&w).slog_error_kind().wrap(w.0) } )), $args_ready); )
};
(@ $args_ready:expr; $k:expr => #$v:expr, $($args:tt)* ) => {
kv!(@ ($crate::SingleKV::from(($k, $crate::ErrorValue($v))), $args_ready); $($args)* )
kv!(@ ($crate::SingleKV::from(($k, { use $crate::{ErrorRefKind, ErrorValueKind}; let v = $v; let w = $crate::ErrorTagWrapper(v); (&&w).slog_error_kind().wrap(w.0) } )), $args_ready); $($args)* )
};
(@ $args_ready:expr; $k:expr => $v:expr) => {
kv!(@ ($crate::SingleKV::from(($k, $v)), $args_ready); )
Expand Down Expand Up @@ -462,10 +462,10 @@ macro_rules! slog_kv(
slog_kv!(@ ($crate::SingleKV::from(($k, __slog_builtin!(@format_args "{:#?}", $v))), $args_ready); $($args)* )
};
(@ $args_ready:expr; $k:expr => #$v:expr) => {
slog_kv!(@ ($crate::SingleKV::from(($k, $crate::ErrorValue($v))), $args_ready); )
slog_kv!(@ ($crate::SingleKV::from(($k, { use $crate::{ErrorRefKind, ErrorValueKind}; let v = $v; let w = $crate::ErrorTagWrapper(v); (&&w).slog_error_kind().wrap(w.0) } )), $args_ready); )
};
(@ $args_ready:expr; $k:expr => #$v:expr, $($args:tt)* ) => {
slog_kv!(@ ($crate::SingleKV::from(($k, $crate::ErrorValue($v))), $args_ready); $($args)* )
slog_kv!(@ ($crate::SingleKV::from(($k, { use $crate::{ErrorRefKind, ErrorValueKind}; let v = $v; let w = $crate::ErrorTagWrapper(v); (&&w).slog_error_kind().wrap(w.0) } )), $args_ready); $($args)* )
};
(@ $args_ready:expr; $k:expr => $v:expr) => {
slog_kv!(@ ($crate::SingleKV::from(($k, $v)), $args_ready); )
Expand Down Expand Up @@ -730,7 +730,8 @@ macro_rules! slog_record(
/// Similarly to use `std::fmt::Debug` value can be prefixed with `?`,
/// or pretty-printed with `#?`.
///
/// Errors can be prefixed with `#` as a shorthand for wrapping with `ErrorValue`.
/// Errors can be prefixed with `#` as a shorthand. Error values will be wrapped
/// with [`ErrorValue`]. Error references will be wrapped in [`ErrorRef`].
///
/// Full list of supported formats can always be inspected by looking at
/// [`kv` macro](macro.log.html)
Expand Down Expand Up @@ -3349,6 +3350,115 @@ where
}
}

/// Wrapper for auto-deref specialization for error values and references.
///
/// See <https://lukaskalbertodt.github.io/2019/12/05/generalized-autoref-based-specialization.html>
/// and <https://github.com/dtolnay/case-studies/tree/master/autoref-specialization> for
/// details about the technique.
///
/// This type allows to detect if it received an error reference or value.
/// To test the kind of `e`, you have to call it as `(&&ErrorTagWrapper(e)).slog_error_kind()`.
/// It will return [`ErrorValueTag`] for values and [`ErrorRefTag`] for references.
///
/// This is an internal implementation detail of the `kv!` macro and should not
/// be used directly.
#[cfg(feature = "std")]
#[doc(hidden)]
pub struct ErrorTagWrapper<E>(E);

#[cfg(feature = "std")]
#[test]
fn test_error_tag_wrapper() {
#[derive(Debug, Clone, Copy)]
struct MyError(&'static str);
impl core::fmt::Display for MyError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.write_str(self.0)
}
}
impl std::error::Error for MyError {}
let e = MyError("everything is on fire");
assert_eq!((&&ErrorTagWrapper(e)).slog_error_kind(), ErrorValueTag);
let e = &e;
assert_eq!((&&ErrorTagWrapper(e)).slog_error_kind(), ErrorRefTag);
}

/// Unit struct indicating that the content of `ErrorTagWrapper` was a value.
///
/// This is an internal implementation detail of the `kv!` macro and should not
/// be used directly.
#[cfg(feature = "std")]
#[doc(hidden)]
#[derive(Debug, PartialEq, Eq)]
pub struct ErrorValueTag;
#[cfg(feature = "std")]
impl ErrorValueTag {
/// Create a [`Value`] wrapper for an owned error value.
pub fn wrap<E>(self, e: E) -> ErrorValue<E>
where
E: std::error::Error,
{
ErrorValue(e)
}
}

/// Auxiliary trait for auto-deref dispatch of owned error values
///
/// This is an internal implementation detail of the `kv!` macro and should not
/// be used directly.
#[cfg(feature = "std")]
#[doc(hidden)]
pub trait ErrorValueKind {
#[inline]
fn slog_error_kind(&self) -> ErrorValueTag {
ErrorValueTag
}
}
#[cfg(feature = "std")]
impl<E: std::error::Error> ErrorValueKind for ErrorTagWrapper<E> {}

/// Unit struct indicating that the content of `ErrorTagWrapper` was a reference.
///
/// This is an internal implementation detail of the `kv!` macro and should not
/// be used directly.
#[cfg(feature = "std")]
#[doc(hidden)]
#[derive(Debug, PartialEq, Eq)]
pub struct ErrorRefTag;
#[cfg(feature = "std")]
impl ErrorRefTag {
/// Create a [`Value`] wrapper for an error reference.
pub fn wrap<'a, E>(self, e: &'a E) -> ErrorRef<'a, E>
where
E: ?Sized + 'static + std::error::Error,
{
ErrorRef(e)
}
}

/// Auxiliary trait for auto-deref dispatch of error references
///
/// This is an internal implementation detail of the `kv!` macro and should not
/// be used directly.
#[cfg(feature = "std")]
#[doc(hidden)]
pub trait ErrorRefKind {
#[inline]
fn slog_error_kind(self) -> ErrorRefTag
where
Self: Sized,
{
ErrorRefTag
}
}
#[cfg(feature = "std")]
impl<ERef> ErrorRefKind for &&ErrorTagWrapper<ERef>
where
ERef: core::ops::Deref,
ERef::Target: std::error::Error,
{
}

/// A wrapper struct for serializing errors
///
/// This struct can be used to wrap types that don't implement `slog::Value` but
Expand Down Expand Up @@ -3388,7 +3498,7 @@ where
///
/// Use [`ErrorValue`] if you want to move ownership of the error value.
#[cfg(feature = "std")]
pub struct ErrorRef<'a, E: std::error::Error>(pub &'a E);
pub struct ErrorRef<'a, E: ?Sized + std::error::Error>(pub &'a E);

#[cfg(feature = "std")]
impl<'a, E> Value for ErrorRef<'a, E>
Expand Down
47 changes: 36 additions & 11 deletions src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,30 @@ mod std_only {
info!(log, "(d2, d1, c, b2, b1, a)");
}

#[test]
fn error_ref() {
let error = TestError::new("foo");
let error = &error;
let logger = Logger::root(CheckError, o!());
info!(logger, "foo"; "error" => #error);
}

#[test]
fn error_box_ref() {
let error = TestError::new("foo");
let error = Box::new(error);
let logger = Logger::root(CheckError, o!());
info!(logger, "foo"; "error" => #&error);
}

#[test]
fn error_arc_ref() {
let error = TestError::new("foo");
let error = Arc::new(error);
let logger = Logger::root(CheckError, o!());
info!(logger, "foo"; "error" => #&error);
}

#[test]
fn error_fmt_no_source() {
let logger =
Expand All @@ -145,8 +169,8 @@ mod std_only {
let error = TestError::new("foo");
let error = &error;
let logger = Logger::root(CheckError, o!());
info!(logger, "foo"; "error" => ErrorRef(error));
slog_info!(logger, "foo"; "error" => ErrorRef(error));
info!(logger, "foo"; "error" => #error);
slog_info!(logger, "foo"; "error" => #error);
}

#[test]
Expand All @@ -164,8 +188,8 @@ mod std_only {
let error = TestError::new("foo");
let error = &error;
let logger = Logger::root(CheckError, o!());
info!(logger, "not-error: not-error; foo"; "error" => ErrorRef(error), "not-error" => "not-error");
slog_info!(logger, "not-error: not-error; foo"; "error" => ErrorRef(error), "not-error" => "not-error");
info!(logger, "not-error: not-error; foo"; "error" => #error, "not-error" => "not-error");
slog_info!(logger, "not-error: not-error; foo"; "error" => #error, "not-error" => "not-error");
}

#[test]
Expand All @@ -183,8 +207,8 @@ mod std_only {
let error = TestError::new("foo");
let error = &error;
let logger = Logger::root(CheckError, o!());
info!(logger, "foonot-error: not-error; "; "not-error" => "not-error", "error" => ErrorRef(error));
slog_info!(logger, "foonot-error: not-error; "; "not-error" => "not-error", "error" => ErrorRef(error));
info!(logger, "foonot-error: not-error; "; "not-error" => "not-error", "error" => #error);
slog_info!(logger, "foonot-error: not-error; "; "not-error" => "not-error", "error" => #error);
}

#[test]
Expand All @@ -202,8 +226,8 @@ mod std_only {
let error = TestError("foo", Some(TestError::new("bar")));
let error = &error;
let logger = Logger::root(CheckError, o!());
info!(logger, "foo: bar"; "error" => ErrorRef(error));
slog_info!(logger, "foo: bar"; "error" => ErrorRef(error));
info!(logger, "foo: bar"; "error" => #error);
slog_info!(logger, "foo: bar"; "error" => #error);
}

#[test]
Expand All @@ -224,8 +248,8 @@ mod std_only {
);
let error = &error;
let logger = Logger::root(CheckError, o!());
info!(logger, "foo: bar: baz"; "error" => ErrorRef(error));
slog_info!(logger, "foo: bar: baz"; "error" => ErrorRef(error));
info!(logger, "foo: bar: baz"; "error" => #error);
slog_info!(logger, "foo: bar: baz"; "error" => #error);
}

#[test]
Expand All @@ -234,7 +258,8 @@ mod std_only {
info!(logger, "not found"; "error" => std::io::Error::from(std::io::ErrorKind::NotFound));
// compiles?
info!(logger, "not found"; "error" => #std::io::Error::from(std::io::ErrorKind::NotFound));
info!(logger, "not found"; "error" => ErrorRef(&std::io::Error::from(std::io::ErrorKind::NotFound)));
let error = std::io::Error::from(std::io::ErrorKind::NotFound);
info!(logger, "not found"; "error" => #&error);
}
}

Expand Down

0 comments on commit e2a25e9

Please sign in to comment.