use async_trait::async_trait;
use std::collections::BTreeMap;
use std::sync::Arc;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ErrorSeverity {
Warning,
Error,
Fatal,
}
#[derive(Debug, Clone, Default)]
pub struct ErrorScope {
pub user_id: Option<String>,
pub org_id: Option<String>,
pub session_id: Option<String>,
pub request_id: Option<String>,
pub route: Option<String>,
pub component: Option<String>,
pub task_id: Option<String>,
pub workflow_id: Option<String>,
pub extra: BTreeMap<String, String>,
}
impl ErrorScope {
pub fn new() -> Self {
Self::default()
}
pub fn with_user(mut self, user_id: impl Into<String>) -> Self {
self.user_id = Some(user_id.into());
self
}
pub fn with_org(mut self, org_id: impl Into<String>) -> Self {
self.org_id = Some(org_id.into());
self
}
pub fn with_session(mut self, session_id: impl Into<String>) -> Self {
self.session_id = Some(session_id.into());
self
}
pub fn with_request(mut self, request_id: impl Into<String>) -> Self {
self.request_id = Some(request_id.into());
self
}
pub fn with_route(mut self, route: impl Into<String>) -> Self {
self.route = Some(route.into());
self
}
pub fn with_component(mut self, component: impl Into<String>) -> Self {
self.component = Some(component.into());
self
}
pub fn with_task(mut self, task_id: impl Into<String>) -> Self {
self.task_id = Some(task_id.into());
self
}
pub fn with_workflow(mut self, workflow_id: impl Into<String>) -> Self {
self.workflow_id = Some(workflow_id.into());
self
}
pub fn with_extra(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.extra.insert(key.into(), value.into());
self
}
}
#[derive(Debug, Clone)]
pub struct ErrorReport {
pub severity: ErrorSeverity,
pub kind: String,
pub message: String,
pub scope: ErrorScope,
}
impl ErrorReport {
pub fn new(
severity: ErrorSeverity,
kind: impl Into<String>,
message: impl Into<String>,
) -> Self {
Self {
severity,
kind: kind.into(),
message: message.into(),
scope: ErrorScope::default(),
}
}
pub fn error(kind: impl Into<String>, message: impl Into<String>) -> Self {
Self::new(ErrorSeverity::Error, kind, message)
}
pub fn warning(kind: impl Into<String>, message: impl Into<String>) -> Self {
Self::new(ErrorSeverity::Warning, kind, message)
}
pub fn fatal(kind: impl Into<String>, message: impl Into<String>) -> Self {
Self::new(ErrorSeverity::Fatal, kind, message)
}
pub fn with_scope(mut self, scope: ErrorScope) -> Self {
self.scope = scope;
self
}
}
#[async_trait]
pub trait ErrorReporter: Send + Sync {
async fn report(&self, report: ErrorReport);
fn name(&self) -> &'static str {
"ErrorReporter"
}
}
pub type SharedErrorReporter = Arc<dyn ErrorReporter>;
#[derive(Debug, Clone, Default)]
pub struct NoopErrorReporter;
#[async_trait]
impl ErrorReporter for NoopErrorReporter {
async fn report(&self, _report: ErrorReport) {}
fn name(&self) -> &'static str {
"NoopErrorReporter"
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Mutex;
struct CaptureReporter {
reports: Mutex<Vec<ErrorReport>>,
}
#[async_trait]
impl ErrorReporter for CaptureReporter {
async fn report(&self, report: ErrorReport) {
self.reports.lock().unwrap().push(report);
}
}
#[tokio::test]
async fn captures_report_with_scope() {
let reporter = CaptureReporter {
reports: Mutex::new(Vec::new()),
};
let scope = ErrorScope::new()
.with_user("user_1")
.with_org("org_1")
.with_request("req_1")
.with_extra("provider", "openai");
let report = ErrorReport::error("server.request", "boom").with_scope(scope);
reporter.report(report).await;
let reports = reporter.reports.lock().unwrap();
assert_eq!(reports.len(), 1);
assert_eq!(reports[0].kind, "server.request");
assert_eq!(reports[0].scope.user_id.as_deref(), Some("user_1"));
assert_eq!(
reports[0].scope.extra.get("provider").map(String::as_str),
Some("openai")
);
}
#[tokio::test]
async fn noop_reporter_does_not_panic() {
let reporter = NoopErrorReporter;
reporter
.report(ErrorReport::fatal("panic", "ignored"))
.await;
}
}