#![cfg_attr(unstable_never_type, feature(never_type))]
#![cfg_attr(unstable_try_trait_v2, feature(try_trait_v2))]
#![cfg_attr(unstable_try_trait_v2_residual, feature(try_trait_v2_residual))]
use std::{
error::Error,
ops::{ControlFlow, FromResidual, Residual, Try},
};
use tracing::Level;
#[derive(Debug, Clone, Copy)]
pub struct TracingConfig {
pub level: Level,
pub message: &'static str,
}
pub enum TracingResult<T, E: Error> {
Ok {
val: T,
config: Option<TracingConfig>,
},
Err {
err: E,
config: Option<TracingConfig>,
},
}
impl<T, E: Error> Try for TracingResult<T, E> {
type Output = T;
type Residual = TracingResult<!, E>;
fn from_output(output: Self::Output) -> Self {
Self::Ok {
val: output,
config: None,
}
}
#[track_caller]
#[inline(always)]
fn branch(self) -> ControlFlow<Self::Residual, Self::Output> {
match self {
TracingResult::Ok { val, config } => {
if let Some(cfg) = config {
match cfg.level {
Level::ERROR => tracing::error!("{}", cfg.message),
Level::WARN => tracing::warn!("{}", cfg.message),
Level::INFO => tracing::info!("{}", cfg.message),
Level::DEBUG => tracing::debug!("{}", cfg.message),
Level::TRACE => tracing::trace!("{}", cfg.message),
}
}
ControlFlow::Continue(val)
}
TracingResult::Err { err, config } => {
if let Some(cfg) = config {
match cfg.level {
Level::ERROR => tracing::error!(error = err.to_string(), "{}", cfg.message),
Level::WARN => tracing::warn!(error = err.to_string(), "{}", cfg.message),
Level::INFO => tracing::info!(error = err.to_string(), "{}", cfg.message),
Level::DEBUG => tracing::debug!(error = err.to_string(), "{}", cfg.message),
Level::TRACE => tracing::trace!(error = err.to_string(), "{}", cfg.message),
}
}
ControlFlow::Break(TracingResult::Err { err, config })
}
}
}
}
impl<T, E: Error> FromResidual for TracingResult<T, E> {
fn from_residual(residual: <Self as Try>::Residual) -> Self {
match residual {
TracingResult::Ok { .. } => unreachable!(),
TracingResult::Err { err, config } => Self::Err { err, config },
}
}
}
impl<T, E: Error> FromResidual<TracingResult<!, E>> for Result<T, E> {
fn from_residual(residual: TracingResult<!, E>) -> Self {
match residual {
TracingResult::Ok { .. } => unreachable!(),
TracingResult::Err { err, .. } => Result::Err(err),
}
}
}
impl<T, E: Error> Residual<T> for TracingResult<!, E> {
type TryType = TracingResult<T, E>;
}
pub trait Trace<T, E: Error> {
fn or_warn(self, name: &'static str) -> TracingResult<T, E>;
fn and_warn(self, name: &'static str) -> TracingResult<T, E>;
fn or_error(self, name: &'static str) -> TracingResult<T, E>;
fn and_error(self, name: &'static str) -> TracingResult<T, E>;
fn or_debug(self, name: &'static str) -> TracingResult<T, E>;
fn and_debug(self, name: &'static str) -> TracingResult<T, E>;
fn or_trace(self, name: &'static str) -> TracingResult<T, E>;
fn and_trace(self, name: &'static str) -> TracingResult<T, E>;
}
impl<T, E: Error> Trace<T, E> for Result<T, E> {
fn or_warn(self, name: &'static str) -> TracingResult<T, E> {
match self {
Ok(val) => TracingResult::Ok { val, config: None },
Err(err) => TracingResult::Err {
err,
config: Some(TracingConfig {
level: Level::WARN,
message: name,
}),
},
}
}
fn and_warn(self, name: &'static str) -> TracingResult<T, E> {
match self {
Ok(val) => TracingResult::Ok {
val,
config: Some(TracingConfig {
level: Level::WARN,
message: name,
}),
},
Err(err) => TracingResult::Err { err, config: None },
}
}
fn or_error(self, name: &'static str) -> TracingResult<T, E> {
match self {
Ok(val) => TracingResult::Ok { val, config: None },
Err(err) => TracingResult::Err {
err,
config: Some(TracingConfig {
level: Level::ERROR,
message: name,
}),
},
}
}
fn and_error(self, name: &'static str) -> TracingResult<T, E> {
match self {
Ok(val) => TracingResult::Ok {
val,
config: Some(TracingConfig {
level: Level::ERROR,
message: name,
}),
},
Err(err) => TracingResult::Err { err, config: None },
}
}
fn or_debug(self, name: &'static str) -> TracingResult<T, E> {
match self {
Ok(val) => TracingResult::Ok { val, config: None },
Err(err) => TracingResult::Err {
err,
config: Some(TracingConfig {
level: Level::DEBUG,
message: name,
}),
},
}
}
fn and_debug(self, name: &'static str) -> TracingResult<T, E> {
match self {
Ok(val) => TracingResult::Ok {
val,
config: Some(TracingConfig {
level: Level::DEBUG,
message: name,
}),
},
Err(err) => TracingResult::Err { err, config: None },
}
}
fn or_trace(self, name: &'static str) -> TracingResult<T, E> {
match self {
Ok(val) => TracingResult::Ok { val, config: None },
Err(err) => TracingResult::Err {
err,
config: Some(TracingConfig {
level: Level::TRACE,
message: name,
}),
},
}
}
fn and_trace(self, name: &'static str) -> TracingResult<T, E> {
match self {
Ok(val) => TracingResult::Ok {
val,
config: Some(TracingConfig {
level: Level::TRACE,
message: name,
}),
},
Err(err) => TracingResult::Err { err, config: None },
}
}
}
#[cfg(test)]
mod tests {
use std::io;
use tracing_test::traced_test;
use super::*;
#[traced_test]
#[test]
fn or_warn_ok() {
fn no_error() -> io::Result<()> {
Ok(()).or_warn("stuff")?;
Ok(())
}
assert!(no_error().is_ok());
assert!(!logs_contain("stuff"));
}
#[traced_test]
#[test]
fn or_warn_err() {
fn err() -> io::Result<()> {
Err(io::Error::other("oops")).or_warn("stuff")?;
Ok(())
}
assert!(err().is_err());
assert!(!logs_contain("name"));
assert!(logs_contain("stuff"));
}
#[traced_test]
#[test]
fn and_warn_ok() {
fn ok() -> io::Result<()> {
Ok(()).and_warn("ok warn")?;
Ok(())
}
assert!(ok().is_ok());
assert!(logs_contain("ok warn"));
}
#[traced_test]
#[test]
fn and_warn_err() {
fn err() -> io::Result<()> {
Err(io::Error::other("oops")).and_warn("should not log")?;
Ok(())
}
assert!(err().is_err());
assert!(!logs_contain("should not log"));
}
#[traced_test]
#[test]
fn or_error_ok() {
fn no_error() -> io::Result<()> {
Ok(()).or_error("should not log")?;
Ok(())
}
assert!(no_error().is_ok());
assert!(!logs_contain("should not log"));
}
#[traced_test]
#[test]
fn or_error_err() {
fn err() -> io::Result<()> {
Err(io::Error::other("oops")).or_error("error message")?;
Ok(())
}
assert!(err().is_err());
assert!(logs_contain("error message"));
}
#[traced_test]
#[test]
fn and_error_ok() {
fn ok() -> io::Result<()> {
Ok(()).and_error("error on ok")?;
Ok(())
}
assert!(ok().is_ok());
assert!(logs_contain("error on ok"));
}
#[traced_test]
#[test]
fn and_error_err() {
fn err() -> io::Result<()> {
Err(io::Error::other("oops")).and_error("should not log")?;
Ok(())
}
assert!(err().is_err());
assert!(!logs_contain("should not log"));
}
#[traced_test]
#[test]
fn or_debug_ok() {
fn no_error() -> io::Result<()> {
Ok(()).or_debug("should not log")?;
Ok(())
}
assert!(no_error().is_ok());
assert!(!logs_contain("should not log"));
}
#[traced_test]
#[test]
fn or_debug_err() {
fn err() -> io::Result<()> {
Err(io::Error::other("oops")).or_debug("debug message")?;
Ok(())
}
assert!(err().is_err());
assert!(logs_contain("debug message"));
}
#[traced_test]
#[test]
fn and_debug_ok() {
fn ok() -> io::Result<()> {
Ok(()).and_debug("debug on ok")?;
Ok(())
}
assert!(ok().is_ok());
assert!(logs_contain("debug on ok"));
}
#[traced_test]
#[test]
fn and_debug_err() {
fn err() -> io::Result<()> {
Err(io::Error::other("oops")).and_debug("should not log")?;
Ok(())
}
assert!(err().is_err());
assert!(!logs_contain("should not log"));
}
#[traced_test]
#[test]
fn or_trace_ok() {
fn no_error() -> io::Result<()> {
Ok(()).or_trace("should not log")?;
Ok(())
}
assert!(no_error().is_ok());
assert!(!logs_contain("should not log"));
}
#[traced_test]
#[test]
fn or_trace_err() {
fn err() -> io::Result<()> {
Err(io::Error::other("oops")).or_trace("trace message")?;
Ok(())
}
assert!(err().is_err());
assert!(logs_contain("trace message"));
}
#[traced_test]
#[test]
fn and_trace_ok() {
fn ok() -> io::Result<()> {
Ok(()).and_trace("trace on ok")?;
Ok(())
}
assert!(ok().is_ok());
assert!(logs_contain("trace on ok"));
}
#[traced_test]
#[test]
fn and_trace_err() {
fn err() -> io::Result<()> {
Err(io::Error::other("oops")).and_trace("should not log")?;
Ok(())
}
assert!(err().is_err());
assert!(!logs_contain("should not log"));
}
}