Improved error handling (part 1)
This commit is contained in:
16
crates/trc/Cargo.toml
Normal file
16
crates/trc/Cargo.toml
Normal file
@@ -0,0 +1,16 @@
|
||||
[package]
|
||||
name = "trc"
|
||||
version = "0.8.5"
|
||||
edition = "2021"
|
||||
resolver = "2"
|
||||
|
||||
[dependencies]
|
||||
base64 = "0.22.1"
|
||||
serde_json = "1.0.120"
|
||||
reqwest = { version = "0.12", default-features = false, features = ["rustls-tls-webpki-roots", "http2"]}
|
||||
bincode = "1.3.3"
|
||||
|
||||
[features]
|
||||
test_mode = []
|
||||
|
||||
[dev-dependencies]
|
||||
214
crates/trc/src/conv.rs
Normal file
214
crates/trc/src/conv.rs
Normal file
@@ -0,0 +1,214 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <hello@stalw.art>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use std::fmt::Debug;
|
||||
|
||||
use crate::*;
|
||||
|
||||
impl<T, const N: usize> AsRef<T> for Context<T, N> {
|
||||
fn as_ref(&self) -> &T {
|
||||
&self.inner
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&'static str> for Value {
|
||||
fn from(value: &'static str) -> Self {
|
||||
Self::Static(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for Value {
|
||||
fn from(value: String) -> Self {
|
||||
Self::String(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<u64> for Value {
|
||||
fn from(value: u64) -> Self {
|
||||
Self::UInt(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<f64> for Value {
|
||||
fn from(value: f64) -> Self {
|
||||
Self::Float(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<u16> for Value {
|
||||
fn from(value: u16) -> Self {
|
||||
Self::UInt(value.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<i32> for Value {
|
||||
fn from(value: i32) -> Self {
|
||||
Self::Int(value.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<u32> for Value {
|
||||
fn from(value: u32) -> Self {
|
||||
Self::UInt(value.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<usize> for Value {
|
||||
fn from(value: usize) -> Self {
|
||||
Self::UInt(value as u64)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<bool> for Value {
|
||||
fn from(value: bool) -> Self {
|
||||
Self::Bool(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<IpAddr> for Value {
|
||||
fn from(value: IpAddr) -> Self {
|
||||
match value {
|
||||
IpAddr::V4(ip) => Value::Ipv4(ip),
|
||||
IpAddr::V6(ip) => Value::Ipv6(Box::new(ip)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Error> for Value {
|
||||
fn from(value: Error) -> Self {
|
||||
Self::Error(Box::new(value))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ErrorKind> for Value {
|
||||
fn from(value: ErrorKind) -> Self {
|
||||
Self::ErrorKind(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Cause> for Error {
|
||||
fn from(value: Cause) -> Self {
|
||||
Error::new(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Protocol> for Value {
|
||||
fn from(value: Protocol) -> Self {
|
||||
Self::Protocol(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Vec<u8>> for Value {
|
||||
fn from(value: Vec<u8>) -> Self {
|
||||
Self::Bytes(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&[u8]> for Value {
|
||||
fn from(value: &[u8]) -> Self {
|
||||
Self::Bytes(value.to_vec())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<&crate::Result<T>> for Value
|
||||
where
|
||||
T: Debug,
|
||||
{
|
||||
fn from(value: &crate::Result<T>) -> Self {
|
||||
match value {
|
||||
Ok(value) => format!("{:?}", value).into(),
|
||||
Err(err) => err.clone().into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<Vec<T>> for Value
|
||||
where
|
||||
T: Into<Value>,
|
||||
{
|
||||
fn from(value: Vec<T>) -> Self {
|
||||
Self::Array(value.into_iter().map(Into::into).collect())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<&[T]> for Value
|
||||
where
|
||||
T: Into<Value> + Clone,
|
||||
{
|
||||
fn from(value: &[T]) -> Self {
|
||||
Self::Array(value.iter().map(|v| v.clone().into()).collect())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<std::io::Error> for Error {
|
||||
fn from(err: std::io::Error) -> Self {
|
||||
Cause::Io
|
||||
.ctx(Key::Reason, err.kind())
|
||||
.ctx(Key::Details, err.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<serde_json::Error> for Error {
|
||||
fn from(err: serde_json::Error) -> Self {
|
||||
Cause::Deserialize
|
||||
.reason(err)
|
||||
.details("JSON deserialization failed")
|
||||
}
|
||||
}
|
||||
|
||||
impl From<base64::DecodeError> for Error {
|
||||
fn from(err: base64::DecodeError) -> Self {
|
||||
Cause::DataCorruption
|
||||
.reason(err)
|
||||
.details("Base64 decoding failed")
|
||||
}
|
||||
}
|
||||
|
||||
impl From<reqwest::Error> for Error {
|
||||
fn from(err: reqwest::Error) -> Self {
|
||||
Cause::Http
|
||||
.into_err()
|
||||
.ctx_opt(Key::Url, err.url().map(|url| url.as_ref().to_string()))
|
||||
.ctx_opt(Key::Code, err.status().map(|status| status.as_u16()))
|
||||
.reason(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<bincode::Error> for Error {
|
||||
fn from(value: bincode::Error) -> Self {
|
||||
Cause::Deserialize
|
||||
.reason(value)
|
||||
.details("Bincode deserialization failed")
|
||||
}
|
||||
}
|
||||
|
||||
impl From<reqwest::header::ToStrError> for Error {
|
||||
fn from(value: reqwest::header::ToStrError) -> Self {
|
||||
Cause::Http
|
||||
.reason(value)
|
||||
.details("Failed to convert header to string")
|
||||
}
|
||||
}
|
||||
|
||||
pub trait AssertSuccess
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
fn assert_success(self) -> impl std::future::Future<Output = crate::Result<Self>> + Send;
|
||||
}
|
||||
|
||||
impl AssertSuccess for reqwest::Response {
|
||||
async fn assert_success(self) -> crate::Result<Self> {
|
||||
let status = self.status();
|
||||
if status.is_success() {
|
||||
Ok(self)
|
||||
} else {
|
||||
Err(Cause::Http
|
||||
.ctx(Key::Code, status.as_u16())
|
||||
.ctx_opt(Key::Reason, self.text().await.ok()))
|
||||
}
|
||||
}
|
||||
}
|
||||
211
crates/trc/src/imple.rs
Normal file
211
crates/trc/src/imple.rs
Normal file
@@ -0,0 +1,211 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <hello@stalw.art>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use std::{borrow::Cow, fmt::Display};
|
||||
|
||||
use crate::*;
|
||||
|
||||
impl<T, const N: usize> Context<T, N>
|
||||
where
|
||||
[(Key, Value); N]: Default,
|
||||
T: Eq,
|
||||
{
|
||||
pub fn new(inner: T) -> Self {
|
||||
Self {
|
||||
inner,
|
||||
keys: Default::default(),
|
||||
keys_size: 0,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn ctx(mut self, key: Key, value: impl Into<Value>) -> Self {
|
||||
if self.keys_size < N {
|
||||
self.keys[self.keys_size] = (key, value.into());
|
||||
self.keys_size += 1;
|
||||
} else {
|
||||
#[cfg(debug_assertions)]
|
||||
panic!(
|
||||
"Context is full while inserting {:?}: {:?}",
|
||||
key,
|
||||
value.into()
|
||||
);
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
pub fn ctx_opt(self, key: Key, value: Option<impl Into<Value>>) -> Self {
|
||||
match value {
|
||||
Some(value) => self.ctx(key, value),
|
||||
None => self,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn matches(&self, inner: T) -> bool {
|
||||
self.inner == inner
|
||||
}
|
||||
|
||||
pub fn value(&self, key: Key) -> Option<&Value> {
|
||||
self.keys.iter().take(self.keys_size).find_map(
|
||||
|(k, v)| {
|
||||
if *k == key {
|
||||
Some(v)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
pub fn take_value(&mut self, key: Key) -> Option<Value> {
|
||||
self.keys
|
||||
.iter_mut()
|
||||
.take(self.keys_size)
|
||||
.find_map(|(k, v)| {
|
||||
if *k == key {
|
||||
Some(std::mem::take(v))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn caused_by(self, error: impl Into<Value>) -> Self {
|
||||
self.ctx(Key::CausedBy, error)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn details(self, error: impl Into<Value>) -> Self {
|
||||
self.ctx(Key::Details, error)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn reason(self, error: impl Display) -> Self {
|
||||
self.ctx(Key::Reason, error.to_string())
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn protocol(self, protocol: Protocol) -> Self {
|
||||
self.ctx(Key::Protocol, protocol)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn document_id(self, id: u32) -> Self {
|
||||
self.ctx(Key::DocumentId, id)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn account_id(self, id: u32) -> Self {
|
||||
self.ctx(Key::AccountId, id)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn collection(self, id: impl Into<u8>) -> Self {
|
||||
self.ctx(Key::Code, id.into() as u64)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn property(self, id: impl Into<u8>) -> Self {
|
||||
self.ctx(Key::Property, id.into() as u64)
|
||||
}
|
||||
|
||||
pub fn corrupted_key(key: &[u8], value: Option<&[u8]>, caused_by: &'static str) -> Error {
|
||||
Cause::DataCorruption
|
||||
.ctx(Key::Key, key)
|
||||
.ctx_opt(Key::Value, value)
|
||||
.ctx(Key::CausedBy, caused_by)
|
||||
}
|
||||
}
|
||||
|
||||
impl Cause {
|
||||
#[inline(always)]
|
||||
pub fn ctx(self, key: Key, value: impl Into<Value>) -> Error {
|
||||
Error::new(self).ctx(key, value)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn caused_by(self, error: impl Into<Value>) -> Error {
|
||||
Error::new(self).caused_by(error)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn reason(self, error: impl Display) -> Error {
|
||||
Error::new(self).reason(error)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn into_err(self) -> Error {
|
||||
Error::new(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl Error {
|
||||
#[inline(always)]
|
||||
pub fn wrap(self, cause: Cause) -> Self {
|
||||
Error::new(cause).caused_by(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl Value {
|
||||
pub fn to_uint(&self) -> Option<u64> {
|
||||
match self {
|
||||
Self::UInt(value) => Some(*value),
|
||||
Self::Int(value) => Some(*value as u64),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_str(&self) -> Option<&str> {
|
||||
match self {
|
||||
Self::String(value) => Some(value.as_str()),
|
||||
Self::Static(value) => Some(value),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn into_string(self) -> Option<Cow<'static, str>> {
|
||||
match self {
|
||||
Self::String(value) => Some(Cow::Owned(value)),
|
||||
Self::Static(value) => Some(Cow::Borrowed(value)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> AddContext<T> for Result<T> {
|
||||
#[inline(always)]
|
||||
fn caused_by(self, location: &'static str) -> Result<T> {
|
||||
match self {
|
||||
Ok(value) => Ok(value),
|
||||
Err(err) => Err(err.ctx(Key::CausedBy, location)),
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn add_context<F>(self, f: F) -> Result<T>
|
||||
where
|
||||
F: FnOnce(Error) -> Error,
|
||||
{
|
||||
match self {
|
||||
Ok(value) => Ok(value),
|
||||
Err(err) => Err(f(err)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: std::fmt::Debug, const N: usize> Display for Context<T, N> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{:?}", self.inner)?;
|
||||
for (key, value) in self.keys.iter().take(self.keys_size) {
|
||||
write!(f, "\n {:?} = {:?}", key, value)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for Error {}
|
||||
144
crates/trc/src/lib.rs
Normal file
144
crates/trc/src/lib.rs
Normal file
@@ -0,0 +1,144 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <hello@stalw.art>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
pub mod conv;
|
||||
pub mod imple;
|
||||
pub mod macros;
|
||||
|
||||
use std::{
|
||||
io::ErrorKind,
|
||||
net::{IpAddr, Ipv4Addr, Ipv6Addr},
|
||||
};
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
pub type Error = Context<Cause, ERROR_CONTEXT_SIZE>;
|
||||
pub type Trace = Context<Event, TRACE_CONTEXT_SIZE>;
|
||||
|
||||
const ERROR_CONTEXT_SIZE: usize = 5;
|
||||
const TRACE_CONTEXT_SIZE: usize = 10;
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub enum Value {
|
||||
Static(&'static str),
|
||||
String(String),
|
||||
UInt(u64),
|
||||
Int(i64),
|
||||
Float(f64),
|
||||
Bytes(Vec<u8>),
|
||||
Bool(bool),
|
||||
Ipv4(Ipv4Addr),
|
||||
Ipv6(Box<Ipv6Addr>),
|
||||
Protocol(Protocol),
|
||||
Error(Box<Error>),
|
||||
ErrorKind(ErrorKind),
|
||||
Array(Vec<Value>),
|
||||
#[default]
|
||||
None,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Key {
|
||||
RemoteIp,
|
||||
#[default]
|
||||
CausedBy,
|
||||
Reason,
|
||||
Details,
|
||||
Query,
|
||||
Result,
|
||||
Parameters,
|
||||
Type,
|
||||
Id,
|
||||
Code,
|
||||
Key,
|
||||
Value,
|
||||
Size,
|
||||
Status,
|
||||
Protocol,
|
||||
Property,
|
||||
Path,
|
||||
Url,
|
||||
DocumentId,
|
||||
Collection,
|
||||
AccountId,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Event {
|
||||
NewConnection,
|
||||
Error(Cause),
|
||||
SqlQuery,
|
||||
LdapQuery,
|
||||
PurgeTaskStarted,
|
||||
PurgeTaskRunning,
|
||||
PurgeTaskFinished,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Cause {
|
||||
FoundationDB,
|
||||
MySQL,
|
||||
PostgreSQL,
|
||||
RocksDB,
|
||||
SQLite,
|
||||
ElasticSearch,
|
||||
Redis,
|
||||
S3,
|
||||
Io,
|
||||
Imap,
|
||||
Smtp,
|
||||
Ldap,
|
||||
BlobMissingMarker,
|
||||
Unknown,
|
||||
Purge,
|
||||
AssertValue,
|
||||
Timeout,
|
||||
Thread,
|
||||
Pool,
|
||||
DataCorruption,
|
||||
Decompress,
|
||||
Deserialize,
|
||||
NotConfigured,
|
||||
Unsupported,
|
||||
Unexpected,
|
||||
MissingParameter,
|
||||
Invalid,
|
||||
AlreadyExists,
|
||||
NotFound,
|
||||
Configuration,
|
||||
Fetch,
|
||||
Acme,
|
||||
Http,
|
||||
Crypto,
|
||||
Dns,
|
||||
Authentication,
|
||||
Jmap,
|
||||
OverQuota,
|
||||
Ingest,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Protocol {
|
||||
Jmap,
|
||||
Imap,
|
||||
Smtp,
|
||||
ManageSieve,
|
||||
Ldap,
|
||||
Sql,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Context<T, const N: usize> {
|
||||
inner: T,
|
||||
keys: [(Key, Value); N],
|
||||
keys_size: usize,
|
||||
}
|
||||
|
||||
pub trait AddContext<T> {
|
||||
fn caused_by(self, location: &'static str) -> Result<T>;
|
||||
fn add_context<F>(self, f: F) -> Result<T>
|
||||
where
|
||||
F: FnOnce(Error) -> Error;
|
||||
}
|
||||
46
crates/trc/src/macros.rs
Normal file
46
crates/trc/src/macros.rs
Normal file
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <hello@stalw.art>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! trace {
|
||||
($event:ident $(, $key:ident = $value:expr)* $(,)?) => {
|
||||
{
|
||||
let event = $crate::Trace::new($crate::Event::$event)
|
||||
$(
|
||||
.ctx($crate::Key::$key, $crate::Value::from($value))
|
||||
)* ;
|
||||
|
||||
eprintln!("{}", event);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! error {
|
||||
($cause:ident $(, $key:ident = $value:expr)* $(,)?) => {{
|
||||
let event = $crate::Trace::new($crate::Event::Error($crate::Cause::$cause))
|
||||
.ctx($crate::Key::CausedBy, $crate::location!())
|
||||
$(
|
||||
.ctx($crate::Key::$key, $crate::Value::from($value))
|
||||
)* ;
|
||||
|
||||
eprintln!("{}", event);
|
||||
}};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! location {
|
||||
() => {{
|
||||
concat!(file!(), ":", line!(), " (", module_path!(), ")")
|
||||
}};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! bail {
|
||||
($err:expr $(,)?) => {
|
||||
return Err($err);
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user