#![warn(missing_docs)]
use std::collections::HashMap;
use std::env;
use std::fmt;
use std::sync::OnceLock;
#[derive(Debug)]
pub struct DecrustBacktrace {
inner: Option<std::backtrace::Backtrace>,
capture_enabled: bool,
capture_timestamp: std::time::SystemTime,
thread_id: std::thread::ThreadId,
thread_name: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BacktraceStatus {
Captured,
Disabled,
Unsupported,
}
impl DecrustBacktrace {
pub fn capture() -> Self {
let should_capture = Self::should_capture_from_env();
let current_thread = std::thread::current();
if should_capture {
Self {
inner: Some(std::backtrace::Backtrace::capture()),
capture_enabled: true,
capture_timestamp: std::time::SystemTime::now(),
thread_id: current_thread.id(),
thread_name: current_thread.name().map(|s| s.to_string()),
}
} else {
Self {
inner: None,
capture_enabled: false,
capture_timestamp: std::time::SystemTime::now(),
thread_id: current_thread.id(),
thread_name: current_thread.name().map(|s| s.to_string()),
}
}
}
pub fn force_capture() -> Self {
let current_thread = std::thread::current();
Self {
inner: Some(std::backtrace::Backtrace::force_capture()),
capture_enabled: true,
capture_timestamp: std::time::SystemTime::now(),
thread_id: current_thread.id(),
thread_name: current_thread.name().map(|s| s.to_string()),
}
}
pub fn disabled() -> Self {
let current_thread = std::thread::current();
Self {
inner: None,
capture_enabled: false,
capture_timestamp: std::time::SystemTime::now(),
thread_id: current_thread.id(),
thread_name: current_thread.name().map(|s| s.to_string()),
}
}
pub fn status(&self) -> BacktraceStatus {
match &self.inner {
Some(bt) => {
use std::backtrace::BacktraceStatus as StdStatus;
match bt.status() {
StdStatus::Captured => BacktraceStatus::Captured,
StdStatus::Disabled => BacktraceStatus::Disabled,
StdStatus::Unsupported => BacktraceStatus::Unsupported,
#[allow(unreachable_patterns)]
_ => BacktraceStatus::Unsupported,
}
}
None => BacktraceStatus::Disabled,
}
}
pub fn capture_timestamp(&self) -> std::time::SystemTime {
self.capture_timestamp
}
pub fn thread_id(&self) -> std::thread::ThreadId {
self.thread_id
}
pub fn thread_name(&self) -> Option<&str> {
self.thread_name.as_deref()
}
fn should_capture_from_env() -> bool {
static SHOULD_CAPTURE: OnceLock<bool> = OnceLock::new();
*SHOULD_CAPTURE.get_or_init(|| {
if let Ok(val) = env::var("RUST_LIB_BACKTRACE") {
return val == "1" || val.to_lowercase() == "full";
}
if let Ok(val) = env::var("RUST_BACKTRACE") {
return val == "1" || val.to_lowercase() == "full";
}
false
})
}
pub fn as_std_backtrace(&self) -> Option<&std::backtrace::Backtrace> {
self.inner.as_ref()
}
pub fn extract_frames(&self) -> Vec<BacktraceFrame> {
match &self.inner {
Some(bt) => {
let bt_string = format!("{}", bt);
self.parse_backtrace_string(&bt_string)
}
None => Vec::new(),
}
}
fn parse_backtrace_string(&self, bt_str: &str) -> Vec<BacktraceFrame> {
let mut frames = Vec::new();
for line in bt_str.lines() {
if let Some(frame) = self.parse_frame_line(line) {
frames.push(frame);
}
}
frames
}
fn parse_frame_line(&self, line: &str) -> Option<BacktraceFrame> {
let trimmed = line.trim();
if let Some(colon_pos) = trimmed.find(':') {
let number_part = &trimmed[..colon_pos].trim();
let rest = &trimmed[colon_pos + 1..].trim();
if number_part.parse::<usize>().is_ok() {
if let Some(at_pos) = rest.rfind(" at ") {
let symbol = rest[..at_pos].trim().to_string();
let location = rest[at_pos + 4..].trim();
let (file, line, column) = self.parse_location(location);
return Some(BacktraceFrame {
symbol,
file,
line,
column,
});
} else {
return Some(BacktraceFrame {
symbol: rest.to_string(),
file: None,
line: None,
column: None,
});
}
}
}
None
}
fn parse_location(&self, location: &str) -> (Option<String>, Option<u32>, Option<u32>) {
let parts: Vec<&str> = location.rsplitn(3, ':').collect();
match parts.len() {
3 => {
let column = parts[0].parse().ok();
let line = parts[1].parse().ok();
let file = Some(parts[2].to_string());
(file, line, column)
}
2 => {
let line = parts[0].parse().ok();
let file = Some(parts[1].to_string());
(file, line, None)
}
1 => (Some(parts[0].to_string()), None, None),
_ => (None, None, None),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BacktraceFrame {
pub symbol: String,
pub file: Option<String>,
pub line: Option<u32>,
pub column: Option<u32>,
}
impl fmt::Display for BacktraceFrame {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.symbol)?;
if let Some(ref file) = self.file {
write!(f, " at {}", file)?;
if let Some(line) = self.line {
write!(f, ":{}", line)?;
if let Some(column) = self.column {
write!(f, ":{}", column)?;
}
}
}
Ok(())
}
}
impl fmt::Display for DecrustBacktrace {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match &self.inner {
Some(bt) => {
writeln!(f, "Backtrace captured at: {:?}", self.capture_timestamp)?;
if let Some(ref thread_name) = self.thread_name {
writeln!(f, "Thread: {} ({:?})", thread_name, self.thread_id)?;
} else {
writeln!(f, "Thread: {:?}", self.thread_id)?;
}
write!(f, "{}", bt)
}
None => write!(f, "<backtrace disabled>"),
}
}
}
impl Clone for DecrustBacktrace {
fn clone(&self) -> Self {
if self.capture_enabled {
Self::force_capture()
} else {
Self::disabled()
}
}
}
pub trait GenerateImplicitData {
fn generate() -> Self;
fn generate_with_source(_source: &dyn std::error::Error) -> Self
where
Self: Sized,
{
Self::generate()
}
fn generate_with_context(context: &HashMap<String, String>) -> Self
where
Self: Sized,
{
let _ = context; Self::generate()
}
}
impl GenerateImplicitData for DecrustBacktrace {
fn generate() -> Self {
Self::capture()
}
fn generate_with_source(source: &dyn std::error::Error) -> Self {
let _ = source; Self::capture()
}
fn generate_with_context(context: &HashMap<String, String>) -> Self {
if context
.get("force_backtrace")
.map(|s| s == "true")
.unwrap_or(false)
{
Self::force_capture()
} else {
Self::capture()
}
}
}
impl DecrustBacktrace {
pub fn generate() -> Self {
Self::capture()
}
}
impl From<std::backtrace::Backtrace> for DecrustBacktrace {
fn from(backtrace: std::backtrace::Backtrace) -> Self {
let current_thread = std::thread::current();
Self {
inner: Some(backtrace),
capture_enabled: true,
capture_timestamp: std::time::SystemTime::now(),
thread_id: current_thread.id(),
thread_name: current_thread.name().map(|s| s.to_string()),
}
}
}
pub trait BacktraceCompat: std::error::Error {
fn backtrace(&self) -> Option<&DecrustBacktrace>;
}
pub trait BacktraceProvider {
fn get_deepest_backtrace(&self) -> Option<&DecrustBacktrace>;
}
impl<E: std::error::Error + BacktraceCompat> BacktraceProvider for E {
fn get_deepest_backtrace(&self) -> Option<&DecrustBacktrace> {
if let Some(bt) = self.backtrace() {
return Some(bt);
}
None
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Timestamp {
instant: std::time::SystemTime,
formatted: String,
}
impl Timestamp {
pub fn now() -> Self {
let instant = std::time::SystemTime::now();
let formatted = Self::format_timestamp(&instant);
Self { instant, formatted }
}
pub fn from_system_time(time: std::time::SystemTime) -> Self {
let formatted = Self::format_timestamp(&time);
Self {
instant: time,
formatted,
}
}
pub fn as_system_time(&self) -> std::time::SystemTime {
self.instant
}
pub fn formatted(&self) -> &str {
&self.formatted
}
fn format_timestamp(time: &std::time::SystemTime) -> String {
match time.duration_since(std::time::UNIX_EPOCH) {
Ok(duration) => {
let secs = duration.as_secs();
let millis = duration.subsec_millis();
let datetime = std::time::UNIX_EPOCH + std::time::Duration::from_secs(secs);
format!(
"{}.{:03} (epoch: {})",
secs,
millis,
datetime
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0)
)
}
Err(_) => "<invalid timestamp>".to_string(),
}
}
}
impl GenerateImplicitData for Timestamp {
fn generate() -> Self {
Self::now()
}
fn generate_with_context(context: &HashMap<String, String>) -> Self {
if let Some(timestamp_str) = context.get("timestamp") {
if let Ok(secs) = timestamp_str.parse::<u64>() {
let time = std::time::UNIX_EPOCH + std::time::Duration::from_secs(secs);
return Self::from_system_time(time);
}
}
Self::now()
}
}
impl fmt::Display for Timestamp {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.formatted)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ThreadId {
id: std::thread::ThreadId,
name: Option<String>,
formatted: String,
}
impl ThreadId {
pub fn current() -> Self {
let thread = std::thread::current();
let id = thread.id();
let name = thread.name().map(|s| s.to_string());
let formatted = Self::format_thread_info(id, name.as_deref());
Self {
id,
name,
formatted,
}
}
pub fn from_components(id: std::thread::ThreadId, name: Option<String>) -> Self {
let formatted = Self::format_thread_info(id, name.as_deref());
Self {
id,
name,
formatted,
}
}
pub fn id(&self) -> std::thread::ThreadId {
self.id
}
pub fn name(&self) -> Option<&str> {
self.name.as_deref()
}
pub fn formatted(&self) -> &str {
&self.formatted
}
fn format_thread_info(id: std::thread::ThreadId, name: Option<&str>) -> String {
match name {
Some(thread_name) => format!("{}({:?})", thread_name, id),
None => format!("{:?}", id),
}
}
}
impl GenerateImplicitData for ThreadId {
fn generate() -> Self {
Self::current()
}
fn generate_with_context(context: &HashMap<String, String>) -> Self {
let _ = context;
Self::current()
}
}
impl fmt::Display for ThreadId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.formatted)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Location {
file: &'static str,
line: u32,
column: u32,
formatted: String,
}
impl Location {
pub const fn new(file: &'static str, line: u32, column: u32) -> Self {
Self {
file,
line,
column,
formatted: String::new(), }
}
pub fn new_formatted(file: &'static str, line: u32, column: u32) -> Self {
let formatted = format!("{}:{}:{}", file, line, column);
Self {
file,
line,
column,
formatted,
}
}
pub fn with_context(file: &'static str, line: u32, column: u32, context: &str) -> Self {
let formatted = format!("{}:{}:{} ({})", file, line, column, context);
Self {
file,
line,
column,
formatted,
}
}
pub fn with_function(file: &'static str, line: u32, column: u32, function: &str) -> Self {
let formatted = format!("{}:{}:{} in {}", file, line, column, function);
Self {
file,
line,
column,
formatted,
}
}
pub fn with_context_and_function(
file: &'static str,
line: u32,
column: u32,
context: &str,
function: &str,
) -> Self {
let formatted = format!("{}:{}:{} in {} ({})", file, line, column, function, context);
Self {
file,
line,
column,
formatted,
}
}
pub fn file(&self) -> &'static str {
self.file
}
pub fn line(&self) -> u32 {
self.line
}
pub fn column(&self) -> u32 {
self.column
}
pub fn formatted(&self) -> String {
if self.formatted.is_empty() {
format!("{}:{}:{}", self.file, self.line, self.column)
} else {
self.formatted.clone()
}
}
}
impl fmt::Display for Location {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}:{}:{}", self.file, self.line, self.column)
}
}
#[macro_export]
macro_rules! location {
() => {
$crate::backtrace::Location::new_formatted(file!(), line!(), column!())
};
(context: $context:expr) => {
$crate::backtrace::Location::with_context(file!(), line!(), column!(), $context)
};
(function: $function:expr) => {
$crate::backtrace::Location::with_function(file!(), line!(), column!(), $function)
};
(context: $context:expr, function: $function:expr) => {
$crate::backtrace::Location::with_context_and_function(
file!(),
line!(),
column!(),
$context,
$function,
)
};
(function: $function:expr, context: $context:expr) => {
$crate::backtrace::Location::with_context_and_function(
file!(),
line!(),
column!(),
$context,
$function,
)
};
}
#[macro_export]
macro_rules! implicit_data {
($type:ty) => {
<$type as $crate::backtrace::GenerateImplicitData>::generate()
};
($type:ty, context: $context:expr) => {
<$type as $crate::backtrace::GenerateImplicitData>::generate_with_context($context)
};
($type:ty, $context:expr) => {
<$type as $crate::backtrace::GenerateImplicitData>::generate_with_context($context)
};
($type:ty, source: $source:expr) => {
<$type as $crate::backtrace::GenerateImplicitData>::generate_with_source($source)
};
($type:ty, force: true) => {{
let mut context = std::collections::HashMap::new();
context.insert("force_backtrace".to_string(), "true".to_string());
<$type as $crate::backtrace::GenerateImplicitData>::generate_with_context(&context)
}};
($type:ty, timestamp: $secs:expr) => {{
let mut context = std::collections::HashMap::new();
context.insert("timestamp".to_string(), $secs.to_string());
<$type as $crate::backtrace::GenerateImplicitData>::generate_with_context(&context)
}};
($type:ty, location: true) => {{
let mut context = std::collections::HashMap::new();
context.insert("file".to_string(), file!().to_string());
context.insert("line".to_string(), line!().to_string());
context.insert("column".to_string(), column!().to_string());
<$type as $crate::backtrace::GenerateImplicitData>::generate_with_context(&context)
}};
($type:ty, force: true, location: true) => {{
let mut context = std::collections::HashMap::new();
context.insert("force_backtrace".to_string(), "true".to_string());
context.insert("file".to_string(), file!().to_string());
context.insert("line".to_string(), line!().to_string());
context.insert("column".to_string(), column!().to_string());
<$type as $crate::backtrace::GenerateImplicitData>::generate_with_context(&context)
}};
($type:ty, $($key:ident: $value:expr),+ $(,)?) => {{
let mut context = std::collections::HashMap::new();
$(
context.insert(stringify!($key).to_string(), $value.to_string());
)+
<$type as $crate::backtrace::GenerateImplicitData>::generate_with_context(&context)
}};
}
#[macro_export]
macro_rules! error_context {
($message:expr) => {
$crate::types::ErrorContext::new($message)
.with_location($crate::location!())
};
($message:expr, severity: $severity:expr) => {
$crate::types::ErrorContext::new($message)
.with_location($crate::location!())
.with_severity($severity)
};
($message:expr, $($key:ident: $value:expr),+ $(,)?) => {{
let mut context = $crate::types::ErrorContext::new($message)
.with_location($crate::location!());
$(
context = match stringify!($key) {
"severity" => context.with_severity($value),
"component" => context.with_component(format!("{}", $value)),
"correlation_id" => context.with_correlation_id(format!("{}", $value)),
"recovery_suggestion" => context.with_recovery_suggestion(format!("{}", $value)),
_ => {
context.add_metadata(stringify!($key), format!("{:?}", $value));
context
}
};
)+
context
}};
}
#[macro_export]
macro_rules! oops {
($message:expr, $source:expr) => {
$crate::DecrustError::Oops {
message: $message.to_string(),
source: Box::new($source),
backtrace: $crate::implicit_data!($crate::backtrace::DecrustBacktrace, location: true),
}
};
($message:expr, $source:expr, $($key:ident: $value:expr),+ $(,)?) => {{
let error = $crate::DecrustError::Oops {
message: $message.to_string(),
source: Box::new($source),
backtrace: $crate::implicit_data!($crate::backtrace::DecrustBacktrace, location: true),
};
let context = $crate::error_context!($message, $($key: $value),+);
$crate::DecrustError::WithRichContext {
context,
source: Box::new(error),
}
}};
}
#[macro_export]
macro_rules! validation_error {
($field:expr, $message:expr) => {
$crate::DecrustError::Validation {
field: $field.to_string(),
message: $message.to_string(),
expected: None,
actual: None,
rule: None,
backtrace: $crate::implicit_data!($crate::backtrace::DecrustBacktrace, location: true),
}
};
($field:expr, $message:expr, suggestion: $suggestion:expr) => {{
let error = $crate::DecrustError::Validation {
field: $field.to_string(),
message: $message.to_string(),
expected: None,
actual: None,
rule: None,
backtrace: $crate::implicit_data!($crate::backtrace::DecrustBacktrace, location: true),
};
let context = $crate::error_context!($message)
.with_recovery_suggestion($suggestion.to_string());
$crate::DecrustError::WithRichContext {
context,
source: Box::new(error),
}
}};
}
impl GenerateImplicitData for std::backtrace::Backtrace {
fn generate() -> Self {
std::backtrace::Backtrace::force_capture()
}
fn generate_with_context(context: &HashMap<String, String>) -> Self {
if context
.get("force_backtrace")
.map(|s| s == "true")
.unwrap_or(false)
{
std::backtrace::Backtrace::force_capture()
} else {
std::backtrace::Backtrace::capture()
}
}
}
pub trait AsBacktrace {
fn as_backtrace(&self) -> Option<&std::backtrace::Backtrace>;
}
impl AsBacktrace for std::backtrace::Backtrace {
fn as_backtrace(&self) -> Option<&std::backtrace::Backtrace> {
Some(self)
}
}
impl AsBacktrace for DecrustBacktrace {
fn as_backtrace(&self) -> Option<&std::backtrace::Backtrace> {
self.as_std_backtrace()
}
}