#[cfg(test)]
mod tests;
use std::fmt;
use std::error::Error;
use std::collections::HashMap;
use std::backtrace::Backtrace;
use std::sync::Arc;
pub use luhlog::Level;
pub use luhlog;
pub type ErrorSource = Box<dyn Error + Send + Sync + 'static>;
#[derive(Debug, Clone)]
pub struct ErrorContext {
pub msg: String,
pub file: Option<String>,
pub line: Option<u32>,
pub doc_link: Option<String>,
pub issues: Vec<String>,
pub metadata: HashMap<String, String>,
pub severity: Level,
}
impl fmt::Display for ErrorContext {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let sev = self.severity.to_string();
writeln!(f, "{}: {}", sev, self.msg)?;
if let Some(file) = &self.file {
if let Some(line) = self.line {
writeln!(f, " --> {}:{}", file, line)?;
} else {
writeln!(f, " --> {}", file)?;
}
}
if let Some(link) = &self.doc_link {
writeln!(f, " documentation: {}", link)?;
}
if !self.issues.is_empty() {
writeln!(f, "\nrelated issues:")?;
for issue in &self.issues {
writeln!(f, " - {}", issue)?;
}
}
if !self.metadata.is_empty() {
writeln!(f, "\nmetadata:")?;
let mut keys: Vec<_> = self.metadata.keys().collect();
keys.sort();
for key in keys {
if let Some(value) = self.metadata.get(key) {
writeln!(f, " {}: {}", key, value)?;
}
}
}
Ok(())
}
}
impl ErrorContext {
pub fn with_doc_link(mut self, link: impl Into<String>) -> Self {
self.doc_link = Some(link.into());
self
}
pub fn with_issues<I, S>(mut self, issues: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.issues.extend(issues.into_iter().map(Into::into));
self
}
pub fn with_severity(mut self, severity: Level) -> Self {
self.severity = severity;
self
}
pub fn with_metadata<K, V>(mut self, key: K, value: V) -> Self
where
K: Into<String>,
V: fmt::Display,
{
self.metadata.insert(key.into(), value.to_string());
self
}
}
pub struct AnyError {
contexts: Vec<ErrorContext>,
source: Option<ErrorSource>,
backtrace: Backtrace,
logged: std::sync::atomic::AtomicBool,
}
pub type LuhTwin<T> = Result<T, AnyError>;
impl From<std::io::Error> for AnyError {
fn from(err: std::io::Error) -> Self {
AnyError::new(err)
}
}
impl From<std::fmt::Error> for AnyError {
fn from(err: std::fmt::Error) -> Self {
AnyError::new(err)
}
}
impl From<std::string::FromUtf8Error> for AnyError {
fn from(err: std::string::FromUtf8Error) -> Self {
AnyError::new(err)
}
}
pub type BigTwin<T, E> = Result<T, E>;
impl fmt::Display for AnyError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if let Some(ctx) = self.contexts.last() {
write!(f, "{}", ctx.msg)
} else if let Some(src) = &self.source {
write!(f, "{}", src)
} else {
write!(f, "unknown error")
}
}
}
impl fmt::Debug for AnyError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(f, "\n==== error ====")?;
if !self.contexts.is_empty() {
writeln!(f, "contexts:")?;
for (i, ctx) in self.contexts.iter().enumerate() {
writeln!(f, " {}: {}", i + 1, ctx.msg)?;
}
} else {
writeln!(f, "no contexts")?;
}
if let Some(src) = &self.source {
writeln!(f, "source: {}", src)?;
}
writeln!(f, "backtrace: {}", self.backtrace)?;
writeln!(f, "logged: {}", self.logged.load(std::sync::atomic::Ordering::Relaxed))?;
Ok(())
}
}
impl Error for AnyError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
self.source
.as_deref()
.map(|e| e as &(dyn Error + 'static))
}
}
impl AnyError {
pub fn new<E>(err: E) -> Self
where
E: Error + Send + Sync + 'static,
{
Self {
contexts: vec!(),
source: Some(Box::new(err)),
backtrace: Backtrace::capture(),
logged: std::sync::atomic::AtomicBool::new(false),
}
}
pub fn mark_logged(&self) {
self.logged.store(true, std::sync::atomic::Ordering::SeqCst);
}
pub fn is_logged(&self) -> bool {
self.logged.load(std::sync::atomic::Ordering::SeqCst)
}
}
#[derive(Debug)]
pub struct ErrorSourceWrapper(pub Box<dyn CloneableError>);
impl std::fmt::Display for ErrorSourceWrapper {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
impl Clone for ErrorSourceWrapper {
fn clone(&self) -> Self {
Self(self.0.clone_box())
}
}
impl Error for ErrorSourceWrapper {
fn source(&self) -> Option<&(dyn Error + 'static)> {
Some(self.0.as_error())
}
}
pub trait CloneableError: Error + Send + Sync {
fn clone_box(&self) -> Box<dyn CloneableError>;
fn as_error(&self) -> &(dyn Error + 'static);
fn into_error_box(self: Box<Self>) -> Box<dyn Error + Send + Sync>;
}
#[derive(Debug)]
pub struct NonCloneableWrapper {
msg: String,
}
impl NonCloneableWrapper {
pub fn new<E: std::error::Error>(err: &E) -> Self {
Self { msg: err.to_string() }
}
}
impl std::fmt::Display for NonCloneableWrapper {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.msg)
}
}
impl std::error::Error for NonCloneableWrapper {}
impl CloneableError for NonCloneableWrapper {
fn clone_box(&self) -> Box<dyn CloneableError> {
Box::new(self.clone())
}
fn as_error(&self) -> &(dyn std::error::Error + 'static) {
self
}
fn into_error_box(self: Box<Self>) -> Box<dyn std::error::Error + Send + Sync> {
self
}
}
pub trait ErrorClonableExt: Error + Send + Sync + 'static {
fn make_clonable(&self) -> Box<dyn CloneableError>;
}
impl<T> ErrorClonableExt for T
where
T: Error + Send + Sync + 'static,
{
fn make_clonable(&self) -> Box<dyn CloneableError> {
if let Some(clonable) = (self as &dyn std::any::Any).downcast_ref::<Box<dyn CloneableError>>() {
return clonable.clone_box();
}
Box::new(NonCloneableWrapper::new(self))
}
}
impl Clone for NonCloneableWrapper {
fn clone(&self) -> Self {
Self { msg: self.msg.clone() }
}
}
#[derive(Debug, Clone)]
pub struct AnyErrorBuilder {
ctx: ErrorContext,
source: Option<Arc<ErrorSourceWrapper>>,
}
impl AnyErrorBuilder {
pub fn new(msg: impl Into<String>) -> Self {
Self {
ctx: ErrorContext {
msg: msg.into(),
file: Some(file!().to_string()),
line: Some(line!()),
doc_link: None,
issues: vec![],
metadata: HashMap::new(),
severity: Level::Error,
},
source: None,
}
}
pub fn doc_link(mut self, link: impl Into<String>) -> Self {
self.ctx.doc_link = Some(link.into());
self
}
pub fn issues<I, S>(mut self, issues: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.ctx.issues.extend(issues.into_iter().map(Into::into));
self
}
pub fn metadata<K, V>(mut self, key: K, value: V) -> Self
where
K: Into<String>,
V: fmt::Display,
{
self.ctx.metadata.insert(key.into(), value.to_string());
self
}
pub fn severity(mut self, severity: Level) -> Self {
self.ctx.severity = severity;
self
}
pub fn source<E>(mut self, err: E) -> Self
where
E: Error + Send + Sync + 'static,
{
self.source = Some(Arc::new(ErrorSourceWrapper(err.make_clonable())));
self
}
pub fn build(self) -> AnyError {
AnyError {
contexts: vec![self.ctx],
source: self.source.map(|arc_err| {
let wrapper: Box<dyn CloneableError> = match Arc::try_unwrap(arc_err) {
Ok(wrapper) => wrapper.0,
Err(shared) => shared.0.clone_box(),
};
wrapper.into_error_box()
}),
backtrace: Backtrace::capture(),
logged: std::sync::atomic::AtomicBool::new(false),
}
}
}
impl AnyError {
pub fn with_context(mut self, ctx: ErrorContext) -> Self {
self.contexts.push(ctx);
self
}
pub fn max_severity(&self) -> Level {
self.contexts
.iter()
.map(|c| c.severity)
.max()
.unwrap_or(Level::Error)
}
pub fn to_log_format(&self) -> String {
let mut parts = vec![];
if let Some(ctx) = self.contexts.last() {
parts.push(format!("message=\"{}\"", ctx.msg.replace('"', "\\\"")));
}
parts.push(format!("severity={}", self.max_severity()));
if let Some(ctx) = self.contexts.last() {
if let Some(file) = &ctx.file {
if let Some(line) = ctx.line {
parts.push(format!("location=\"{}:{}\"", file, line));
}
}
}
if let Some(src) = &self.source {
parts.push(format!("source=\"{}\"", src.to_string().replace('"', "\\\"")));
}
parts.join(" ")
}
pub fn display_pretty(&self) -> String {
const RED: &str = "\x1b[31m";
const YELLOW: &str = "\x1b[33m";
const BLUE: &str = "\x1b[34m";
const GRAY: &str = "\x1b[90m";
const RESET: &str = "\x1b[0m";
const BOLD: &str = "\x1b[1m";
let mut result = String::new();
if let Some(ctx) = self.contexts.last() {
let color = match ctx.severity {
Level::Critical | Level::Error => RED,
Level::Warn => YELLOW,
Level::Info => BLUE,
Level::Debug => GRAY,
Level::Trace => BOLD,
};
result.push_str(&format!("{}{}error:{} {}{}\n",
BOLD, color, RESET, BOLD, ctx.msg));
result.push_str(RESET);
if let Some(file) = &ctx.file {
if let Some(line) = ctx.line {
result.push_str(&format!(" {}{}-->{} {}:{}\n",
BOLD, BLUE, RESET, file, line));
}
}
}
if self.contexts.len() > 1 {
result.push_str(&format!("\n{}context chain:{}\n", BOLD, RESET));
for (i, ctx) in self.contexts.iter().rev().enumerate() {
result.push_str(&format!(" {}. {}\n", i + 1, ctx.msg));
}
}
if let Some(src) = &self.source {
result.push_str(&format!("\n{}caused by:{} {}\n", BOLD, RESET, src));
}
result
}
pub fn display_contexts(&self) -> String {
let mut result = String::new();
for (i, ctx) in self.contexts.iter().rev().enumerate() {
let mut line = format!("{}: {}", i + 1, ctx.msg);
if let Some(file) = &ctx.file {
if let Some(line_num) = ctx.line {
line.push_str(&format!(" ({}:{})", file, line_num));
} else {
line.push_str(&format!(" ({})", file));
}
}
result.push_str(&line);
if let Some(link) = &ctx.doc_link {
result.push_str(&format!("\n [doc: {}]", link));
}
if !ctx.issues.is_empty() {
result.push_str(&format!("\n [issues: {}]", ctx.issues.join(", ")));
}
if i != self.contexts.len() - 1 {
result.push_str("\n-> ");
}
}
result
}
pub fn display_contexts_tree(&self) -> String {
let mut result = String::new();
let last_idx = self.contexts.len().saturating_sub(1);
for (i, ctx) in self.contexts.iter().rev().enumerate() {
let is_last = i == last_idx;
let prefix = if is_last { "└─ " } else { "├─ " };
let child_prefix = if is_last { " " } else { "│ " };
result.push_str(&format!("{}{}\n", prefix, ctx.msg));
if let Some(file) = &ctx.file {
if let Some(line) = ctx.line {
result.push_str(&format!("{}at {}:{}\n", child_prefix, file, line));
} else {
result.push_str(&format!("{}at {}\n", child_prefix, file));
}
}
if let Some(link) = &ctx.doc_link {
result.push_str(&format!("{}doc: {}\n", child_prefix, link));
}
for issue in &ctx.issues {
result.push_str(&format!("{}issue: {}\n", child_prefix, issue));
}
}
result
}
pub fn display_backtrace(&self) -> String {
format!("{}", self.backtrace)
}
pub fn display_full(&self) -> String {
let mut result = String::new();
result.push_str("=== error report ===\n\n");
result.push_str(&format!("severity: {}\n", self.max_severity()));
result.push_str(&format!("message: {}\n\n", self));
if !self.contexts.is_empty() {
result.push_str("context chain:\n");
result.push_str(&self.display_contexts_tree());
result.push_str("\n");
}
if let Some(src) = &self.source {
result.push_str(&format!("root cause: {}\n\n", src));
result.push_str("error chain:\n");
for (i, err) in self.iter_sources().enumerate() {
result.push_str(&format!(" {}: {}\n", i, err));
}
result.push_str("\n");
}
result.push_str("backtrace:\n");
result.push_str(&self.display_backtrace());
result
}
}
impl AnyError {
pub fn root_cause(&self) -> &(dyn Error + 'static) {
let mut source: &(dyn Error + 'static) = self;
while let Some(s) = source.source() {
source = s;
}
source
}
pub fn iter_sources(&self) -> impl Iterator<Item = &(dyn Error + 'static)> + '_ {
std::iter::successors(Some(self as &(dyn Error + 'static)), |&e| e.source())
}
}
impl From<ErrorContext> for AnyError {
fn from(ctx: ErrorContext) -> Self {
AnyError {
contexts: vec![ctx],
source: None,
backtrace: Backtrace::capture(),
logged: std::sync::atomic::AtomicBool::new(false),
}
}
}
impl ErrorContext {
pub fn into_error(self) -> AnyError {
self.into()
}
}
pub trait Context<T> {
fn context<C>(self, msg: C) -> LuhTwin<T>
where
C: fmt::Display;
fn with_context<C, F>(self, f: F) -> LuhTwin<T>
where
C: fmt::Display,
F: FnOnce() -> C;
}
impl<T, E> Context<T> for BigTwin<T, E>
where
E: Error + Send + Sync + 'static,
{
fn context<C>(self, msg: C) -> LuhTwin<T>
where
C: fmt::Display,
{
self.map_err(|e| AnyError::new(e).with_context(at!(msg)))
}
fn with_context<C, F>(self, f: F) -> LuhTwin<T>
where
C: fmt::Display,
F: FnOnce() -> C,
{
self.map_err(|e| AnyError::new(e).with_context(at!(f())))
}
}
pub trait WrapErrExt<T> {
fn wrap<F, C>(self, f: F) -> LuhTwin<T>
where
F: FnOnce() -> C,
C: fmt::Display;
}
impl<T, E> WrapErrExt<T> for BigTwin<T, E>
where
E: Error + Send + Sync + 'static,
{
fn wrap<F, C>(self, f: F) -> LuhTwin<T>
where
F: FnOnce() -> C,
C: fmt::Display,
{
self.map_err(|e| {
let msg = format!("{}: {}", f(), e);
AnyError::new(e).with_context(at!(msg))
})
}
}
pub trait LogError<T> {
fn log_error(self) -> Self;
fn log_error_with(self, prefix: &str) -> Self;
fn log_once(self) -> Self;
}
impl<T> LogError<T> for LuhTwin<T> {
fn log_error(self) -> Self {
if let Err(ref e) = self {
eprintln!("{}", e.display_pretty());
}
self
}
fn log_error_with(self, prefix: &str) -> Self {
if let Err(ref e) = self {
eprintln!("{}: {}", prefix, e.display_pretty());
}
self
}
fn log_once(self) -> Self {
if let Err(ref e) = self {
if !e.is_logged() {
eprintln!("{}", e.display_pretty());
e.mark_logged();
}
}
self
}
}
#[macro_export]
macro_rules! anyerror {
($msg:expr) => {
$crate::AnyErrorBuilder::new($msg)
};
($fmt:expr, $($arg:tt)*) => {
$crate::AnyErrorBuilder::new(format!($fmt, $($arg)*))
};
}
#[macro_export]
macro_rules! at {
() => {
$crate::ErrorContext {
msg: "unknown error".to_string(),
file: Some(file!().to_string()),
line: Some(line!()),
doc_link: None,
issues: vec![],
severity: $crate::luhlog::Level::Error,
metadata: std::collections::HashMap::new(),
}
};
($fmt:literal, $($arg:expr),+ ; $severity:expr $(,)?) => {
$crate::ErrorContext {
msg: format!($fmt, $($arg),+),
file: Some(file!().to_string()),
line: Some(line!()),
doc_link: None,
issues: vec![],
severity: $severity,
metadata: std::collections::HashMap::new(),
}
};
($fmt:literal, $($arg:expr),+ $(,)?) => {
$crate::ErrorContext {
msg: format!($fmt, $($arg),+),
file: Some(file!().to_string()),
line: Some(line!()),
doc_link: None,
issues: vec![],
severity: $crate::luhlog::Level::Error,
metadata: std::collections::HashMap::new(),
}
};
($msg:expr ; $severity:expr) => {
$crate::ErrorContext {
msg: $msg.to_string(),
file: Some(file!().to_string()),
line: Some(line!()),
doc_link: None,
issues: vec![],
severity: $severity,
metadata: std::collections::HashMap::new(),
}
};
($msg:expr) => {
$crate::ErrorContext {
msg: $msg.to_string(),
file: Some(file!().to_string()),
line: Some(line!()),
doc_link: None,
issues: vec![],
severity: $crate::luhlog::Level::Error,
metadata: std::collections::HashMap::new(),
}
};
}
#[macro_export]
macro_rules! bail {
($($arg:tt)*) => {
return Err($crate::AnyError::new(std::io::Error::new(
std::io::ErrorKind::Other,
format!($($arg)*),
)))
};
}
#[macro_export]
macro_rules! ensure {
($cond:expr, $($arg:tt)*) => {
if !$cond {
$crate::bail!($($arg)*);
}
};
}
#[macro_export]
macro_rules! context {
($res:expr, $msg:expr) => {
$res.with_context(|| $msg)
};
}