1use 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 #[serde(skip_serializing_if = "Option::is_none")]
45 pub duration_ms: Option<f64>,
46}
47
48#[derive(Clone, Debug, Serialize)]
49pub struct CacheLine {
50 pub op: String,
52 pub key: String,
53 #[serde(skip_serializing_if = "Option::is_none")]
54 pub hit: Option<bool>,
55 #[serde(skip_serializing_if = "Option::is_none")]
56 pub bytes: Option<u64>,
57 #[serde(skip_serializing_if = "Option::is_none")]
58 pub duration_ms: Option<f64>,
59 pub backend: String,
61 #[serde(skip_serializing_if = "Option::is_none")]
62 pub ok: Option<bool>,
63}
64
65#[derive(Clone, Debug, Default, Serialize)]
66pub struct RouteSnap {
67 pub path: String,
68 #[serde(skip_serializing_if = "Option::is_none")]
69 pub pattern: Option<String>,
70 #[serde(default, skip_serializing_if = "Vec::is_empty")]
71 pub captures: Vec<(String, String)>,
72}
73
74#[derive(Clone, Debug, Default, Serialize)]
75pub struct RateLimitSnap {
76 #[serde(skip_serializing_if = "Option::is_none")]
77 pub limit: Option<u64>,
78 #[serde(skip_serializing_if = "Option::is_none")]
79 pub remaining: Option<u64>,
80 #[serde(skip_serializing_if = "Option::is_none")]
81 pub reset: Option<u64>,
82}
83
84#[derive(Clone, Debug, Default, Serialize)]
85pub struct AuthSnap {
86 pub session_id: Option<String>,
87 pub user_id: Option<String>,
88 #[serde(skip_serializing_if = "Option::is_none")]
89 pub email: Option<String>,
90 #[serde(default, skip_serializing_if = "Vec::is_empty")]
91 pub roles: Vec<String>,
92 pub session_keys: Vec<(String, String)>,
93}
94
95#[derive(Clone, Debug, Serialize)]
96pub struct RequestSnapshot {
97 pub id: String,
98 pub request_id: String,
99 pub method: String,
100 pub path: String,
101 pub status: u16,
102 pub duration_ms: f64,
103 pub at_ms: u64,
104 pub logs: Vec<LogLine>,
105 pub queries: Vec<QueryLine>,
106 pub http: Vec<HttpLine>,
107 pub mail: Vec<MailLine>,
108 pub jobs: Vec<JobLine>,
109 pub cache: Vec<CacheLine>,
110 pub auth: AuthSnap,
111 pub route: RouteSnap,
112 #[serde(skip_serializing_if = "Option::is_none")]
113 pub locale: Option<String>,
114 #[serde(skip_serializing_if = "Option::is_none")]
115 pub csrf: Option<bool>,
116 #[serde(skip_serializing_if = "Option::is_none")]
117 pub rate_limit: Option<RateLimitSnap>,
118 #[serde(skip_serializing_if = "Option::is_none")]
119 pub encoding: Option<String>,
120}
121
122#[derive(Clone, Debug, Serialize)]
123pub struct RequestMeta {
124 pub id: String,
125 pub request_id: String,
126 pub method: String,
127 pub path: String,
128 pub status: u16,
129 pub duration_ms: f64,
130 pub at_ms: u64,
131 pub sql_count: usize,
132 pub log_errors: usize,
133 pub http_count: usize,
134 pub mail_count: usize,
135 pub cache_count: usize,
136 pub job_count: usize,
137}
138
139impl From<&RequestSnapshot> for RequestMeta {
140 fn from(s: &RequestSnapshot) -> Self {
141 Self {
142 id: s.id.clone(),
143 request_id: s.request_id.clone(),
144 method: s.method.clone(),
145 path: s.path.clone(),
146 status: s.status,
147 duration_ms: s.duration_ms,
148 at_ms: s.at_ms,
149 sql_count: s.queries.len(),
150 log_errors: s
151 .logs
152 .iter()
153 .filter(|l| l.level.eq_ignore_ascii_case("ERROR") || l.level == "ERROR")
154 .count(),
155 http_count: s.http.len(),
156 mail_count: s.mail.len(),
157 cache_count: s.cache.len(),
158 job_count: s.jobs.len(),
159 }
160 }
161}
162
163#[derive(Default)]
164struct BagInner {
165 logs: Vec<LogLine>,
166 queries: Vec<QueryLine>,
167 http: Vec<HttpLine>,
168 mail: Vec<MailLine>,
169 jobs: Vec<JobLine>,
170 cache: Vec<CacheLine>,
171 auth: AuthSnap,
172 route: RouteSnap,
173 locale: Option<String>,
174 csrf: Option<bool>,
175 rate_limit: Option<RateLimitSnap>,
176 encoding: Option<String>,
177}
178
179#[derive(Clone)]
181pub struct DevToolsBag {
182 pub id: String,
183 pub request_id: String,
184 pub method: String,
185 pub path: String,
186 pub started: Instant,
187 inner: Arc<Mutex<BagInner>>,
188}
189
190impl DevToolsBag {
191 pub fn new(id: String, request_id: String, method: String, path: String) -> Self {
192 Self {
193 id,
194 request_id,
195 method,
196 path: path.clone(),
197 started: Instant::now(),
198 inner: Arc::new(Mutex::new(BagInner {
199 route: RouteSnap {
200 path,
201 ..Default::default()
202 },
203 ..Default::default()
204 })),
205 }
206 }
207
208 pub fn push_log(&self, line: LogLine) {
209 let mut g = self.inner.lock().unwrap();
210 if g.logs.len() < 200 {
211 g.logs.push(line);
212 }
213 }
214
215 pub fn push_query(&self, q: QueryLine) {
216 let mut g = self.inner.lock().unwrap();
217 if g.queries.len() < 200 {
218 g.queries.push(q);
219 }
220 }
221
222 pub fn push_http(&self, h: HttpLine) {
223 let mut g = self.inner.lock().unwrap();
224 if g.http.len() < 100 {
225 g.http.push(h);
226 }
227 }
228
229 pub fn push_mail(&self, m: MailLine) {
230 let mut g = self.inner.lock().unwrap();
231 if g.mail.len() < 50 {
232 g.mail.push(m);
233 }
234 }
235
236 pub fn push_job(&self, j: JobLine) {
237 let mut g = self.inner.lock().unwrap();
238 if g.jobs.len() < 50 {
239 g.jobs.push(j);
240 }
241 }
242
243 pub fn push_cache(&self, c: CacheLine) {
244 let mut g = self.inner.lock().unwrap();
245 if g.cache.len() < 200 {
246 g.cache.push(c);
247 }
248 }
249
250 pub fn set_auth(&self, auth: AuthSnap) {
251 self.inner.lock().unwrap().auth = auth;
252 }
253
254 pub fn set_route(&self, route: RouteSnap) {
255 self.inner.lock().unwrap().route = route;
256 }
257
258 pub fn set_locale(&self, locale: Option<String>) {
259 self.inner.lock().unwrap().locale = locale;
260 }
261
262 pub fn set_csrf(&self, present: Option<bool>) {
263 self.inner.lock().unwrap().csrf = present;
264 }
265
266 pub fn set_rate_limit(&self, rl: Option<RateLimitSnap>) {
267 self.inner.lock().unwrap().rate_limit = rl;
268 }
269
270 pub fn set_encoding(&self, encoding: Option<String>) {
271 self.inner.lock().unwrap().encoding = encoding;
272 }
273
274 pub fn finish(self, status: u16) -> RequestSnapshot {
275 let duration_ms = self.started.elapsed().as_secs_f64() * 1000.0;
276 let at_ms = SystemTime::now()
277 .duration_since(UNIX_EPOCH)
278 .unwrap_or(Duration::ZERO)
279 .as_millis() as u64;
280 let inner = self.inner.lock().unwrap();
281 RequestSnapshot {
282 id: self.id,
283 request_id: self.request_id,
284 method: self.method,
285 path: self.path,
286 status,
287 duration_ms,
288 at_ms,
289 logs: inner.logs.clone(),
290 queries: inner.queries.clone(),
291 http: inner.http.clone(),
292 mail: inner.mail.clone(),
293 jobs: inner.jobs.clone(),
294 cache: inner.cache.clone(),
295 auth: inner.auth.clone(),
296 route: inner.route.clone(),
297 locale: inner.locale.clone(),
298 csrf: inner.csrf,
299 rate_limit: inner.rate_limit.clone(),
300 encoding: inner.encoding.clone(),
301 }
302 }
303}
304
305pub fn now_ms() -> u64 {
306 SystemTime::now()
307 .duration_since(UNIX_EPOCH)
308 .unwrap_or(Duration::ZERO)
309 .as_millis() as u64
310}
311
312pub fn truncate_key(key: &str, max: usize) -> String {
314 if key.len() <= max {
315 key.to_string()
316 } else {
317 format!("{}…", &key[..max.saturating_sub(1)])
318 }
319}