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