1use std::time::Instant;
7
8use serde::{Deserialize, Serialize};
9use tracing::{Level, debug, span, warn};
10
11use crate::error::Result;
12
13#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct QueryPhaseSpan {
16 pub phase: String,
18
19 pub duration_us: u64,
21
22 pub success: bool,
24
25 pub error: Option<String>,
27}
28
29#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct QueryExecutionTrace {
35 pub query_id: String,
37
38 pub query: String,
40
41 pub phases: Vec<QueryPhaseSpan>,
43
44 pub total_duration_us: u64,
46
47 pub success: bool,
49
50 pub error: Option<String>,
52
53 pub result_count: Option<usize>,
55}
56
57#[must_use = "call .finish() to construct the final value"]
83pub struct QueryTraceBuilder {
84 query_id: String,
85 query: String,
86 phases: Vec<QueryPhaseSpan>,
87 start: Instant,
88}
89
90impl QueryTraceBuilder {
91 pub fn new(query_id: &str, query: &str) -> Self {
98 let query_str = if query.len() > 500 {
99 format!("{}...", &query[..500])
100 } else {
101 query.to_string()
102 };
103
104 Self {
105 query_id: query_id.to_string(),
106 query: query_str,
107 phases: Vec::new(),
108 start: Instant::now(),
109 }
110 }
111
112 pub fn record_phase_success(&mut self, phase_name: &str, duration_us: u64) {
119 self.phases.push(QueryPhaseSpan {
120 phase: phase_name.to_string(),
121 duration_us,
122 success: true,
123 error: None,
124 });
125
126 debug!(phase = phase_name, duration_us = duration_us, "Query phase completed");
127 }
128
129 pub fn record_phase_error(&mut self, phase_name: &str, duration_us: u64, error: &str) {
137 self.phases.push(QueryPhaseSpan {
138 phase: phase_name.to_string(),
139 duration_us,
140 success: false,
141 error: Some(error.to_string()),
142 });
143
144 warn!(
145 phase = phase_name,
146 duration_us = duration_us,
147 error = error,
148 "Query phase failed"
149 );
150 }
151
152 pub fn finish(
168 self,
169 success: bool,
170 error: Option<&str>,
171 result_count: Option<usize>,
172 ) -> Result<QueryExecutionTrace> {
173 let total_duration_us = u64::try_from(self.start.elapsed().as_micros()).unwrap_or(u64::MAX);
174
175 Ok(QueryExecutionTrace {
176 query_id: self.query_id.clone(),
177 query: self.query,
178 phases: self.phases,
179 total_duration_us,
180 success,
181 error: error.map(|e| e.to_string()),
182 result_count,
183 })
184 }
185
186 pub fn query_id(&self) -> &str {
188 &self.query_id
189 }
190
191 pub fn elapsed_us(&self) -> u64 {
193 u64::try_from(self.start.elapsed().as_micros()).unwrap_or(u64::MAX)
194 }
195}
196
197impl QueryExecutionTrace {
198 pub fn average_phase_duration_us(&self) -> u64 {
200 if self.phases.is_empty() {
201 0
202 } else {
203 self.phases.iter().map(|p| p.duration_us).sum::<u64>() / self.phases.len() as u64
204 }
205 }
206
207 pub fn slowest_phase(&self) -> Option<&QueryPhaseSpan> {
209 self.phases.iter().max_by_key(|p| p.duration_us)
210 }
211
212 pub fn to_log_string(&self) -> String {
216 let status = if self.success { "success" } else { "error" };
217 let phases_str = self
218 .phases
219 .iter()
220 .map(|p| format!("{}={}us", p.phase, p.duration_us))
221 .collect::<Vec<_>>()
222 .join(" ");
223
224 let error_str = self.error.as_ref().map(|e| format!(" error={}", e)).unwrap_or_default();
225
226 format!(
227 "query_id={} status={} total={}us phases=[{}]{}",
228 self.query_id, status, self.total_duration_us, phases_str, error_str
229 )
230 }
231}
232
233pub fn create_query_span(query_id: &str, query_text: &str) -> tracing::Span {
249 span!(
250 Level::DEBUG,
251 "query_execute",
252 query_id = query_id,
253 query = truncate_query(query_text, 100),
254 )
255}
256
257pub fn create_phase_span(phase_name: &str, query_id: &str) -> tracing::Span {
264 span!(Level::DEBUG, "query_phase", phase = phase_name, query_id = query_id)
265}
266
267fn truncate_query(query: &str, max_len: usize) -> String {
271 if query.len() > max_len {
272 format!("{}...", &query[..max_len])
273 } else {
274 query.to_string()
275 }
276}
277
278#[cfg(test)]
279mod tests {
280 #![allow(clippy::unwrap_used)] use super::*;
283
284 #[test]
285 fn test_trace_builder_new() {
286 let builder = QueryTraceBuilder::new("query_1", "{ user { id } }");
287 assert_eq!(builder.query_id, "query_1");
288 assert_eq!(builder.query, "{ user { id } }");
289 assert!(builder.phases.is_empty());
290 }
291
292 #[test]
293 fn test_trace_builder_truncate_long_query() {
294 let long_query = "a".repeat(600);
295 let builder = QueryTraceBuilder::new("query_1", &long_query);
296 assert!(builder.query.len() < 600);
297 assert!(builder.query.ends_with("..."));
298 }
299
300 #[test]
301 fn test_record_phase_success() {
302 let mut builder = QueryTraceBuilder::new("query_1", "{ user { id } }");
303 builder.record_phase_success("parse", 100);
304 builder.record_phase_success("validate", 50);
305
306 assert_eq!(builder.phases.len(), 2);
307 assert_eq!(builder.phases[0].phase, "parse");
308 assert_eq!(builder.phases[0].duration_us, 100);
309 assert!(builder.phases[0].success);
310 }
311
312 #[test]
313 fn test_record_phase_error() {
314 let mut builder = QueryTraceBuilder::new("query_1", "{ user { id } }");
315 builder.record_phase_error("parse", 100, "Invalid syntax");
316
317 assert_eq!(builder.phases.len(), 1);
318 assert_eq!(builder.phases[0].phase, "parse");
319 assert!(!builder.phases[0].success);
320 assert_eq!(builder.phases[0].error, Some("Invalid syntax".to_string()));
321 }
322
323 #[test]
324 fn test_finish_success() {
325 let mut builder = QueryTraceBuilder::new("query_1", "{ user { id } }");
326 builder.record_phase_success("parse", 100);
327 builder.record_phase_success("execute", 500);
328
329 let trace = builder.finish(true, None, Some(10)).unwrap();
330 assert!(trace.success);
331 assert_eq!(trace.query_id, "query_1");
332 assert_eq!(trace.phases.len(), 2);
333 assert_eq!(trace.result_count, Some(10));
334 }
336
337 #[test]
338 fn test_finish_error() {
339 let mut builder = QueryTraceBuilder::new("query_1", "{ user { id } }");
340 builder.record_phase_success("parse", 100);
341 builder.record_phase_error("execute", 50, "Database connection failed");
342
343 let trace = builder.finish(false, Some("Database connection failed"), None).unwrap();
344 assert!(!trace.success);
345 assert_eq!(trace.error, Some("Database connection failed".to_string()));
346 }
347
348 #[test]
349 fn test_average_phase_duration() {
350 let mut builder = QueryTraceBuilder::new("query_1", "{ user { id } }");
351 builder.record_phase_success("parse", 100);
352 builder.record_phase_success("validate", 200);
353 builder.record_phase_success("execute", 300);
354
355 let trace = builder.finish(true, None, None).unwrap();
356 assert_eq!(trace.average_phase_duration_us(), 200);
357 }
358
359 #[test]
360 fn test_slowest_phase() {
361 let mut builder = QueryTraceBuilder::new("query_1", "{ user { id } }");
362 builder.record_phase_success("parse", 100);
363 builder.record_phase_success("execute", 500);
364 builder.record_phase_success("cache_check", 50);
365
366 let trace = builder.finish(true, None, None).unwrap();
367 let slowest = trace.slowest_phase().unwrap();
368 assert_eq!(slowest.phase, "execute");
369 assert_eq!(slowest.duration_us, 500);
370 }
371
372 #[test]
373 fn test_to_log_string_success() {
374 let mut builder = QueryTraceBuilder::new("query_1", "{ user { id } }");
375 builder.record_phase_success("parse", 100);
376 builder.record_phase_success("execute", 500);
377
378 let trace = builder.finish(true, None, Some(5)).unwrap();
379 let log_str = trace.to_log_string();
380 assert!(log_str.contains("query_id=query_1"));
381 assert!(log_str.contains("status=success"));
382 assert!(log_str.contains("parse=100us"));
383 assert!(log_str.contains("execute=500us"));
384 }
385
386 #[test]
387 fn test_to_log_string_error() {
388 let mut builder = QueryTraceBuilder::new("query_1", "{ user { id } }");
389 builder.record_phase_success("parse", 100);
390 builder.record_phase_error("validate", 50, "Type mismatch");
391
392 let trace = builder.finish(false, Some("Type mismatch"), None).unwrap();
393 let log_str = trace.to_log_string();
394 assert!(log_str.contains("status=error"));
395 assert!(log_str.contains("error=Type mismatch"));
396 }
397
398 #[test]
399 fn test_average_phase_duration_empty() {
400 let builder = QueryTraceBuilder::new("query_1", "{ user { id } }");
401 let trace = builder.finish(true, None, None).unwrap();
402 assert_eq!(trace.average_phase_duration_us(), 0);
403 }
404
405 #[test]
406 fn test_elapsed_us() {
407 let builder = QueryTraceBuilder::new("query_1", "{ user { id } }");
408 let elapsed = builder.elapsed_us();
409 let _ = elapsed;
411 }
412
413 #[test]
414 fn test_trace_serialization() {
415 let mut builder = QueryTraceBuilder::new("query_1", "{ user { id } }");
416 builder.record_phase_success("parse", 100);
417
418 let trace = builder.finish(true, None, Some(5)).unwrap();
419 let json = serde_json::to_string(&trace).expect("serialize should work");
420 let restored: QueryExecutionTrace =
421 serde_json::from_str(&json).expect("deserialize should work");
422
423 assert_eq!(restored.query_id, trace.query_id);
424 assert_eq!(restored.phases.len(), trace.phases.len());
425 }
426
427 #[test]
428 fn test_query_phase_span_serialize() {
429 let span = QueryPhaseSpan {
430 phase: "parse".to_string(),
431 duration_us: 100,
432 success: true,
433 error: None,
434 };
435
436 let json = serde_json::to_string(&span).expect("serialize should work");
437 let restored: QueryPhaseSpan =
438 serde_json::from_str(&json).expect("deserialize should work");
439
440 assert_eq!(restored.phase, span.phase);
441 assert_eq!(restored.duration_us, span.duration_us);
442 }
443
444 #[test]
445 fn test_truncate_query_helper() {
446 assert_eq!(truncate_query("hello", 100), "hello");
447 assert!(truncate_query(&"a".repeat(200), 50).ends_with("..."));
448 }
449}