Skip to main content

oxirs_gql/observability/
query_debugger.rs

1//! Debug Query Execution Plans
2//!
3//! Provides detailed debugging capabilities for GraphQL query execution,
4//! including execution plan visualization and step-by-step tracing.
5//!
6//! # Features
7//!
8//! - Execution plan generation and visualization
9//! - Step-by-step query execution tracing
10//! - Field resolver timing and performance
11//! - Data source query inspection
12//! - Variable interpolation tracking
13//! - Error propagation analysis
14//! - Execution tree visualization
15
16use std::collections::HashMap;
17use std::time::{Duration, Instant};
18
19/// Execution step type
20#[derive(Debug, Clone, PartialEq)]
21pub enum StepType {
22    Parse,
23    Validate,
24    Optimize,
25    ResolveField {
26        field_name: String,
27        parent_type: String,
28    },
29    ExecuteDataSource {
30        query: String,
31        source: String,
32    },
33    Transform,
34    Serialize,
35}
36
37/// Execution step with timing
38#[derive(Debug, Clone)]
39pub struct ExecutionStep {
40    pub step_id: usize,
41    pub step_type: StepType,
42    pub started_at: Instant,
43    pub duration: Option<Duration>,
44    pub status: StepStatus,
45    pub input: Option<String>,
46    pub output: Option<String>,
47    pub error: Option<String>,
48    pub metadata: HashMap<String, String>,
49}
50
51/// Step execution status
52#[derive(Debug, Clone, PartialEq)]
53pub enum StepStatus {
54    Pending,
55    Running,
56    Completed,
57    Failed,
58    Skipped,
59}
60
61/// Query execution plan
62#[derive(Debug, Clone)]
63pub struct ExecutionPlan {
64    pub query_id: String,
65    pub query_text: String,
66    pub variables: HashMap<String, String>,
67    pub steps: Vec<ExecutionStep>,
68    pub total_duration: Option<Duration>,
69    pub created_at: Instant,
70}
71
72impl ExecutionPlan {
73    /// Create a new execution plan
74    pub fn new(query_id: String, query_text: String, variables: HashMap<String, String>) -> Self {
75        Self {
76            query_id,
77            query_text,
78            variables,
79            steps: Vec::new(),
80            total_duration: None,
81            created_at: Instant::now(),
82        }
83    }
84
85    /// Add a step to the plan
86    pub fn add_step(&mut self, step_type: StepType) -> usize {
87        let step_id = self.steps.len();
88        self.steps.push(ExecutionStep {
89            step_id,
90            step_type,
91            started_at: Instant::now(),
92            duration: None,
93            status: StepStatus::Pending,
94            input: None,
95            output: None,
96            error: None,
97            metadata: HashMap::new(),
98        });
99        step_id
100    }
101
102    /// Start a step
103    pub fn start_step(&mut self, step_id: usize) {
104        if let Some(step) = self.steps.get_mut(step_id) {
105            step.status = StepStatus::Running;
106            step.started_at = Instant::now();
107        }
108    }
109
110    /// Complete a step
111    pub fn complete_step(&mut self, step_id: usize, output: Option<String>) {
112        if let Some(step) = self.steps.get_mut(step_id) {
113            step.duration = Some(step.started_at.elapsed());
114            step.status = StepStatus::Completed;
115            step.output = output;
116        }
117    }
118
119    /// Fail a step
120    pub fn fail_step(&mut self, step_id: usize, error: String) {
121        if let Some(step) = self.steps.get_mut(step_id) {
122            step.duration = Some(step.started_at.elapsed());
123            step.status = StepStatus::Failed;
124            step.error = Some(error);
125        }
126    }
127
128    /// Skip a step
129    pub fn skip_step(&mut self, step_id: usize, reason: String) {
130        if let Some(step) = self.steps.get_mut(step_id) {
131            step.status = StepStatus::Skipped;
132            step.metadata.insert("skip_reason".to_string(), reason);
133        }
134    }
135
136    /// Set step metadata
137    pub fn set_step_metadata(&mut self, step_id: usize, key: String, value: String) {
138        if let Some(step) = self.steps.get_mut(step_id) {
139            step.metadata.insert(key, value);
140        }
141    }
142
143    /// Finalize the execution plan
144    pub fn finalize(&mut self) {
145        self.total_duration = Some(self.created_at.elapsed());
146    }
147
148    /// Get execution summary
149    pub fn summary(&self) -> ExecutionSummary {
150        let mut completed = 0;
151        let mut failed = 0;
152        let mut skipped = 0;
153        let mut total_time = Duration::ZERO;
154
155        for step in &self.steps {
156            match step.status {
157                StepStatus::Completed => {
158                    completed += 1;
159                    if let Some(duration) = step.duration {
160                        total_time += duration;
161                    }
162                }
163                StepStatus::Failed => failed += 1,
164                StepStatus::Skipped => skipped += 1,
165                _ => {}
166            }
167        }
168
169        ExecutionSummary {
170            total_steps: self.steps.len(),
171            completed_steps: completed,
172            failed_steps: failed,
173            skipped_steps: skipped,
174            total_duration: self.total_duration.unwrap_or(total_time),
175        }
176    }
177
178    /// Generate ASCII tree visualization
179    pub fn visualize_tree(&self) -> String {
180        let mut output = String::new();
181        output.push_str(&format!("Query: {} ({})\n", self.query_id, self.query_text));
182        output.push_str("Execution Steps:\n");
183
184        for (i, step) in self.steps.iter().enumerate() {
185            let prefix = if i == self.steps.len() - 1 {
186                "└─"
187            } else {
188                "├─"
189            };
190
191            let status_icon = match step.status {
192                StepStatus::Completed => "✓",
193                StepStatus::Failed => "✗",
194                StepStatus::Skipped => "○",
195                StepStatus::Running => "→",
196                StepStatus::Pending => "◦",
197            };
198
199            let duration_str = step
200                .duration
201                .map(|d| format!(" [{:.2}ms]", d.as_secs_f64() * 1000.0))
202                .unwrap_or_default();
203
204            output.push_str(&format!(
205                "{} {} {:?}{}\n",
206                prefix, status_icon, step.step_type, duration_str
207            ));
208
209            if let Some(ref error) = step.error {
210                output.push_str(&format!("  Error: {}\n", error));
211            }
212        }
213
214        output
215    }
216
217    /// Export as JSON
218    pub fn to_json(&self) -> String {
219        let mut json = String::from("{");
220        json.push_str(&format!("\"query_id\":\"{}\",", self.query_id));
221        json.push_str(&format!(
222            "\"query_text\":\"{}\",",
223            self.query_text.replace('"', "\\\"")
224        ));
225
226        // Steps
227        json.push_str("\"steps\":[");
228        for (i, step) in self.steps.iter().enumerate() {
229            if i > 0 {
230                json.push(',');
231            }
232            json.push_str(&format!(
233                "{{\"id\":{},\"type\":\"{:?}\",\"status\":\"{:?}\"",
234                step.step_id, step.step_type, step.status
235            ));
236            if let Some(duration) = step.duration {
237                json.push_str(&format!(
238                    ",\"duration_ms\":{}",
239                    duration.as_secs_f64() * 1000.0
240                ));
241            }
242            json.push('}');
243        }
244        json.push_str("],");
245
246        // Summary
247        let summary = self.summary();
248        json.push_str(&format!(
249            "\"summary\":{{\"total\":{},\"completed\":{},\"failed\":{}}}",
250            summary.total_steps, summary.completed_steps, summary.failed_steps
251        ));
252
253        json.push('}');
254        json
255    }
256}
257
258/// Execution summary
259#[derive(Debug, Clone)]
260pub struct ExecutionSummary {
261    pub total_steps: usize,
262    pub completed_steps: usize,
263    pub failed_steps: usize,
264    pub skipped_steps: usize,
265    pub total_duration: Duration,
266}
267
268/// Query debugger for managing execution plans
269pub struct QueryDebugger {
270    plans: HashMap<String, ExecutionPlan>,
271    max_plans: usize,
272}
273
274impl QueryDebugger {
275    /// Create a new query debugger
276    pub fn new(max_plans: usize) -> Self {
277        Self {
278            plans: HashMap::new(),
279            max_plans,
280        }
281    }
282
283    /// Start debugging a query
284    pub fn start_query(
285        &mut self,
286        query_id: String,
287        query_text: String,
288        variables: HashMap<String, String>,
289    ) -> String {
290        // Clean up old plans if at limit
291        if self.plans.len() >= self.max_plans {
292            // Remove oldest plan
293            if let Some(oldest_id) = self.plans.keys().next().cloned() {
294                self.plans.remove(&oldest_id);
295            }
296        }
297
298        let plan = ExecutionPlan::new(query_id.clone(), query_text, variables);
299        self.plans.insert(query_id.clone(), plan);
300        query_id
301    }
302
303    /// Add a step to a query's execution plan
304    pub fn add_step(&mut self, query_id: &str, step_type: StepType) -> Option<usize> {
305        self.plans
306            .get_mut(query_id)
307            .map(|plan| plan.add_step(step_type))
308    }
309
310    /// Start a step
311    pub fn start_step(&mut self, query_id: &str, step_id: usize) {
312        if let Some(plan) = self.plans.get_mut(query_id) {
313            plan.start_step(step_id);
314        }
315    }
316
317    /// Complete a step
318    pub fn complete_step(&mut self, query_id: &str, step_id: usize, output: Option<String>) {
319        if let Some(plan) = self.plans.get_mut(query_id) {
320            plan.complete_step(step_id, output);
321        }
322    }
323
324    /// Fail a step
325    pub fn fail_step(&mut self, query_id: &str, step_id: usize, error: String) {
326        if let Some(plan) = self.plans.get_mut(query_id) {
327            plan.fail_step(step_id, error);
328        }
329    }
330
331    /// Finalize a query execution plan
332    pub fn finalize_query(&mut self, query_id: &str) {
333        if let Some(plan) = self.plans.get_mut(query_id) {
334            plan.finalize();
335        }
336    }
337
338    /// Get an execution plan
339    pub fn get_plan(&self, query_id: &str) -> Option<&ExecutionPlan> {
340        self.plans.get(query_id)
341    }
342
343    /// Get all execution plans
344    pub fn get_all_plans(&self) -> Vec<&ExecutionPlan> {
345        self.plans.values().collect()
346    }
347
348    /// Get execution summary for a query
349    pub fn get_summary(&self, query_id: &str) -> Option<ExecutionSummary> {
350        self.plans.get(query_id).map(|plan| plan.summary())
351    }
352
353    /// Clear all plans
354    pub fn clear(&mut self) {
355        self.plans.clear();
356    }
357
358    /// Get plan count
359    pub fn plan_count(&self) -> usize {
360        self.plans.len()
361    }
362
363    /// Generate report for all plans
364    pub fn generate_report(&self) -> String {
365        let mut report = String::from("=== Query Execution Debug Report ===\n\n");
366        report.push_str(&format!("Total queries tracked: {}\n\n", self.plans.len()));
367
368        for plan in self.plans.values() {
369            report.push_str(&format!("Query ID: {}\n", plan.query_id));
370            report.push_str(&format!("Query: {}\n", plan.query_text));
371
372            let summary = plan.summary();
373            report.push_str(&format!(
374                "Steps: {} total, {} completed, {} failed\n",
375                summary.total_steps, summary.completed_steps, summary.failed_steps
376            ));
377            report.push_str(&format!(
378                "Duration: {:.2}ms\n",
379                summary.total_duration.as_secs_f64() * 1000.0
380            ));
381            report.push('\n');
382        }
383
384        report
385    }
386}
387
388#[cfg(test)]
389mod tests {
390    use super::*;
391
392    #[test]
393    fn test_execution_plan_creation() {
394        let plan = ExecutionPlan::new(
395            "query-1".to_string(),
396            "{ user { id name } }".to_string(),
397            HashMap::new(),
398        );
399
400        assert_eq!(plan.query_id, "query-1");
401        assert_eq!(plan.steps.len(), 0);
402    }
403
404    #[test]
405    fn test_add_step() {
406        let mut plan =
407            ExecutionPlan::new("query-1".to_string(), "query".to_string(), HashMap::new());
408
409        let step_id = plan.add_step(StepType::Parse);
410        assert_eq!(step_id, 0);
411        assert_eq!(plan.steps.len(), 1);
412        assert_eq!(plan.steps[0].status, StepStatus::Pending);
413    }
414
415    #[test]
416    fn test_start_complete_step() {
417        let mut plan =
418            ExecutionPlan::new("query-1".to_string(), "query".to_string(), HashMap::new());
419
420        let step_id = plan.add_step(StepType::Parse);
421        plan.start_step(step_id);
422        assert_eq!(plan.steps[step_id].status, StepStatus::Running);
423
424        plan.complete_step(step_id, Some("parsed".to_string()));
425        assert_eq!(plan.steps[step_id].status, StepStatus::Completed);
426        assert!(plan.steps[step_id].duration.is_some());
427    }
428
429    #[test]
430    fn test_fail_step() {
431        let mut plan =
432            ExecutionPlan::new("query-1".to_string(), "query".to_string(), HashMap::new());
433
434        let step_id = plan.add_step(StepType::Validate);
435        plan.start_step(step_id);
436        plan.fail_step(step_id, "Validation error".to_string());
437
438        assert_eq!(plan.steps[step_id].status, StepStatus::Failed);
439        assert_eq!(
440            plan.steps[step_id].error,
441            Some("Validation error".to_string())
442        );
443    }
444
445    #[test]
446    fn test_skip_step() {
447        let mut plan =
448            ExecutionPlan::new("query-1".to_string(), "query".to_string(), HashMap::new());
449
450        let step_id = plan.add_step(StepType::Optimize);
451        plan.skip_step(step_id, "Optimization disabled".to_string());
452
453        assert_eq!(plan.steps[step_id].status, StepStatus::Skipped);
454        assert_eq!(
455            plan.steps[step_id].metadata.get("skip_reason"),
456            Some(&"Optimization disabled".to_string())
457        );
458    }
459
460    #[test]
461    fn test_execution_summary() {
462        let mut plan =
463            ExecutionPlan::new("query-1".to_string(), "query".to_string(), HashMap::new());
464
465        let step1 = plan.add_step(StepType::Parse);
466        plan.start_step(step1);
467        plan.complete_step(step1, None);
468
469        let step2 = plan.add_step(StepType::Validate);
470        plan.start_step(step2);
471        plan.fail_step(step2, "Error".to_string());
472
473        let step3 = plan.add_step(StepType::Optimize);
474        plan.skip_step(step3, "Skipped".to_string());
475
476        let summary = plan.summary();
477        assert_eq!(summary.total_steps, 3);
478        assert_eq!(summary.completed_steps, 1);
479        assert_eq!(summary.failed_steps, 1);
480        assert_eq!(summary.skipped_steps, 1);
481    }
482
483    #[test]
484    fn test_visualize_tree() {
485        let mut plan = ExecutionPlan::new(
486            "query-1".to_string(),
487            "{ user { id } }".to_string(),
488            HashMap::new(),
489        );
490
491        plan.add_step(StepType::Parse);
492        plan.complete_step(0, None);
493
494        plan.add_step(StepType::Validate);
495        plan.complete_step(1, None);
496
497        let tree = plan.visualize_tree();
498        assert!(tree.contains("Query: query-1"));
499        assert!(tree.contains("Parse"));
500        assert!(tree.contains("Validate"));
501    }
502
503    #[test]
504    fn test_to_json() {
505        let mut plan =
506            ExecutionPlan::new("query-1".to_string(), "query".to_string(), HashMap::new());
507
508        plan.add_step(StepType::Parse);
509        plan.complete_step(0, None);
510
511        let json = plan.to_json();
512        assert!(json.contains("query_id"));
513        assert!(json.contains("steps"));
514        assert!(json.contains("summary"));
515    }
516
517    #[test]
518    fn test_query_debugger_start() {
519        let mut debugger = QueryDebugger::new(100);
520
521        let query_id =
522            debugger.start_query("query-1".to_string(), "query".to_string(), HashMap::new());
523
524        assert_eq!(query_id, "query-1");
525        assert_eq!(debugger.plan_count(), 1);
526    }
527
528    #[test]
529    fn test_query_debugger_add_step() {
530        let mut debugger = QueryDebugger::new(100);
531
532        debugger.start_query("query-1".to_string(), "query".to_string(), HashMap::new());
533        let step_id = debugger.add_step("query-1", StepType::Parse);
534
535        assert_eq!(step_id, Some(0));
536    }
537
538    #[test]
539    fn test_query_debugger_complete_flow() {
540        let mut debugger = QueryDebugger::new(100);
541
542        debugger.start_query("query-1".to_string(), "query".to_string(), HashMap::new());
543
544        let step_id = debugger
545            .add_step("query-1", StepType::Parse)
546            .expect("should succeed");
547        debugger.start_step("query-1", step_id);
548        debugger.complete_step("query-1", step_id, Some("parsed".to_string()));
549
550        debugger.finalize_query("query-1");
551
552        let plan = debugger.get_plan("query-1").expect("should succeed");
553        assert_eq!(plan.steps[0].status, StepStatus::Completed);
554        assert!(plan.total_duration.is_some());
555    }
556
557    #[test]
558    fn test_query_debugger_get_summary() {
559        let mut debugger = QueryDebugger::new(100);
560
561        debugger.start_query("query-1".to_string(), "query".to_string(), HashMap::new());
562
563        let step_id = debugger
564            .add_step("query-1", StepType::Parse)
565            .expect("should succeed");
566        debugger.complete_step("query-1", step_id, None);
567
568        let summary = debugger.get_summary("query-1").expect("should succeed");
569        assert_eq!(summary.completed_steps, 1);
570    }
571
572    #[test]
573    fn test_query_debugger_max_plans() {
574        let mut debugger = QueryDebugger::new(2);
575
576        debugger.start_query("query-1".to_string(), "query".to_string(), HashMap::new());
577        debugger.start_query("query-2".to_string(), "query".to_string(), HashMap::new());
578        debugger.start_query("query-3".to_string(), "query".to_string(), HashMap::new());
579
580        // Should only keep 2 plans
581        assert_eq!(debugger.plan_count(), 2);
582    }
583
584    #[test]
585    fn test_query_debugger_get_all_plans() {
586        let mut debugger = QueryDebugger::new(100);
587
588        debugger.start_query("query-1".to_string(), "query".to_string(), HashMap::new());
589        debugger.start_query("query-2".to_string(), "query".to_string(), HashMap::new());
590
591        let plans = debugger.get_all_plans();
592        assert_eq!(plans.len(), 2);
593    }
594
595    #[test]
596    fn test_query_debugger_clear() {
597        let mut debugger = QueryDebugger::new(100);
598
599        debugger.start_query("query-1".to_string(), "query".to_string(), HashMap::new());
600        assert_eq!(debugger.plan_count(), 1);
601
602        debugger.clear();
603        assert_eq!(debugger.plan_count(), 0);
604    }
605
606    #[test]
607    fn test_query_debugger_generate_report() {
608        let mut debugger = QueryDebugger::new(100);
609
610        debugger.start_query(
611            "query-1".to_string(),
612            "{ user { id } }".to_string(),
613            HashMap::new(),
614        );
615        debugger.add_step("query-1", StepType::Parse);
616
617        let report = debugger.generate_report();
618        assert!(report.contains("Debug Report"));
619        assert!(report.contains("query-1"));
620    }
621
622    #[test]
623    fn test_step_metadata() {
624        let mut plan =
625            ExecutionPlan::new("query-1".to_string(), "query".to_string(), HashMap::new());
626
627        let step_id = plan.add_step(StepType::ExecuteDataSource {
628            query: "SELECT * FROM users".to_string(),
629            source: "postgres".to_string(),
630        });
631
632        plan.set_step_metadata(step_id, "rows_returned".to_string(), "100".to_string());
633
634        assert_eq!(
635            plan.steps[step_id].metadata.get("rows_returned"),
636            Some(&"100".to_string())
637        );
638    }
639
640    #[test]
641    fn test_resolve_field_step() {
642        let mut plan =
643            ExecutionPlan::new("query-1".to_string(), "query".to_string(), HashMap::new());
644
645        let step_id = plan.add_step(StepType::ResolveField {
646            field_name: "user".to_string(),
647            parent_type: "Query".to_string(),
648        });
649
650        if let StepType::ResolveField { field_name, .. } = &plan.steps[step_id].step_type {
651            assert_eq!(field_name, "user");
652        } else {
653            panic!("Expected ResolveField step");
654        }
655    }
656
657    #[test]
658    fn test_finalize_sets_total_duration() {
659        let mut plan =
660            ExecutionPlan::new("query-1".to_string(), "query".to_string(), HashMap::new());
661
662        assert!(plan.total_duration.is_none());
663
664        plan.finalize();
665
666        assert!(plan.total_duration.is_some());
667    }
668
669    #[test]
670    fn test_multiple_steps_different_types() {
671        let mut plan =
672            ExecutionPlan::new("query-1".to_string(), "query".to_string(), HashMap::new());
673
674        plan.add_step(StepType::Parse);
675        plan.add_step(StepType::Validate);
676        plan.add_step(StepType::Optimize);
677        plan.add_step(StepType::Transform);
678        plan.add_step(StepType::Serialize);
679
680        assert_eq!(plan.steps.len(), 5);
681        assert!(matches!(plan.steps[0].step_type, StepType::Parse));
682        assert!(matches!(plan.steps[1].step_type, StepType::Validate));
683        assert!(matches!(plan.steps[4].step_type, StepType::Serialize));
684    }
685}