1#![warn(missing_docs)]
3use std::collections::HashMap;
21use std::env;
22use std::fmt;
23use std::sync::OnceLock;
24
25#[derive(Debug)]
28pub struct DecrustBacktrace {
29 inner: Option<std::backtrace::Backtrace>,
30 capture_enabled: bool,
31 capture_timestamp: std::time::SystemTime,
32 thread_id: std::thread::ThreadId,
33 thread_name: Option<String>,
34}
35
36#[derive(Debug, Clone, Copy, PartialEq, Eq)]
38pub enum BacktraceStatus {
39 Captured,
41 Disabled,
43 Unsupported,
45}
46
47impl DecrustBacktrace {
48 pub fn capture() -> Self {
53 let should_capture = Self::should_capture_from_env();
54 let current_thread = std::thread::current();
55
56 if should_capture {
57 Self {
58 inner: Some(std::backtrace::Backtrace::capture()),
59 capture_enabled: true,
60 capture_timestamp: std::time::SystemTime::now(),
61 thread_id: current_thread.id(),
62 thread_name: current_thread.name().map(|s| s.to_string()),
63 }
64 } else {
65 Self {
66 inner: None,
67 capture_enabled: false,
68 capture_timestamp: std::time::SystemTime::now(),
69 thread_id: current_thread.id(),
70 thread_name: current_thread.name().map(|s| s.to_string()),
71 }
72 }
73 }
74
75 pub fn force_capture() -> Self {
79 let current_thread = std::thread::current();
80 Self {
81 inner: Some(std::backtrace::Backtrace::force_capture()),
82 capture_enabled: true,
83 capture_timestamp: std::time::SystemTime::now(),
84 thread_id: current_thread.id(),
85 thread_name: current_thread.name().map(|s| s.to_string()),
86 }
87 }
88
89 pub fn disabled() -> Self {
91 let current_thread = std::thread::current();
92 Self {
93 inner: None,
94 capture_enabled: false,
95 capture_timestamp: std::time::SystemTime::now(),
96 thread_id: current_thread.id(),
97 thread_name: current_thread.name().map(|s| s.to_string()),
98 }
99 }
100
101 pub fn status(&self) -> BacktraceStatus {
103 match &self.inner {
104 Some(bt) => {
105 use std::backtrace::BacktraceStatus as StdStatus;
107 match bt.status() {
108 StdStatus::Captured => BacktraceStatus::Captured,
109 StdStatus::Disabled => BacktraceStatus::Disabled,
110 StdStatus::Unsupported => BacktraceStatus::Unsupported,
111 #[allow(unreachable_patterns)]
113 _ => BacktraceStatus::Unsupported,
114 }
115 }
116 None => BacktraceStatus::Disabled,
117 }
118 }
119
120 pub fn capture_timestamp(&self) -> std::time::SystemTime {
122 self.capture_timestamp
123 }
124
125 pub fn thread_id(&self) -> std::thread::ThreadId {
127 self.thread_id
128 }
129
130 pub fn thread_name(&self) -> Option<&str> {
132 self.thread_name.as_deref()
133 }
134
135 fn should_capture_from_env() -> bool {
137 static SHOULD_CAPTURE: OnceLock<bool> = OnceLock::new();
138
139 *SHOULD_CAPTURE.get_or_init(|| {
140 if let Ok(val) = env::var("RUST_LIB_BACKTRACE") {
142 return val == "1" || val.to_lowercase() == "full";
143 }
144
145 if let Ok(val) = env::var("RUST_BACKTRACE") {
147 return val == "1" || val.to_lowercase() == "full";
148 }
149
150 false
151 })
152 }
153
154 pub fn as_std_backtrace(&self) -> Option<&std::backtrace::Backtrace> {
156 self.inner.as_ref()
157 }
158
159 pub fn extract_frames(&self) -> Vec<BacktraceFrame> {
161 match &self.inner {
162 Some(bt) => {
163 let bt_string = format!("{}", bt);
164 self.parse_backtrace_string(&bt_string)
165 }
166 None => Vec::new(),
167 }
168 }
169
170 fn parse_backtrace_string(&self, bt_str: &str) -> Vec<BacktraceFrame> {
172 let mut frames = Vec::new();
173
174 for line in bt_str.lines() {
175 if let Some(frame) = self.parse_frame_line(line) {
176 frames.push(frame);
177 }
178 }
179
180 frames
181 }
182
183 fn parse_frame_line(&self, line: &str) -> Option<BacktraceFrame> {
185 let trimmed = line.trim();
187
188 if let Some(colon_pos) = trimmed.find(':') {
189 let number_part = &trimmed[..colon_pos].trim();
190 let rest = &trimmed[colon_pos + 1..].trim();
191
192 if number_part.parse::<usize>().is_ok() {
193 if let Some(at_pos) = rest.rfind(" at ") {
195 let symbol = rest[..at_pos].trim().to_string();
196 let location = rest[at_pos + 4..].trim();
197
198 let (file, line, column) = self.parse_location(location);
199
200 return Some(BacktraceFrame {
201 symbol,
202 file,
203 line,
204 column,
205 });
206 } else {
207 return Some(BacktraceFrame {
209 symbol: rest.to_string(),
210 file: None,
211 line: None,
212 column: None,
213 });
214 }
215 }
216 }
217
218 None
219 }
220
221 fn parse_location(&self, location: &str) -> (Option<String>, Option<u32>, Option<u32>) {
223 let parts: Vec<&str> = location.rsplitn(3, ':').collect();
224
225 match parts.len() {
226 3 => {
227 let column = parts[0].parse().ok();
228 let line = parts[1].parse().ok();
229 let file = Some(parts[2].to_string());
230 (file, line, column)
231 }
232 2 => {
233 let line = parts[0].parse().ok();
234 let file = Some(parts[1].to_string());
235 (file, line, None)
236 }
237 1 => (Some(parts[0].to_string()), None, None),
238 _ => (None, None, None),
239 }
240 }
241}
242
243#[derive(Debug, Clone, PartialEq, Eq)]
245pub struct BacktraceFrame {
246 pub symbol: String,
248 pub file: Option<String>,
250 pub line: Option<u32>,
252 pub column: Option<u32>,
254}
255
256impl fmt::Display for BacktraceFrame {
257 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
258 write!(f, "{}", self.symbol)?;
259 if let Some(ref file) = self.file {
260 write!(f, " at {}", file)?;
261 if let Some(line) = self.line {
262 write!(f, ":{}", line)?;
263 if let Some(column) = self.column {
264 write!(f, ":{}", column)?;
265 }
266 }
267 }
268 Ok(())
269 }
270}
271
272impl fmt::Display for DecrustBacktrace {
273 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
274 match &self.inner {
275 Some(bt) => {
276 writeln!(f, "Backtrace captured at: {:?}", self.capture_timestamp)?;
277 if let Some(ref thread_name) = self.thread_name {
278 writeln!(f, "Thread: {} ({:?})", thread_name, self.thread_id)?;
279 } else {
280 writeln!(f, "Thread: {:?}", self.thread_id)?;
281 }
282 write!(f, "{}", bt)
283 }
284 None => write!(f, "<backtrace disabled>"),
285 }
286 }
287}
288
289impl Clone for DecrustBacktrace {
290 fn clone(&self) -> Self {
291 if self.capture_enabled {
294 Self::force_capture()
296 } else {
297 Self::disabled()
298 }
299 }
300}
301
302pub trait GenerateImplicitData {
306 fn generate() -> Self;
308
309 fn generate_with_source(_source: &dyn std::error::Error) -> Self
313 where
314 Self: Sized,
315 {
316 Self::generate()
317 }
318
319 fn generate_with_context(context: &HashMap<String, String>) -> Self
321 where
322 Self: Sized,
323 {
324 let _ = context; Self::generate()
326 }
327}
328
329impl GenerateImplicitData for DecrustBacktrace {
331 fn generate() -> Self {
332 Self::capture()
333 }
334
335 fn generate_with_source(source: &dyn std::error::Error) -> Self {
336 let _ = source; Self::capture()
341 }
342
343 fn generate_with_context(context: &HashMap<String, String>) -> Self {
344 if context
346 .get("force_backtrace")
347 .map(|s| s == "true")
348 .unwrap_or(false)
349 {
350 Self::force_capture()
351 } else {
352 Self::capture()
353 }
354 }
355}
356
357impl DecrustBacktrace {
359 pub fn generate() -> Self {
361 Self::capture()
362 }
363}
364
365impl From<std::backtrace::Backtrace> for DecrustBacktrace {
367 fn from(backtrace: std::backtrace::Backtrace) -> Self {
368 let current_thread = std::thread::current();
369 Self {
370 inner: Some(backtrace),
371 capture_enabled: true,
372 capture_timestamp: std::time::SystemTime::now(),
373 thread_id: current_thread.id(),
374 thread_name: current_thread.name().map(|s| s.to_string()),
375 }
376 }
377}
378
379pub trait BacktraceCompat: std::error::Error {
383 fn backtrace(&self) -> Option<&DecrustBacktrace>;
385}
386
387pub trait BacktraceProvider {
389 fn get_deepest_backtrace(&self) -> Option<&DecrustBacktrace>;
391}
392
393impl<E: std::error::Error + BacktraceCompat> BacktraceProvider for E {
394 fn get_deepest_backtrace(&self) -> Option<&DecrustBacktrace> {
395 if let Some(bt) = self.backtrace() {
397 return Some(bt);
398 }
399 None
400 }
401}
402
403#[derive(Debug, Clone, PartialEq, Eq)]
405pub struct Timestamp {
406 instant: std::time::SystemTime,
407 formatted: String,
408}
409
410impl Timestamp {
411 pub fn now() -> Self {
413 let instant = std::time::SystemTime::now();
414 let formatted = Self::format_timestamp(&instant);
415 Self { instant, formatted }
416 }
417
418 pub fn from_system_time(time: std::time::SystemTime) -> Self {
420 let formatted = Self::format_timestamp(&time);
421 Self {
422 instant: time,
423 formatted,
424 }
425 }
426
427 pub fn as_system_time(&self) -> std::time::SystemTime {
429 self.instant
430 }
431
432 pub fn formatted(&self) -> &str {
434 &self.formatted
435 }
436
437 fn format_timestamp(time: &std::time::SystemTime) -> String {
439 match time.duration_since(std::time::UNIX_EPOCH) {
440 Ok(duration) => {
441 let secs = duration.as_secs();
442 let millis = duration.subsec_millis();
443
444 let datetime = std::time::UNIX_EPOCH + std::time::Duration::from_secs(secs);
446 format!(
447 "{}.{:03} (epoch: {})",
448 secs,
449 millis,
450 datetime
451 .duration_since(std::time::UNIX_EPOCH)
452 .map(|d| d.as_secs())
453 .unwrap_or(0)
454 )
455 }
456 Err(_) => "<invalid timestamp>".to_string(),
457 }
458 }
459}
460
461impl GenerateImplicitData for Timestamp {
462 fn generate() -> Self {
463 Self::now()
464 }
465
466 fn generate_with_context(context: &HashMap<String, String>) -> Self {
467 if let Some(timestamp_str) = context.get("timestamp") {
469 if let Ok(secs) = timestamp_str.parse::<u64>() {
470 let time = std::time::UNIX_EPOCH + std::time::Duration::from_secs(secs);
471 return Self::from_system_time(time);
472 }
473 }
474 Self::now()
475 }
476}
477
478impl fmt::Display for Timestamp {
479 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
480 write!(f, "{}", self.formatted)
481 }
482}
483
484#[derive(Debug, Clone, PartialEq, Eq)]
486pub struct ThreadId {
487 id: std::thread::ThreadId,
488 name: Option<String>,
489 formatted: String,
490}
491
492impl ThreadId {
493 pub fn current() -> Self {
495 let thread = std::thread::current();
496 let id = thread.id();
497 let name = thread.name().map(|s| s.to_string());
498 let formatted = Self::format_thread_info(id, name.as_deref());
499
500 Self {
501 id,
502 name,
503 formatted,
504 }
505 }
506
507 pub fn from_components(id: std::thread::ThreadId, name: Option<String>) -> Self {
509 let formatted = Self::format_thread_info(id, name.as_deref());
510 Self {
511 id,
512 name,
513 formatted,
514 }
515 }
516
517 pub fn id(&self) -> std::thread::ThreadId {
519 self.id
520 }
521
522 pub fn name(&self) -> Option<&str> {
524 self.name.as_deref()
525 }
526
527 pub fn formatted(&self) -> &str {
529 &self.formatted
530 }
531
532 fn format_thread_info(id: std::thread::ThreadId, name: Option<&str>) -> String {
534 match name {
535 Some(thread_name) => format!("{}({:?})", thread_name, id),
536 None => format!("{:?}", id),
537 }
538 }
539}
540
541impl GenerateImplicitData for ThreadId {
542 fn generate() -> Self {
543 Self::current()
544 }
545
546 fn generate_with_context(context: &HashMap<String, String>) -> Self {
547 let _ = context;
550 Self::current()
551 }
552}
553
554impl fmt::Display for ThreadId {
555 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
556 write!(f, "{}", self.formatted)
557 }
558}
559
560#[derive(Debug, Clone, PartialEq, Eq)]
562pub struct Location {
563 file: &'static str,
564 line: u32,
565 column: u32,
566 formatted: String,
567}
568
569impl Location {
570 pub const fn new(file: &'static str, line: u32, column: u32) -> Self {
572 Self {
573 file,
574 line,
575 column,
576 formatted: String::new(), }
578 }
579
580 pub fn new_formatted(file: &'static str, line: u32, column: u32) -> Self {
582 let formatted = format!("{}:{}:{}", file, line, column);
583 Self {
584 file,
585 line,
586 column,
587 formatted,
588 }
589 }
590
591 pub fn with_context(file: &'static str, line: u32, column: u32, context: &str) -> Self {
593 let formatted = format!("{}:{}:{} ({})", file, line, column, context);
594 Self {
595 file,
596 line,
597 column,
598 formatted,
599 }
600 }
601
602 pub fn with_function(file: &'static str, line: u32, column: u32, function: &str) -> Self {
604 let formatted = format!("{}:{}:{} in {}", file, line, column, function);
605 Self {
606 file,
607 line,
608 column,
609 formatted,
610 }
611 }
612
613 pub fn with_context_and_function(
615 file: &'static str,
616 line: u32,
617 column: u32,
618 context: &str,
619 function: &str,
620 ) -> Self {
621 let formatted = format!("{}:{}:{} in {} ({})", file, line, column, function, context);
622 Self {
623 file,
624 line,
625 column,
626 formatted,
627 }
628 }
629
630 pub fn file(&self) -> &'static str {
632 self.file
633 }
634
635 pub fn line(&self) -> u32 {
637 self.line
638 }
639
640 pub fn column(&self) -> u32 {
642 self.column
643 }
644
645 pub fn formatted(&self) -> String {
647 if self.formatted.is_empty() {
648 format!("{}:{}:{}", self.file, self.line, self.column)
649 } else {
650 self.formatted.clone()
651 }
652 }
653}
654
655impl fmt::Display for Location {
656 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
657 write!(f, "{}:{}:{}", self.file, self.line, self.column)
658 }
659}
660
661#[macro_export]
669macro_rules! location {
670 () => {
671 $crate::backtrace::Location::new_formatted(file!(), line!(), column!())
672 };
673
674 (context: $context:expr) => {
675 $crate::backtrace::Location::with_context(file!(), line!(), column!(), $context)
676 };
677
678 (function: $function:expr) => {
679 $crate::backtrace::Location::with_function(file!(), line!(), column!(), $function)
680 };
681
682 (context: $context:expr, function: $function:expr) => {
683 $crate::backtrace::Location::with_context_and_function(
684 file!(),
685 line!(),
686 column!(),
687 $context,
688 $function,
689 )
690 };
691
692 (function: $function:expr, context: $context:expr) => {
693 $crate::backtrace::Location::with_context_and_function(
694 file!(),
695 line!(),
696 column!(),
697 $context,
698 $function,
699 )
700 };
701}
702
703#[macro_export]
713macro_rules! implicit_data {
714 ($type:ty) => {
716 <$type as $crate::backtrace::GenerateImplicitData>::generate()
717 };
718
719 ($type:ty, context: $context:expr) => {
721 <$type as $crate::backtrace::GenerateImplicitData>::generate_with_context($context)
722 };
723
724 ($type:ty, $context:expr) => {
726 <$type as $crate::backtrace::GenerateImplicitData>::generate_with_context($context)
727 };
728
729 ($type:ty, source: $source:expr) => {
731 <$type as $crate::backtrace::GenerateImplicitData>::generate_with_source($source)
732 };
733
734 ($type:ty, force: true) => {{
736 let mut context = std::collections::HashMap::new();
737 context.insert("force_backtrace".to_string(), "true".to_string());
738 <$type as $crate::backtrace::GenerateImplicitData>::generate_with_context(&context)
739 }};
740
741 ($type:ty, timestamp: $secs:expr) => {{
743 let mut context = std::collections::HashMap::new();
744 context.insert("timestamp".to_string(), $secs.to_string());
745 <$type as $crate::backtrace::GenerateImplicitData>::generate_with_context(&context)
746 }};
747
748 ($type:ty, location: true) => {{
750 let mut context = std::collections::HashMap::new();
751 context.insert("file".to_string(), file!().to_string());
752 context.insert("line".to_string(), line!().to_string());
753 context.insert("column".to_string(), column!().to_string());
754 <$type as $crate::backtrace::GenerateImplicitData>::generate_with_context(&context)
755 }};
756
757 ($type:ty, force: true, location: true) => {{
759 let mut context = std::collections::HashMap::new();
760 context.insert("force_backtrace".to_string(), "true".to_string());
761 context.insert("file".to_string(), file!().to_string());
762 context.insert("line".to_string(), line!().to_string());
763 context.insert("column".to_string(), column!().to_string());
764 <$type as $crate::backtrace::GenerateImplicitData>::generate_with_context(&context)
765 }};
766
767 ($type:ty, $($key:ident: $value:expr),+ $(,)?) => {{
769 let mut context = std::collections::HashMap::new();
770 $(
771 context.insert(stringify!($key).to_string(), $value.to_string());
772 )+
773 <$type as $crate::backtrace::GenerateImplicitData>::generate_with_context(&context)
774 }};
775}
776
777#[macro_export]
784macro_rules! error_context {
785 ($message:expr) => {
786 $crate::types::ErrorContext::new($message)
787 .with_location($crate::location!())
788 };
789
790 ($message:expr, severity: $severity:expr) => {
791 $crate::types::ErrorContext::new($message)
792 .with_location($crate::location!())
793 .with_severity($severity)
794 };
795
796 ($message:expr, $($key:ident: $value:expr),+ $(,)?) => {{
797 let mut context = $crate::types::ErrorContext::new($message)
798 .with_location($crate::location!());
799
800 $(
801 context = match stringify!($key) {
802 "severity" => context.with_severity($value),
803 "component" => context.with_component(format!("{}", $value)),
804 "correlation_id" => context.with_correlation_id(format!("{}", $value)),
805 "recovery_suggestion" => context.with_recovery_suggestion(format!("{}", $value)),
806 _ => {
807 context.add_metadata(stringify!($key), format!("{:?}", $value));
808 context
809 }
810 };
811 )+
812
813 context
814 }};
815}
816
817#[macro_export]
824macro_rules! oops {
825 ($message:expr, $source:expr) => {
826 $crate::DecrustError::Oops {
827 message: $message.to_string(),
828 source: Box::new($source),
829 backtrace: $crate::implicit_data!($crate::backtrace::DecrustBacktrace, location: true),
830 }
831 };
832
833 ($message:expr, $source:expr, $($key:ident: $value:expr),+ $(,)?) => {{
834 let error = $crate::DecrustError::Oops {
835 message: $message.to_string(),
836 source: Box::new($source),
837 backtrace: $crate::implicit_data!($crate::backtrace::DecrustBacktrace, location: true),
838 };
839
840 let context = $crate::error_context!($message, $($key: $value),+);
842 $crate::DecrustError::WithRichContext {
843 context,
844 source: Box::new(error),
845 }
846 }};
847}
848
849#[macro_export]
855macro_rules! validation_error {
856 ($field:expr, $message:expr) => {
857 $crate::DecrustError::Validation {
858 field: $field.to_string(),
859 message: $message.to_string(),
860 expected: None,
861 actual: None,
862 rule: None,
863 backtrace: $crate::implicit_data!($crate::backtrace::DecrustBacktrace, location: true),
864 }
865 };
866
867 ($field:expr, $message:expr, suggestion: $suggestion:expr) => {{
868 let error = $crate::DecrustError::Validation {
869 field: $field.to_string(),
870 message: $message.to_string(),
871 expected: None,
872 actual: None,
873 rule: None,
874 backtrace: $crate::implicit_data!($crate::backtrace::DecrustBacktrace, location: true),
875 };
876
877 let context = $crate::error_context!($message)
878 .with_recovery_suggestion($suggestion.to_string());
879 $crate::DecrustError::WithRichContext {
880 context,
881 source: Box::new(error),
882 }
883 }};
884}
885
886impl GenerateImplicitData for std::backtrace::Backtrace {
890 fn generate() -> Self {
891 std::backtrace::Backtrace::force_capture()
892 }
893
894 fn generate_with_context(context: &HashMap<String, String>) -> Self {
895 if context
897 .get("force_backtrace")
898 .map(|s| s == "true")
899 .unwrap_or(false)
900 {
901 std::backtrace::Backtrace::force_capture()
902 } else {
903 std::backtrace::Backtrace::capture()
904 }
905 }
906}
907
908pub trait AsBacktrace {
912 fn as_backtrace(&self) -> Option<&std::backtrace::Backtrace>;
914}
915
916impl AsBacktrace for std::backtrace::Backtrace {
918 fn as_backtrace(&self) -> Option<&std::backtrace::Backtrace> {
919 Some(self)
920 }
921}
922
923impl AsBacktrace for DecrustBacktrace {
925 fn as_backtrace(&self) -> Option<&std::backtrace::Backtrace> {
926 self.as_std_backtrace()
927 }
928}