Improved error handling (part 1)

This commit is contained in:
mdecimus
2024-07-11 18:44:51 +02:00
parent ea77a98260
commit 0c2a3f09fe
179 changed files with 3409 additions and 3048 deletions

16
crates/trc/Cargo.toml Normal file
View 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
View 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
View 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
View 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
View 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);
};
}