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