From ddd6792205515e32792049b8b9c9a636588e6904 Mon Sep 17 00:00:00 2001 From: Caleb Vorderbruggen Date: Sat, 21 Mar 2026 21:21:45 -0400 Subject: [PATCH] initial working exn tracing --- README.md | 31 +++++++- src/error.rs | 171 --------------------------------------------- src/exn_tracing.rs | 102 +++++++++++++++++++++++++++ src/main.rs | 16 +++-- 4 files changed, 142 insertions(+), 178 deletions(-) create mode 100644 src/exn_tracing.rs diff --git a/README.md b/README.md index 0cd4540..2506e78 100644 --- a/README.md +++ b/README.md @@ -2,11 +2,13 @@ Context aware errors. +Trying to get exn into structured output for tracing_subscriber. + https://github.com/fast/exn https://fast.github.io/blog/stop-forwarding-errors-start-designing-them/ -example output: +example default output: ``` the program died, at src/main.rs:35:34 @@ -15,3 +17,30 @@ the program died, at src/main.rs:35:34 | |-> Permission denied (os error 13), at src/main.rs:79:43 ``` + +example structured output + +```json +{ + "fields": { + "error": { + "causes": [ + { + "causes": [ + { + "location": "src/main.rs:86:43", + "message": "Permission denied (os error 13)" + } + ], + "location": "src/main.rs:86:43", + "message": "failed to create file: res/foo.txt" + } + ], + "location": "src/main.rs:41:34", + "message": "the program died" + } + }, + "level": "ERROR", + "target": "playground_exn", + "timestamp": "2026-03-22T01:19:11.256874822+00:00" +}``` diff --git a/src/error.rs b/src/error.rs index 0df36f6..49e6d33 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,15 +1,4 @@ -use crate::MainError; -use chrono::Utc; use derive_more::Display; -use exn::{Exn, Frame}; -use serde_json::{Value, json}; -use std::{collections::BTreeMap, fmt::Write}; -use tracing::{Event, Subscriber}; -use tracing_subscriber::{ - Layer, - fmt::{FmtContext, FormatEvent, FormatFields, format::Writer}, - registry::LookupSpan, -}; #[derive(Debug, Display)] pub enum DatabaseError { @@ -23,163 +12,3 @@ pub enum DatabaseError { Query, } impl std::error::Error for DatabaseError {} - -// Example Output: -// Error: fatal error occurred in application: -// 0: failed to run app, at examples/src/custom-layout.rs:74:30 -// 1: failed to send request to server: https://example.com, at examples/src/custom-layout.rs:94:9 -pub fn err_to_custom_string_example(err: Exn) -> String { - fn collect_frames(report: &mut String, i: usize, frame: &Frame) { - if i > 0 { - report.push('\n'); - } - write!(report, "{}: {}, at {}", i, frame.error(), frame.location()).unwrap(); - for child in frame.children() { - collect_frames(report, i + 1, child); - } - } - - let mut report = String::new(); - collect_frames(&mut report, 0, err.frame()); - - report -} - -fn frame_to_json(frame: &Frame) -> Value { - let msg = frame.error().to_string(); // or .downcast_ref::() - let loc = frame.location().to_string(); // gives "file.rs:line:column" - - let mut obj = json!({ - "message": msg.trim(), - "location": loc, - }); - - let children: Vec = frame.children().iter().map(frame_to_json).collect(); - - if !children.is_empty() { - obj["causes"] = Value::Array(children); - } - - obj -} - -fn error_to_json_value(exn: &Exn) -> Value { - let root_frame = exn.frame(); - - let mut root = frame_to_json(root_frame); - - // Optional: promote root message & location to top-level fields - // (you can keep everything nested if you prefer) - let top_level = json!({ - "message": root.get("message").unwrap_or(&Value::String("unknown error".into())).as_str().unwrap_or("unknown"), - "location": root.get("location").unwrap_or(&Value::String("unknown".into())).as_str().unwrap_or("unknown"), - "causes": root.get("causes").cloned().unwrap_or_else(|| json!([])), - }); - - top_level -} - -pub fn exn_to_json_value(exn: &Exn) -> Value -where - E: std::fmt::Display + std::fmt::Debug + std::error::Error + Send + Sync + 'static, -{ - fn frame_to_value(frame: &Frame) -> Value { - // Use .to_string() → calls Display on the dyn Error - // (most errors implement Display reasonably well) - let msg = frame.error().to_string(); - - // or for Debug output (often more detailed): - // let msg = format!("{:?}", frame.error()); - - let loc = frame.location().to_string(); - - let mut obj = json!({ - "message": msg.trim(), - "location": loc.trim(), - }); - - let children: Vec = frame.children().iter().map(frame_to_value).collect(); - - if !children.is_empty() { - obj["causes"] = Value::Array(children); - } - - obj - } - - frame_to_value(exn.frame()) -} - -pub struct ExnLayer; - -impl Layer for ExnLayer -where - S: tracing::Subscriber, -{ - fn on_event( - &self, - event: &tracing::Event<'_>, - _ctx: tracing_subscriber::layer::Context<'_, S>, - ) { - // Covert the values into a JSON object - let mut fields = BTreeMap::new(); - let mut visitor = JsonVisitor(&mut fields); - event.record(&mut visitor); - - // Output the event in JSON - let output = serde_json::json!({ - "target": event.metadata().target(), - "name": event.metadata().name(), - "level": format!("{:?}", event.metadata().level()), - "fields": fields, - }); - println!("{}", serde_json::to_string_pretty(&output).unwrap()); - } -} - -struct JsonVisitor<'a>(&'a mut BTreeMap); - -impl<'a> tracing::field::Visit for JsonVisitor<'a> { - fn record_f64(&mut self, field: &tracing::field::Field, value: f64) { - self.0 - .insert(field.name().to_string(), serde_json::json!(value)); - } - - fn record_i64(&mut self, field: &tracing::field::Field, value: i64) { - self.0 - .insert(field.name().to_string(), serde_json::json!(value)); - } - - fn record_u64(&mut self, field: &tracing::field::Field, value: u64) { - self.0 - .insert(field.name().to_string(), serde_json::json!(value)); - } - - fn record_bool(&mut self, field: &tracing::field::Field, value: bool) { - self.0 - .insert(field.name().to_string(), serde_json::json!(value)); - } - - fn record_str(&mut self, field: &tracing::field::Field, value: &str) { - self.0 - .insert(field.name().to_string(), serde_json::json!(value)); - } - - fn record_error( - &mut self, - field: &tracing::field::Field, - value: &(dyn std::error::Error + 'static), - ) { - self.0.insert( - field.name().to_string(), - serde_json::json!(value.to_string()), - ); - } - - fn record_debug(&mut self, field: &tracing::field::Field, value: &dyn std::fmt::Debug) { - self.0.insert( - field.name().to_string(), - serde_json::json!(format!("{:?}", value)), - ); - } -} diff --git a/src/exn_tracing.rs b/src/exn_tracing.rs new file mode 100644 index 0000000..6a58833 --- /dev/null +++ b/src/exn_tracing.rs @@ -0,0 +1,102 @@ +use exn::{Exn, Frame}; +use serde_json::{Map, Value, json}; +use tracing::{ + Event, + field::{Field, Visit}, +}; +use tracing_subscriber::{Layer, layer::Context}; + +#[macro_export] +macro_rules! error_exn { + ($exn:expr) => { + tracing::error!(error = %exn_to_json_value(&$exn)) + }; + ($exn:expr, $($arg:tt)*) => { + tracing::error!(error = %exn_to_json_value(&$exn), $($arg)*) + }; +} + +fn frame_to_value(frame: &Frame) -> Value { + let msg = frame.error().to_string(); + + let loc = frame.location().to_string(); + + let mut obj = json!({ + "message": msg.trim(), + "location": loc.trim(), + }); + + let children: Vec = frame.children().iter().map(frame_to_value).collect(); + + if !children.is_empty() { + obj["causes"] = Value::Array(children); + } + + obj +} + +pub fn exn_to_json_value(exn: &Exn) -> Value +where + E: std::fmt::Display + std::fmt::Debug + std::error::Error + Send + Sync + 'static, +{ + frame_to_value(exn.frame()) +} + +pub struct ExnLayer; + +impl Layer for ExnLayer +where + S: tracing::Subscriber, +{ + fn on_event(&self, event: &Event<'_>, _ctx: Context<'_, S>) { + let mut fields = Map::new(); + let mut visitor = ExnJsonVisitor(&mut fields); + event.record(&mut visitor); + + let output = json!({ + "timestamp": chrono::Utc::now().to_rfc3339(), + "level": event.metadata().level().to_string(), + "target": event.metadata().target(), + "fields": fields, + }); + + if let Ok(pretty) = serde_json::to_string_pretty(&output) { + println!("{}", pretty); + } + } +} + +struct ExnJsonVisitor<'a>(&'a mut Map); + +impl<'a> Visit for ExnJsonVisitor<'a> { + fn record_f64(&mut self, field: &Field, value: f64) { + self.0.insert(field.name().to_string(), json!(value)); + } + fn record_i64(&mut self, field: &Field, value: i64) { + self.0.insert(field.name().to_string(), json!(value)); + } + fn record_u64(&mut self, field: &Field, value: u64) { + self.0.insert(field.name().to_string(), json!(value)); + } + fn record_bool(&mut self, field: &Field, value: bool) { + self.0.insert(field.name().to_string(), json!(value)); + } + fn record_str(&mut self, field: &Field, value: &str) { + self.0.insert(field.name().to_string(), json!(value)); + } + fn record_error( + &mut self, + field: &tracing::field::Field, + value: &(dyn std::error::Error + 'static), + ) { + let debug_str = format!("{:?}", value); + let val = serde_json::from_str(&debug_str).unwrap_or_else(|_| Value::String(debug_str)); + self.0.insert(field.name().to_string(), val); + } + + fn record_debug(&mut self, field: &tracing::field::Field, value: &dyn std::fmt::Debug) { + let debug_str = format!("{:?}", value); + let val = serde_json::from_str(&debug_str).unwrap_or_else(|_| Value::String(debug_str)); + self.0.insert(field.name().to_string(), val); + } +} diff --git a/src/main.rs b/src/main.rs index 3c23c51..5fcf36d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,9 @@ +mod db; +mod error; +mod exn_tracing; + +use std::process::exit; + use derive_more::Display; use exn::{Result, ResultExt, bail}; use serde::{Deserialize, Serialize}; @@ -6,10 +12,7 @@ use sqlx::prelude::FromRow; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; use uuid::Uuid; -use crate::error::{ExnLayer, exn_to_json_value}; - -mod db; -mod error; +use crate::exn_tracing::{ExnLayer, exn_to_json_value}; #[derive(Debug, Clone, FromRow)] pub struct FooBar { @@ -38,8 +41,9 @@ async fn main() -> Result<(), MainError> { let _ = match files::write().or_raise(err) { Ok(_) => (), Err(e) => { - tracing::error!(exn = %exn_to_json_value(&e)); - bail!(MainError) + error_exn!(e); + exit(1); + //bail!(MainError) } };