initial working exn tracing
This commit is contained in:
31
README.md
31
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"
|
||||
}```
|
||||
|
||||
171
src/error.rs
171
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<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
102
src/exn_tracing.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
16
src/main.rs
16
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)
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user