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 GraphqlLine {
50 pub operation: String,
51 pub kind: String,
52 pub duration_ms: f64,
53 pub errors: usize,
54 #[serde(skip_serializing_if = "Option::is_none")]
55 pub auth: Option<bool>,
56}
57
58#[derive(Clone, Debug, Serialize)]
59pub struct GrpcLine {
60 pub method: String,
61 pub base: String,
62 pub direction: String,
63 pub duration_ms: f64,
64 pub ok: bool,
65 #[serde(skip_serializing_if = "Option::is_none")]
66 pub status: Option<u16>,
67 #[serde(skip_serializing_if = "Option::is_none")]
68 pub error: Option<String>,
69 #[serde(skip_serializing_if = "Option::is_none")]
70 pub bytes_in: Option<u64>,
71 #[serde(skip_serializing_if = "Option::is_none")]
72 pub bytes_out: Option<u64>,
73}
74
75#[derive(Clone, Debug, Serialize)]
76pub struct RabbitLine {
77 pub op: String,
78 #[serde(skip_serializing_if = "Option::is_none")]
79 pub exchange: Option<String>,
80 #[serde(skip_serializing_if = "Option::is_none")]
81 pub routing_key: Option<String>,
82 #[serde(skip_serializing_if = "Option::is_none")]
83 pub queue: Option<String>,
84 #[serde(skip_serializing_if = "Option::is_none")]
85 pub bytes: Option<u64>,
86 pub duration_ms: f64,
87 pub ok: bool,
88 #[serde(skip_serializing_if = "Option::is_none")]
89 pub error: Option<String>,
90}
91
92#[derive(Clone, Debug, Serialize)]
93pub struct CacheLine {
94 pub op: String,
96 pub key: String,
97 #[serde(skip_serializing_if = "Option::is_none")]
98 pub hit: Option<bool>,
99 #[serde(skip_serializing_if = "Option::is_none")]
100 pub bytes: Option<u64>,
101 #[serde(skip_serializing_if = "Option::is_none")]
102 pub duration_ms: Option<f64>,
103 pub backend: String,
105 #[serde(skip_serializing_if = "Option::is_none")]
106 pub ok: Option<bool>,
107}
108
109#[derive(Clone, Debug, Default, Serialize)]
110pub struct RouteSnap {
111 pub path: String,
112 #[serde(skip_serializing_if = "Option::is_none")]
113 pub pattern: Option<String>,
114 #[serde(default, skip_serializing_if = "Vec::is_empty")]
115 pub captures: Vec<(String, String)>,
116}
117
118#[derive(Clone, Debug, Default, Serialize)]
119pub struct RateLimitSnap {
120 #[serde(skip_serializing_if = "Option::is_none")]
121 pub limit: Option<u64>,
122 #[serde(skip_serializing_if = "Option::is_none")]
123 pub remaining: Option<u64>,
124 #[serde(skip_serializing_if = "Option::is_none")]
125 pub reset: Option<u64>,
126}
127
128#[derive(Clone, Debug, Default, Serialize)]
129pub struct AuthSnap {
130 pub session_id: Option<String>,
131 pub user_id: Option<String>,
132 #[serde(skip_serializing_if = "Option::is_none")]
133 pub email: Option<String>,
134 #[serde(default, skip_serializing_if = "Vec::is_empty")]
135 pub roles: Vec<String>,
136 pub session_keys: Vec<(String, String)>,
137}
138
139#[derive(Clone, Debug, Serialize)]
140pub struct RequestSnapshot {
141 pub id: String,
142 pub request_id: String,
143 pub method: String,
144 pub path: String,
145 pub status: u16,
146 pub duration_ms: f64,
147 pub at_ms: u64,
148 pub logs: Vec<LogLine>,
149 pub queries: Vec<QueryLine>,
150 pub http: Vec<HttpLine>,
151 pub mail: Vec<MailLine>,
152 pub jobs: Vec<JobLine>,
153 pub cache: Vec<CacheLine>,
154 #[serde(default, skip_serializing_if = "Vec::is_empty")]
155 pub graphql: Vec<GraphqlLine>,
156 #[serde(default, skip_serializing_if = "Vec::is_empty")]
157 pub grpc: Vec<GrpcLine>,
158 #[serde(default, skip_serializing_if = "Vec::is_empty")]
159 pub rabbit: Vec<RabbitLine>,
160 pub auth: AuthSnap,
161 pub route: RouteSnap,
162 #[serde(skip_serializing_if = "Option::is_none")]
163 pub locale: Option<String>,
164 #[serde(skip_serializing_if = "Option::is_none")]
165 pub csrf: Option<bool>,
166 #[serde(skip_serializing_if = "Option::is_none")]
167 pub rate_limit: Option<RateLimitSnap>,
168 #[serde(skip_serializing_if = "Option::is_none")]
169 pub encoding: Option<String>,
170}
171
172#[derive(Clone, Debug, Serialize)]
173pub struct RequestMeta {
174 pub id: String,
175 pub request_id: String,
176 pub method: String,
177 pub path: String,
178 pub status: u16,
179 pub duration_ms: f64,
180 pub at_ms: u64,
181 pub sql_count: usize,
182 pub log_errors: usize,
183 pub http_count: usize,
184 pub mail_count: usize,
185 pub cache_count: usize,
186 pub graphql_count: usize,
187 pub grpc_count: usize,
188 pub rabbit_count: usize,
189 pub job_count: usize,
190}
191
192impl From<&RequestSnapshot> for RequestMeta {
193 fn from(s: &RequestSnapshot) -> Self {
194 Self {
195 id: s.id.clone(),
196 request_id: s.request_id.clone(),
197 method: s.method.clone(),
198 path: s.path.clone(),
199 status: s.status,
200 duration_ms: s.duration_ms,
201 at_ms: s.at_ms,
202 sql_count: s.queries.len(),
203 log_errors: s
204 .logs
205 .iter()
206 .filter(|l| l.level.eq_ignore_ascii_case("ERROR") || l.level == "ERROR")
207 .count(),
208 http_count: s.http.len(),
209 mail_count: s.mail.len(),
210 cache_count: s.cache.len(),
211 graphql_count: s.graphql.len(),
212 grpc_count: s.grpc.len(),
213 rabbit_count: s.rabbit.len(),
214 job_count: s.jobs.len(),
215 }
216 }
217}
218
219#[derive(Default)]
220struct BagInner {
221 logs: Vec<LogLine>,
222 queries: Vec<QueryLine>,
223 http: Vec<HttpLine>,
224 mail: Vec<MailLine>,
225 jobs: Vec<JobLine>,
226 cache: Vec<CacheLine>,
227 graphql: Vec<GraphqlLine>,
228 grpc: Vec<GrpcLine>,
229 rabbit: Vec<RabbitLine>,
230 auth: AuthSnap,
231 route: RouteSnap,
232 locale: Option<String>,
233 csrf: Option<bool>,
234 rate_limit: Option<RateLimitSnap>,
235 encoding: Option<String>,
236}
237
238#[derive(Clone)]
240pub struct DevToolsBag {
241 pub id: String,
242 pub request_id: String,
243 pub method: String,
244 pub path: String,
245 pub started: Instant,
246 inner: Arc<Mutex<BagInner>>,
247}
248
249impl DevToolsBag {
250 pub fn new(id: String, request_id: String, method: String, path: String) -> Self {
251 Self {
252 id,
253 request_id,
254 method,
255 path: path.clone(),
256 started: Instant::now(),
257 inner: Arc::new(Mutex::new(BagInner {
258 route: RouteSnap {
259 path,
260 ..Default::default()
261 },
262 ..Default::default()
263 })),
264 }
265 }
266
267 pub fn push_log(&self, line: LogLine) {
268 let mut g = self.inner.lock().unwrap();
269 if g.logs.len() < 200 {
270 g.logs.push(line);
271 }
272 }
273
274 pub fn push_query(&self, q: QueryLine) {
275 let mut g = self.inner.lock().unwrap();
276 if g.queries.len() < 200 {
277 g.queries.push(q);
278 }
279 }
280
281 pub fn push_http(&self, h: HttpLine) {
282 let mut g = self.inner.lock().unwrap();
283 if g.http.len() < 100 {
284 g.http.push(h);
285 }
286 }
287
288 pub fn push_mail(&self, m: MailLine) {
289 let mut g = self.inner.lock().unwrap();
290 if g.mail.len() < 50 {
291 g.mail.push(m);
292 }
293 }
294
295 pub fn push_job(&self, j: JobLine) {
296 let mut g = self.inner.lock().unwrap();
297 if g.jobs.len() < 50 {
298 g.jobs.push(j);
299 }
300 }
301
302 pub fn push_cache(&self, c: CacheLine) {
303 let mut g = self.inner.lock().unwrap();
304 if g.cache.len() < 200 {
305 g.cache.push(c);
306 }
307 }
308
309 pub fn push_graphql(&self, gql: GraphqlLine) {
310 let mut g = self.inner.lock().unwrap();
311 if g.graphql.len() < 50 {
312 g.graphql.push(gql);
313 }
314 }
315
316 pub fn push_grpc(&self, line: GrpcLine) {
317 let mut g = self.inner.lock().unwrap();
318 if g.grpc.len() < 50 {
319 g.grpc.push(line);
320 }
321 }
322
323 pub fn push_rabbit(&self, line: RabbitLine) {
324 let mut g = self.inner.lock().unwrap();
325 if g.rabbit.len() < 50 {
326 g.rabbit.push(line);
327 }
328 }
329
330 pub fn set_auth(&self, auth: AuthSnap) {
331 self.inner.lock().unwrap().auth = auth;
332 }
333
334 pub fn set_route(&self, route: RouteSnap) {
335 self.inner.lock().unwrap().route = route;
336 }
337
338 pub fn set_locale(&self, locale: Option<String>) {
339 self.inner.lock().unwrap().locale = locale;
340 }
341
342 pub fn set_csrf(&self, present: Option<bool>) {
343 self.inner.lock().unwrap().csrf = present;
344 }
345
346 pub fn set_rate_limit(&self, rl: Option<RateLimitSnap>) {
347 self.inner.lock().unwrap().rate_limit = rl;
348 }
349
350 pub fn set_encoding(&self, encoding: Option<String>) {
351 self.inner.lock().unwrap().encoding = encoding;
352 }
353
354 pub fn finish(self, status: u16) -> RequestSnapshot {
355 let duration_ms = self.started.elapsed().as_secs_f64() * 1000.0;
356 let at_ms = SystemTime::now()
357 .duration_since(UNIX_EPOCH)
358 .unwrap_or(Duration::ZERO)
359 .as_millis() as u64;
360 let inner = self.inner.lock().unwrap();
361 RequestSnapshot {
362 id: self.id,
363 request_id: self.request_id,
364 method: self.method,
365 path: self.path,
366 status,
367 duration_ms,
368 at_ms,
369 logs: inner.logs.clone(),
370 queries: inner.queries.clone(),
371 http: inner.http.clone(),
372 mail: inner.mail.clone(),
373 jobs: inner.jobs.clone(),
374 cache: inner.cache.clone(),
375 graphql: inner.graphql.clone(),
376 grpc: inner.grpc.clone(),
377 rabbit: inner.rabbit.clone(),
378 auth: inner.auth.clone(),
379 route: inner.route.clone(),
380 locale: inner.locale.clone(),
381 csrf: inner.csrf,
382 rate_limit: inner.rate_limit.clone(),
383 encoding: inner.encoding.clone(),
384 }
385 }
386}
387
388pub fn now_ms() -> u64 {
389 SystemTime::now()
390 .duration_since(UNIX_EPOCH)
391 .unwrap_or(Duration::ZERO)
392 .as_millis() as u64
393}
394
395pub fn truncate_key(key: &str, max: usize) -> String {
397 if key.len() <= max {
398 key.to_string()
399 } else {
400 format!("{}…", &key[..max.saturating_sub(1)])
401 }
402}