initial working exn tracing

This commit is contained in:
2026-03-21 21:21:45 -04:00
parent ebe1836513
commit ddd6792205
4 changed files with 142 additions and 178 deletions

View File

@@ -2,11 +2,13 @@
Context aware errors. Context aware errors.
Trying to get exn into structured output for tracing_subscriber.
https://github.com/fast/exn https://github.com/fast/exn
https://fast.github.io/blog/stop-forwarding-errors-start-designing-them/ 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 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 |-> 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"
}```

View File

@@ -1,15 +1,4 @@
use crate::MainError;
use chrono::Utc;
use derive_more::Display; 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)] #[derive(Debug, Display)]
pub enum DatabaseError { pub enum DatabaseError {
@@ -23,163 +12,3 @@ pub enum DatabaseError {
Query, Query,
} }
impl std::error::Error for DatabaseError {} 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<E: std::error::Error + Send + Sync>(err: Exn<E>) -> 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::<YourErrorType>()
let loc = frame.location().to_string(); // gives "file.rs:line:column"
let mut obj = json!({
"message": msg.trim(),
"location": loc,
});
let children: Vec<Value> = frame.children().iter().map(frame_to_json).collect();
if !children.is_empty() {
obj["causes"] = Value::Array(children);
}
obj
}
fn error_to_json_value<E: std::error::Error + Send + Sync + 'static>(exn: &Exn<E>) -> 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<E>(exn: &Exn<E>) -> 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<Value> = 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<S> Layer<S> 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<String, serde_json::Value>);
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)),
);
}
}

102
src/exn_tracing.rs Normal file
View File

@@ -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<Value> = frame.children().iter().map(frame_to_value).collect();
if !children.is_empty() {
obj["causes"] = Value::Array(children);
}
obj
}
pub fn exn_to_json_value<E>(exn: &Exn<E>) -> Value
where
E: std::fmt::Display + std::fmt::Debug + std::error::Error + Send + Sync + 'static,
{
frame_to_value(exn.frame())
}
pub struct ExnLayer;
impl<S> Layer<S> 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<String, Value>);
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);
}
}

View File

@@ -1,3 +1,9 @@
mod db;
mod error;
mod exn_tracing;
use std::process::exit;
use derive_more::Display; use derive_more::Display;
use exn::{Result, ResultExt, bail}; use exn::{Result, ResultExt, bail};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@@ -6,10 +12,7 @@ use sqlx::prelude::FromRow;
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
use uuid::Uuid; use uuid::Uuid;
use crate::error::{ExnLayer, exn_to_json_value}; use crate::exn_tracing::{ExnLayer, exn_to_json_value};
mod db;
mod error;
#[derive(Debug, Clone, FromRow)] #[derive(Debug, Clone, FromRow)]
pub struct FooBar { pub struct FooBar {
@@ -38,8 +41,9 @@ async fn main() -> Result<(), MainError> {
let _ = match files::write().or_raise(err) { let _ = match files::write().or_raise(err) {
Ok(_) => (), Ok(_) => (),
Err(e) => { Err(e) => {
tracing::error!(exn = %exn_to_json_value(&e)); error_exn!(e);
bail!(MainError) exit(1);
//bail!(MainError)
} }
}; };