diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 9371b90f..493d2fff 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -288,6 +288,7 @@ dependencies = [ "colored", "comfy-table", "deadpool-redis", + "error-stack", "homedir", "once_cell", "opentelemetry", @@ -621,6 +622,16 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "error-stack" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27a72baa257b5e0e2de241967bc5ee8f855d6072351042688621081d66b2a76b" +dependencies = [ + "anyhow", + "rustc_version", +] + [[package]] name = "fastrand" version = "2.0.1" diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 0a2d0151..64514eae 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -8,6 +8,7 @@ shlex = '1.2' spez = '0.1' tracing = '0.1' tracing-appender = '0.2' +error-stack = '0.4' [dependencies.aide] features = ['axum'] diff --git a/rust/bitbazaar/cli/mod.rs b/rust/bitbazaar/cli/mod.rs index d57b6865..28d0a4ab 100644 --- a/rust/bitbazaar/cli/mod.rs +++ b/rust/bitbazaar/cli/mod.rs @@ -7,7 +7,7 @@ mod tests { use rstest::*; use super::*; - use crate::errors::TracedErr; + use crate::{aer, errors::prelude::*}; #[rstest] // <-- basics: @@ -38,8 +38,8 @@ mod tests { #[case] cmd_str: &str, #[case] exp_std_all: S, #[case] code: i32, - ) -> Result<(), TracedErr> { - let res = run_cmd(cmd_str)?; + ) -> Result<(), AnyErr> { + let res = aer!(run_cmd(cmd_str))?; assert_eq!(res.code, code, "{}", res.std_all()); assert_eq!(res.std_all().trim(), exp_std_all.into()); Ok(()) diff --git a/rust/bitbazaar/cli/run_cmd.rs b/rust/bitbazaar/cli/run_cmd.rs index 0817ab61..f8674c6a 100644 --- a/rust/bitbazaar/cli/run_cmd.rs +++ b/rust/bitbazaar/cli/run_cmd.rs @@ -1,4 +1,4 @@ -use crate::errors::TracedResult; +use crate::errors::prelude::*; /// The result of running a command pub struct CmdOut { @@ -28,6 +28,22 @@ impl CmdOut { } } +#[derive(Debug)] +pub enum CmdErr { + /// An arbitrary downstream error: + Unknown(String), +} + +impl std::fmt::Display for CmdErr { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + CmdErr::Unknown(msg) => write!(f, "{}", msg), + } + } +} + +impl error_stack::Context for CmdErr {} + /// Run a dynamic shell command and return the output. /// /// WARNING: this opens up the possibility of dependency injection attacks, so should only be used when the command is trusted. @@ -38,12 +54,13 @@ impl CmdOut { /// - `||` or /// - `|` pipe /// - `~` home dir -pub fn run_cmd>(cmd_str: S) -> TracedResult { +pub fn run_cmd>(cmd_str: S) -> Result { let (code, output, error) = run_script::run( cmd_str.into().as_str(), &vec![], &run_script::ScriptOptions::new(), - )?; + ) + .map_err(|e| CmdErr::Unknown(e.to_string()))?; Ok(CmdOut { stdout: output, diff --git a/rust/bitbazaar/errors/any.rs b/rust/bitbazaar/errors/any.rs new file mode 100644 index 00000000..e8ebf28b --- /dev/null +++ b/rust/bitbazaar/errors/any.rs @@ -0,0 +1,13 @@ +use error_stack::Context; + +/// A generic trace_stack error to use when you don't want to create custom error types. +#[derive(Debug, Default)] +pub struct AnyErr; + +impl std::fmt::Display for AnyErr { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "AnyErr") + } +} + +impl Context for AnyErr {} diff --git a/rust/bitbazaar/errors/macros.rs b/rust/bitbazaar/errors/macros.rs index d0425724..0f545219 100644 --- a/rust/bitbazaar/errors/macros.rs +++ b/rust/bitbazaar/errors/macros.rs @@ -1,3 +1,88 @@ +#[macro_export] +/// A helper for the aer! macro. +macro_rules! _aer_inner { + ($any_err:expr) => {{ + use error_stack::{Context, Report, Result, ResultExt}; + + $crate::spez! { + for x = $any_err; + + // error_stack err -> Report + match T -> Report { + Report::new(x).change_context(AnyErr) + } + // Error + Sync + Send + 'static -> Report + match T -> Report { + Report::new(x).change_context(AnyErr) + } + // Report -> Report + match Report -> Report { + x.change_context(AnyErr) + } + // error_stack::Result -> Result + match Result -> Result { + x.change_context(AnyErr) + } + // core::result::Result -> Result + match core::result::Result -> Result { + x.change_context(AnyErr) + } + // Into -> Report + match> T -> Report { + Report::new(AnyErr).attach_printable(x.into()) + } + } + }}; +} + +#[macro_export] +/// A helper for the aer! macro. +macro_rules! _aer_inner_with_txt { + ($err_or_result_or_str:expr, $str:expr) => {{ + use error_stack::{Context, Report, Result, ResultExt}; + use $crate::_aer_inner; + + let foo = _aer_inner!($err_or_result_or_str); + $crate::spez! { + for x = foo; + + // Not lazy if already a report: + match Report -> Report { + x.attach_printable($str) + } + + // Otherwise, lazy: + match Result -> Result { + x.attach_printable($str) + } + } + }}; +} + +/// A macro for building `AnyErr` reports easily from other error_stack errors, other reports and base errors outside of error_stack. +#[macro_export] +macro_rules! aer { + () => {{ + use error_stack::Report; + Report::new(AnyErr) + }}; + + ($err_or_result_or_str:expr) => {{ + use $crate::_aer_inner; + _aer_inner!($err_or_result_or_str) + }}; + + ($err_or_result_or_str:expr, $str:expr) => {{ + use $crate::_aer_inner_with_txt; + _aer_inner_with_txt!($err_or_result_or_str, $str) + }}; + + ($err_or_result_or_str:expr, $str:expr, $($arg:expr),*) => {{ + use $crate::_aer_inner_with_txt; + _aer_inner_with_txt!($err_or_result_or_str, format!($str, $($arg),*)) + }}; +} + /// A macro for creating a TracedErr from a string or another existing error. #[macro_export] macro_rules! err { diff --git a/rust/bitbazaar/errors/mod.rs b/rust/bitbazaar/errors/mod.rs index 7a329f75..d09567f5 100755 --- a/rust/bitbazaar/errors/mod.rs +++ b/rust/bitbazaar/errors/mod.rs @@ -1,3 +1,4 @@ +mod any; mod generic_err; mod macros; mod test_errs; @@ -7,6 +8,14 @@ mod traced_error; pub use traced_error::set_axum_debug; pub use traced_error::{TracedErr, TracedResult}; +pub(crate) mod prelude { + pub use error_stack::{bail, report, Result, ResultExt}; + + pub use super::any::AnyErr; + #[allow(unused_imports)] + pub use crate::aer; +} + #[cfg(test)] mod tests { use colored::Colorize; diff --git a/rust/bitbazaar/logging/create_subscriber.rs b/rust/bitbazaar/logging/create_subscriber.rs index fb6bce5f..2722d86d 100644 --- a/rust/bitbazaar/logging/create_subscriber.rs +++ b/rust/bitbazaar/logging/create_subscriber.rs @@ -2,13 +2,14 @@ use std::{path::PathBuf, str::FromStr}; use once_cell::sync::Lazy; use parking_lot::Mutex; +use tonic::metadata::{Ascii, MetadataValue}; use tracing::{Dispatch, Level, Metadata, Subscriber}; use tracing_appender::non_blocking::WorkerGuard; use tracing_subscriber::{ filter::FilterFn, fmt::MakeWriter, prelude::*, registry::LookupSpan, Layer, }; -use crate::{err, errors::TracedErr}; +use crate::errors::prelude::*; /// Specify which logs should be matched by this layer. /// @@ -235,7 +236,7 @@ impl CreatedSubscriber { /// }]).unwrap(); /// sub.into_global(); // Register it as the global logger, this can only be done once /// ``` -pub fn create_subscriber(layers: Vec) -> Result { +pub fn create_subscriber(layers: Vec) -> Result { let all_loc_matchers = layers .iter() .filter_map(|target| target.loc_matcher.clone()) @@ -263,15 +264,15 @@ pub fn create_subscriber(layers: Vec) -> Result { // Throw if dir is an existing file: if dir.is_file() { - return Err(err!( + bail!(report!(AnyErr).attach_printable(format!( "Log directory is an existing file: {}", dir.to_string_lossy() - )); + ))); } // Create the dir if missing: if !dir.exists() { - std::fs::create_dir_all(&dir)?; + std::fs::create_dir_all(&dir).change_context(AnyErr)?; } // Rotate the file daily: @@ -306,8 +307,10 @@ pub fn create_subscriber(layers: Vec) -> Result>() + .change_context(AnyErr)?, ); } @@ -320,7 +323,8 @@ pub fn create_subscriber(layers: Vec) -> Result, all_loc_matchers: &[regex::Regex], -) -> Result) -> bool>, TracedErr> { +) -> Result) -> bool>, AnyErr> { // Needs to be a vec to pass through to the filter fn: let all_loc_matchers = all_loc_matchers.to_vec(); @@ -395,7 +399,7 @@ fn create_fmt_layer( include_loc: bool, include_color: bool, writer: W, -) -> Result + Send + Sync + 'static>, TracedErr> +) -> Result + Send + Sync + 'static>, AnyErr> where S: Subscriber, for<'a> S: LookupSpan<'a>, // Each layer has a different type, so have to box for return @@ -415,7 +419,8 @@ where // also no need for any more than ms precision, // also make it a UTC time: let timer = - time::format_description::parse("[hour]:[minute]:[second].[subsecond digits:3]")?; + time::format_description::parse("[hour]:[minute]:[second].[subsecond digits:3]") + .change_context(AnyErr)?; let time_offset = time::UtcOffset::current_local_offset().unwrap_or(time::UtcOffset::UTC); let timer = tracing_subscriber::fmt::time::OffsetTime::new(time_offset, timer); diff --git a/rust/bitbazaar/logging/mod.rs b/rust/bitbazaar/logging/mod.rs index cc04da7f..ab22c645 100644 --- a/rust/bitbazaar/logging/mod.rs +++ b/rust/bitbazaar/logging/mod.rs @@ -20,7 +20,7 @@ mod tests { use tracing::{debug, error, info, warn, Level}; use super::*; - use crate::errors::TracedErr; + use crate::errors::prelude::*; fn log_all() { debug!("DLOG"); @@ -40,7 +40,7 @@ mod tests { #[values(true, false)] include_lvl: bool, #[values(true, false)] include_timestamp: bool, #[values(true, false)] include_loc: bool, - ) -> Result<(), TracedErr> { + ) -> Result<(), AnyErr> { static LOGS: Lazy>> = Lazy::new(Mutex::default); { // Fn repeat usage so static needs clearing each time: @@ -68,7 +68,7 @@ mod tests { log_all(); }); - let chk_log = |lvl: Level, in_log: &str, out_log: &str| -> Result<(), TracedErr> { + let chk_log = |lvl: Level, in_log: &str, out_log: &str| -> Result<(), AnyErr> { if include_lvl { assert!( out_log.contains(&lvl.to_string().to_uppercase()), @@ -81,7 +81,7 @@ mod tests { } if include_timestamp { // Confirm matches regex HH:MM:SS.mmm: - assert!(regex::Regex::new(r"\d{2}:\d{2}:\d{2}.\d{3}")?.is_match(out_log)); + assert!(aer!(regex::Regex::new(r"\d{2}:\d{2}:\d{2}.\d{3}"))?.is_match(out_log)); } // Should end with the actual log: assert!(out_log.ends_with(in_log), "{}", out_log); @@ -100,7 +100,7 @@ mod tests { } #[rstest] - fn test_log_pretty() -> Result<(), TracedErr> { + fn test_log_pretty() -> Result<(), AnyErr> { static LOGS: Lazy>> = Lazy::new(Mutex::default); let sub = create_subscriber(vec![SubLayer { @@ -134,7 +134,7 @@ mod tests { } #[rstest] - fn test_log_color() -> Result<(), TracedErr> { + fn test_log_color() -> Result<(), AnyErr> { static LOGS: Lazy>> = Lazy::new(Mutex::default); let sub = create_subscriber(vec![SubLayer { @@ -177,7 +177,7 @@ mod tests { fn test_log_matchers( #[case] loc_matcher: Option, #[case] expected_logs: Vec<&str>, - ) -> Result<(), TracedErr> { + ) -> Result<(), AnyErr> { static LOGS: Lazy>> = Lazy::new(Mutex::default); { // Fn repeat usage so static needs clearing each time: @@ -239,7 +239,7 @@ mod tests { fn test_log_filtering( #[case] filter: SubLayerFilter, #[case] expected_found: Vec<&str>, - ) -> Result<(), TracedErr> { + ) -> Result<(), AnyErr> { static LOGS: Lazy>> = Lazy::new(Mutex::default); { // Fn repeat usage so static needs clearing each time: @@ -288,8 +288,8 @@ mod tests { } #[rstest] - fn test_log_to_file() -> Result<(), TracedErr> { - let temp_dir = tempdir()?; + fn test_log_to_file() -> Result<(), AnyErr> { + let temp_dir = aer!(tempdir())?; let sub = create_subscriber(vec![SubLayer { filter: SubLayerFilter::Above(Level::DEBUG), variant: SubLayerVariant::File { @@ -306,9 +306,7 @@ mod tests { // Sleep for 50ms to make sure everything's been flushed to the file: (happens in separate thread) std::thread::sleep(std::time::Duration::from_millis(50)); - let files: HashMap = temp_dir - .path() - .read_dir()? + let files: HashMap = aer!(temp_dir.path().read_dir())? .map(|entry| { let entry = entry.unwrap(); let path = entry.path(); @@ -326,7 +324,7 @@ mod tests { let contents = files.get(name).unwrap(); // Check name matches "foo.log%Y-%m-%d" with regex: - let re = regex::Regex::new(r"^foo.log.\d{4}-\d{2}-\d{2}$")?; + let re = aer!(regex::Regex::new(r"^foo.log.\d{4}-\d{2}-\d{2}$"))?; assert!(re.is_match(name), "{}", name); let out = contents.lines().collect::>(); @@ -342,7 +340,7 @@ mod tests { #[cfg(feature = "opentelemetry")] #[rstest] #[tokio::test] - async fn test_opentelemetry() -> Result<(), TracedErr> { + async fn test_opentelemetry() -> Result<(), AnyErr> { // Not actually going to implement a fake collector on the other side, just check nothing errors: let sub = create_subscriber(vec![SubLayer { diff --git a/rust/bitbazaar/redis/conn.rs b/rust/bitbazaar/redis/conn.rs index 0f37aa41..7b263080 100644 --- a/rust/bitbazaar/redis/conn.rs +++ b/rust/bitbazaar/redis/conn.rs @@ -3,7 +3,7 @@ use std::{borrow::Cow, future::Future}; use deadpool_redis::redis::{FromRedisValue, ToRedisArgs}; use super::batch::RedisBatch; -use crate::errors::TracedResult; +use crate::errors::prelude::*; /// Wrapper around a lazy redis connection. pub struct RedisConn<'a> { @@ -55,10 +55,10 @@ impl<'a> RedisConn<'a> { key: K, expiry: Option, cb: impl FnOnce() -> Fut, - ) -> TracedResult + ) -> Result where T: FromRedisValue + ToRedisArgs, - Fut: Future>, + Fut: Future>, { let key: Cow<'b, str> = key.into(); diff --git a/rust/bitbazaar/redis/mod.rs b/rust/bitbazaar/redis/mod.rs index 56bf3332..079c5e3d 100644 --- a/rust/bitbazaar/redis/mod.rs +++ b/rust/bitbazaar/redis/mod.rs @@ -22,7 +22,7 @@ mod tests { use rstest::*; use super::*; - use crate::{errors::TracedErr, misc::in_ci}; + use crate::{errors::prelude::*, misc::in_ci}; struct ChildGuard(Child); @@ -65,7 +65,7 @@ mod tests { /// Test functionality working as it should when redis up and running fine. #[rstest] #[tokio::test] - async fn test_redis_working() -> Result<(), TracedErr> { + async fn test_redis_working() -> Result<(), AnyErr> { // Can enable to check logging when debugging: // let sub = crate::logging::create_subscriber(vec![crate::logging::SubLayer { // filter: crate::logging::SubLayerFilter::Above(tracing::Level::TRACE), diff --git a/rust/bitbazaar/redis/wrapper.rs b/rust/bitbazaar/redis/wrapper.rs index e5bdcd21..9d0f88c8 100644 --- a/rust/bitbazaar/redis/wrapper.rs +++ b/rust/bitbazaar/redis/wrapper.rs @@ -1,7 +1,7 @@ use deadpool_redis::{Config, Runtime}; use super::RedisConn; -use crate::errors::TracedResult; +use crate::errors::prelude::*; /// A wrapper around redis to make it more concise to use and not need redis in the downstream Cargo.toml. /// @@ -19,9 +19,11 @@ impl Redis { pub fn new, B: Into>( redis_conn_str: A, prefix: B, - ) -> TracedResult { + ) -> Result { let cfg = Config::from_url(redis_conn_str); - let pool = cfg.create_pool(Some(Runtime::Tokio1))?; + let pool = cfg + .create_pool(Some(Runtime::Tokio1)) + .change_context(AnyErr)?; Ok(Self { pool, diff --git a/rust/bitbazaar/timing/recorder.rs b/rust/bitbazaar/timing/recorder.rs index 23520ddb..7cd5ee58 100644 --- a/rust/bitbazaar/timing/recorder.rs +++ b/rust/bitbazaar/timing/recorder.rs @@ -4,7 +4,7 @@ use once_cell::sync::Lazy; use parking_lot::Mutex; use tracing::warn; -use crate::{err, errors::TracedErr, timing::format_duration}; +use crate::{aer, errors::prelude::*, timing::format_duration}; /// A global time recorder, used by the timeit! macro. pub static GLOBAL_TIME_RECORDER: Lazy = Lazy::new(TimeRecorder::new); @@ -86,19 +86,21 @@ impl TimeRecorder { } /// Using from creation time rather than the specific durations recorded, to be sure to cover everything. - pub fn total_elapsed(&self) -> Result { - Ok((chrono::Utc::now() - self.start).to_std()?) + pub fn total_elapsed(&self) -> Result { + (chrono::Utc::now() - self.start) + .to_std() + .change_context(AnyErr) } /// Format the logs in a verbose, table format. - pub fn format_verbose(&self) -> Result { + pub fn format_verbose(&self) -> Result { use comfy_table::*; // Printing should only happen at the end synchronously, shouldn't fail to acquire: let logs = self .logs .try_lock() - .ok_or_else(|| err!("Failed to acquire logs."))?; + .ok_or_else(|| aer!("Failed to acquire logs."))?; let mut table = Table::new(); table @@ -118,7 +120,7 @@ impl TimeRecorder { // Centralize the time column: let time_column = table .column_mut(1) - .ok_or_else(|| err!("Failed to get second column of time recorder table"))?; + .ok_or_else(|| aer!("Failed to get second column of time recorder table"))?; time_column.set_cell_alignment(CellAlignment::Center); Ok(table.to_string())