init
This commit is contained in:
11
.gitignore
vendored
Normal file
11
.gitignore
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
*
|
||||
!*/
|
||||
|
||||
!sql/migrations/*.sql
|
||||
|
||||
!src/**/*.rs
|
||||
|
||||
!.gitignore
|
||||
!Cargo.lock
|
||||
!Cargo.toml
|
||||
!README.md
|
||||
2234
Cargo.lock
generated
Normal file
2234
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
17
Cargo.toml
Normal file
17
Cargo.toml
Normal file
@@ -0,0 +1,17 @@
|
||||
[package]
|
||||
name = "playground-exn"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
derive_more = { version = "2.1.1", features = ["display"] }
|
||||
exn = "0.3.0"
|
||||
sqlx = { version = "0.8", features = ["runtime-tokio", "sqlite", "chrono", "uuid"] }
|
||||
tokio = { version = "1.50.0", features = ["full"] }
|
||||
uuid = { version = "1.19.0", features = ["v4", "v7", "serde"] }
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1.0.149"
|
||||
chrono = { version = "0.4.44", features = ["serde"] }
|
||||
17
README.md
Normal file
17
README.md
Normal file
@@ -0,0 +1,17 @@
|
||||
# Playground - Exn
|
||||
|
||||
Context aware errors.
|
||||
|
||||
https://github.com/fast/exn
|
||||
|
||||
https://fast.github.io/blog/stop-forwarding-errors-start-designing-them/
|
||||
|
||||
example output:
|
||||
|
||||
```
|
||||
the program died, at src/main.rs:35:34
|
||||
|
|
||||
|-> failed to create file: res/foo.txt, at src/main.rs:79:43
|
||||
|
|
||||
|-> Permission denied (os error 13), at src/main.rs:79:43
|
||||
```
|
||||
5
sql/migrations/init.sql
Normal file
5
sql/migrations/init.sql
Normal file
@@ -0,0 +1,5 @@
|
||||
CREATE TABLE IF NOT EXISTS foobar (
|
||||
id TEXT PRIMARY KEY,
|
||||
foo TEXT UNIQUE,
|
||||
bar TEXT UNIQUE
|
||||
);
|
||||
60
src/db.rs
Normal file
60
src/db.rs
Normal file
@@ -0,0 +1,60 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use exn::{Result, ResultExt};
|
||||
use sqlx::{
|
||||
Pool, Sqlite,
|
||||
sqlite::{SqliteConnectOptions, SqlitePoolOptions},
|
||||
};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{FooBar, FooBarRequest, error::DatabaseError};
|
||||
|
||||
/// Shared database pool type
|
||||
pub type DbPool = Pool<Sqlite>;
|
||||
|
||||
pub async fn create_pool(path: &str) -> Result<DbPool, DatabaseError> {
|
||||
let err = || DatabaseError::Initialization {
|
||||
path: path.to_string(),
|
||||
};
|
||||
|
||||
let options = SqliteConnectOptions::from_str(path)
|
||||
.or_raise(err)?
|
||||
.create_if_missing(true);
|
||||
|
||||
let pool = SqlitePoolOptions::new()
|
||||
.max_connections(5)
|
||||
.connect_with(options)
|
||||
.await
|
||||
.or_raise(err)?;
|
||||
|
||||
migrate(&pool).await.or_raise(err)?;
|
||||
|
||||
Ok(pool)
|
||||
}
|
||||
|
||||
pub static MIGRATOR: sqlx::migrate::Migrator = sqlx::migrate!("sql/migrations");
|
||||
|
||||
/// Run database migrations
|
||||
pub async fn migrate(pool: &DbPool) -> Result<(), DatabaseError> {
|
||||
MIGRATOR
|
||||
.run(pool)
|
||||
.await
|
||||
.or_raise(|| DatabaseError::Migration)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn insert_foobar(db: &DbPool, foobar: FooBarRequest) -> Result<FooBar, DatabaseError> {
|
||||
Ok(sqlx::query_as::<_, FooBar>(
|
||||
"INSERT INTO foobar (id, foo, bar)
|
||||
VALUES (?, ?, ?, ?)
|
||||
RETURNING *",
|
||||
)
|
||||
.bind(Uuid::now_v7().to_string())
|
||||
.bind(foobar.foo)
|
||||
.bind(foobar.bar)
|
||||
.fetch_one(db)
|
||||
.await
|
||||
.map_err(|e| DatabaseError::SqlxError(e)) // We are wraping the original error into Exn() as SQLX error does not implement std::error::Error.
|
||||
.or_raise(|| DatabaseError::Query)?)
|
||||
}
|
||||
185
src/error.rs
Normal file
185
src/error.rs
Normal file
@@ -0,0 +1,185 @@
|
||||
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 {
|
||||
#[display("failed to initialize database connection to: {path}")]
|
||||
Initialization { path: String },
|
||||
#[display("failed to run db migration")]
|
||||
Migration,
|
||||
#[display("{_0}")]
|
||||
SqlxError(sqlx::Error),
|
||||
#[display("failed to execute query")]
|
||||
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)),
|
||||
);
|
||||
}
|
||||
}
|
||||
99
src/main.rs
Normal file
99
src/main.rs
Normal file
@@ -0,0 +1,99 @@
|
||||
use derive_more::Display;
|
||||
use exn::{Result, ResultExt, bail};
|
||||
use serde::{Deserialize, Serialize};
|
||||
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;
|
||||
|
||||
#[derive(Debug, Clone, FromRow)]
|
||||
pub struct FooBar {
|
||||
id: Uuid,
|
||||
foo: String,
|
||||
bar: String,
|
||||
}
|
||||
|
||||
pub struct FooBarRequest {
|
||||
foo: String,
|
||||
bar: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Display, Serialize, Deserialize)]
|
||||
#[display("the program died")]
|
||||
pub struct MainError;
|
||||
impl std::error::Error for MainError {}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), MainError> {
|
||||
let err = || MainError;
|
||||
|
||||
init_tracing();
|
||||
//files::write().or_raise(err)?;
|
||||
|
||||
let _ = match files::write().or_raise(err) {
|
||||
Ok(_) => (),
|
||||
Err(e) => {
|
||||
tracing::error!(exn = %exn_to_json_value(&e));
|
||||
bail!(MainError)
|
||||
}
|
||||
};
|
||||
|
||||
let pool = db::create_pool("foobar.db").await.or_raise(err)?;
|
||||
|
||||
let foobar = db::insert_foobar(
|
||||
&pool,
|
||||
FooBarRequest {
|
||||
foo: "foo".to_string(),
|
||||
bar: "bar".to_string(),
|
||||
},
|
||||
)
|
||||
.await
|
||||
.or_raise(err)?;
|
||||
|
||||
println!("FOOBAR! {:?}", foobar);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
mod files {
|
||||
use std::{fs::File, io::Write};
|
||||
|
||||
use derive_more::Display;
|
||||
use exn::{Result, ResultExt};
|
||||
|
||||
#[derive(Debug, Display)]
|
||||
pub enum FileError {
|
||||
#[display("failed to create file: {file}")]
|
||||
Create { file: String },
|
||||
#[display("failed to write to file: {text}")]
|
||||
Write { text: String },
|
||||
}
|
||||
impl std::error::Error for FileError {}
|
||||
|
||||
pub fn write() -> Result<(), FileError> {
|
||||
// mkdir res; chown root:root
|
||||
// will get the error to test output
|
||||
let path = "res/foo.txt";
|
||||
let mut file = File::create(path).or_raise(|| FileError::Create {
|
||||
file: path.to_string(),
|
||||
})?;
|
||||
|
||||
let text = "Hello, world!";
|
||||
file.write_all(text.as_bytes())
|
||||
.or_raise(|| FileError::Write {
|
||||
text: text.to_string(),
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn init_tracing() {
|
||||
tracing_subscriber::registry().with(ExnLayer).init();
|
||||
|
||||
//tracing_subscriber::fmt().pretty().json().init();
|
||||
}
|
||||
Reference in New Issue
Block a user