1use std::future::Future;
27use std::pin::Pin;
28use std::sync::Arc;
29
30use serde::Serialize;
31
32use crate::AppState;
33
34#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
36#[serde(rename_all = "snake_case")]
37#[non_exhaustive]
38pub enum ReportKind {
39 Request,
41 Job,
43 ScheduledTask,
45}
46
47#[derive(Debug, Clone, Serialize)]
49#[non_exhaustive]
50pub struct RequestReport {
51 pub method: String,
53 pub path: String,
55 pub id: String,
57 pub ip: Option<String>,
59 pub user_id: Option<i64>,
61}
62
63#[derive(Debug, Clone, Serialize)]
65#[non_exhaustive]
66pub struct ErrorReport {
67 pub kind: ReportKind,
69 pub message: String,
71 pub details: String,
73 pub source: Option<String>,
75 pub request: Option<RequestReport>,
77 pub environment: String,
79 pub at: i64,
81}
82
83pub(crate) type ReportFn =
84 Arc<dyn Fn(ErrorReport, AppState) -> Pin<Box<dyn Future<Output = ()> + Send>> + Send + Sync>;
85
86pub(crate) fn report_fn<F, Fut>(reporter: F) -> ReportFn
87where
88 F: Fn(ErrorReport, AppState) -> Fut + Send + Sync + 'static,
89 Fut: Future<Output = ()> + Send + 'static,
90{
91 Arc::new(move |report, state| Box::pin(reporter(report, state)))
92}
93
94impl ErrorReport {
95 pub(crate) fn new(
96 state: &AppState,
97 kind: ReportKind,
98 message: String,
99 details: String,
100 source: Option<String>,
101 ) -> Self {
102 let request =
103 crate::context::get::<crate::context::RequestInfo>().map(|info| RequestReport {
104 method: info.method,
105 path: info.path,
106 id: info.id,
107 ip: info.ip,
108 user_id: crate::auth::current_user_id(),
109 });
110 Self {
111 kind,
112 message,
113 details,
114 source,
115 request,
116 environment: format!("{:?}", state.config.env).to_lowercase(),
117 at: crate::clock::unix_secs(),
118 }
119 }
120}
121
122pub(crate) fn send(state: &AppState, report: ErrorReport) {
124 if state.reporters.is_empty() {
125 return;
126 }
127 for reporter in state.reporters.iter() {
128 let (reporter, report, state) = (reporter.clone(), report.clone(), state.clone());
129 let run = async move {
130 let task = tokio::spawn(crate::context::scope_app(
131 state.clone(),
132 reporter(report, state),
133 ));
134 if task.await.is_err() {
135 tracing::error!("an error reporter panicked");
136 }
137 };
138 if let Ok(runtime) = tokio::runtime::Handle::try_current() {
139 runtime.spawn(run);
140 }
141 }
142}
143
144pub(crate) fn request_error(err: &anyhow::Error) {
146 if let Some(state) = crate::context::app() {
147 let report = ErrorReport::new(
148 &state,
149 ReportKind::Request,
150 err.to_string(),
151 format!("{err:?}"),
152 None,
153 );
154 send(&state, report);
155 }
156}