Skip to main content

sova_devtools/
collector.rs

1//! Per-request collector bag + finished snapshot.
2
3use serde::Serialize;
4use std::sync::{Arc, Mutex};
5use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
6
7#[derive(Clone, Debug, Serialize)]
8pub struct LogLine {
9    pub level: String,
10    pub target: String,
11    pub message: String,
12    pub request_id: Option<String>,
13    pub at_ms: u64,
14}
15
16#[derive(Clone, Debug, Serialize)]
17pub struct QueryLine {
18    pub sql: String,
19    pub duration_ms: Option<f64>,
20    pub rows: Option<u64>,
21}
22
23#[derive(Clone, Debug, Serialize)]
24pub struct HttpLine {
25    pub method: String,
26    pub url: String,
27    pub status: Option<u16>,
28    pub duration_ms: Option<f64>,
29    pub error: Option<String>,
30}
31
32#[derive(Clone, Debug, Serialize)]
33pub struct MailLine {
34    pub to: Vec<String>,
35    pub subject: String,
36    pub backend: String,
37}
38
39#[derive(Clone, Debug, Serialize)]
40pub struct JobLine {
41    pub name: String,
42    pub status: String,
43    pub detail: Option<String>,
44}
45
46#[derive(Clone, Debug, Default, Serialize)]
47pub struct AuthSnap {
48    pub session_id: Option<String>,
49    pub user_id: Option<String>,
50    pub session_keys: Vec<(String, String)>,
51}
52
53#[derive(Clone, Debug, Serialize)]
54pub struct RequestSnapshot {
55    pub id: String,
56    pub request_id: String,
57    pub method: String,
58    pub path: String,
59    pub status: u16,
60    pub duration_ms: f64,
61    pub at_ms: u64,
62    pub logs: Vec<LogLine>,
63    pub queries: Vec<QueryLine>,
64    pub http: Vec<HttpLine>,
65    pub mail: Vec<MailLine>,
66    pub jobs: Vec<JobLine>,
67    pub auth: AuthSnap,
68}
69
70#[derive(Clone, Debug, Serialize)]
71pub struct RequestMeta {
72    pub id: String,
73    pub request_id: String,
74    pub method: String,
75    pub path: String,
76    pub status: u16,
77    pub duration_ms: f64,
78    pub at_ms: u64,
79    pub sql_count: usize,
80    pub log_errors: usize,
81    pub http_count: usize,
82    pub mail_count: usize,
83}
84
85impl From<&RequestSnapshot> for RequestMeta {
86    fn from(s: &RequestSnapshot) -> Self {
87        Self {
88            id: s.id.clone(),
89            request_id: s.request_id.clone(),
90            method: s.method.clone(),
91            path: s.path.clone(),
92            status: s.status,
93            duration_ms: s.duration_ms,
94            at_ms: s.at_ms,
95            sql_count: s.queries.len(),
96            log_errors: s
97                .logs
98                .iter()
99                .filter(|l| l.level.eq_ignore_ascii_case("ERROR") || l.level == "ERROR")
100                .count(),
101            http_count: s.http.len(),
102            mail_count: s.mail.len(),
103        }
104    }
105}
106
107#[derive(Default)]
108struct BagInner {
109    logs: Vec<LogLine>,
110    queries: Vec<QueryLine>,
111    http: Vec<HttpLine>,
112    mail: Vec<MailLine>,
113    jobs: Vec<JobLine>,
114    auth: AuthSnap,
115}
116
117/// Per-request collection bag (stored on Request extensions).
118#[derive(Clone)]
119pub struct DevToolsBag {
120    pub id: String,
121    pub request_id: String,
122    pub method: String,
123    pub path: String,
124    pub started: Instant,
125    inner: Arc<Mutex<BagInner>>,
126}
127
128impl DevToolsBag {
129    pub fn new(id: String, request_id: String, method: String, path: String) -> Self {
130        Self {
131            id,
132            request_id,
133            method,
134            path,
135            started: Instant::now(),
136            inner: Arc::new(Mutex::new(BagInner::default())),
137        }
138    }
139
140    pub fn push_log(&self, line: LogLine) {
141        let mut g = self.inner.lock().unwrap();
142        if g.logs.len() < 200 {
143            g.logs.push(line);
144        }
145    }
146
147    pub fn push_query(&self, q: QueryLine) {
148        let mut g = self.inner.lock().unwrap();
149        if g.queries.len() < 200 {
150            g.queries.push(q);
151        }
152    }
153
154    pub fn push_http(&self, h: HttpLine) {
155        let mut g = self.inner.lock().unwrap();
156        if g.http.len() < 100 {
157            g.http.push(h);
158        }
159    }
160
161    pub fn push_mail(&self, m: MailLine) {
162        let mut g = self.inner.lock().unwrap();
163        if g.mail.len() < 50 {
164            g.mail.push(m);
165        }
166    }
167
168    pub fn push_job(&self, j: JobLine) {
169        let mut g = self.inner.lock().unwrap();
170        if g.jobs.len() < 50 {
171            g.jobs.push(j);
172        }
173    }
174
175    pub fn set_auth(&self, auth: AuthSnap) {
176        self.inner.lock().unwrap().auth = auth;
177    }
178
179    pub fn finish(self, status: u16) -> RequestSnapshot {
180        let duration_ms = self.started.elapsed().as_secs_f64() * 1000.0;
181        let at_ms = SystemTime::now()
182            .duration_since(UNIX_EPOCH)
183            .unwrap_or(Duration::ZERO)
184            .as_millis() as u64;
185        let inner = self.inner.lock().unwrap();
186        RequestSnapshot {
187            id: self.id,
188            request_id: self.request_id,
189            method: self.method,
190            path: self.path,
191            status,
192            duration_ms,
193            at_ms,
194            logs: inner.logs.clone(),
195            queries: inner.queries.clone(),
196            http: inner.http.clone(),
197            mail: inner.mail.clone(),
198            jobs: inner.jobs.clone(),
199            auth: inner.auth.clone(),
200        }
201    }
202}
203
204pub fn now_ms() -> u64 {
205    SystemTime::now()
206        .duration_since(UNIX_EPOCH)
207        .unwrap_or(Duration::ZERO)
208        .as_millis() as u64
209}