Skip to main content

fraiseql_core/runtime/
query_tracing.rs

1//! Query execution tracing and observability instrumentation.
2//!
3//! Provides structured tracing for query compilation, validation, and execution phases.
4//! Integrates with the tracing crate for OpenTelemetry-compatible span collection.
5
6use std::time::Instant;
7
8use serde::{Deserialize, Serialize};
9use tracing::{Level, debug, span, warn};
10
11use crate::error::Result;
12
13/// Trace span for a query compilation phase.
14#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct QueryPhaseSpan {
16    /// Name of the phase (e.g., "parse", "validate", "execute").
17    pub phase: String,
18
19    /// Duration in microseconds.
20    pub duration_us: u64,
21
22    /// Whether the phase succeeded.
23    pub success: bool,
24
25    /// Optional error message if phase failed.
26    pub error: Option<String>,
27}
28
29/// Complete trace for a query execution.
30///
31/// Tracks all phases from initial parse through execution,
32/// enabling performance analysis and debugging.
33#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct QueryExecutionTrace {
35    /// Unique query ID for tracing correlation.
36    pub query_id: String,
37
38    /// Query string (truncated for large queries).
39    pub query: String,
40
41    /// List of phase spans (parse, validate, execute, etc.).
42    pub phases: Vec<QueryPhaseSpan>,
43
44    /// Total execution time in microseconds.
45    pub total_duration_us: u64,
46
47    /// Whether execution succeeded.
48    pub success: bool,
49
50    /// Optional error message.
51    pub error: Option<String>,
52
53    /// Number of results returned.
54    pub result_count: Option<usize>,
55}
56
57/// Builder for creating and tracking query execution traces.
58///
59/// # Example
60///
61/// ```rust
62/// use fraiseql_core::runtime::query_tracing::QueryTraceBuilder;
63///
64/// let mut builder = QueryTraceBuilder::new("query_123", "{ user { id name } }");
65///
66/// // Record parse phase (2.5ms = 2500 microseconds)
67/// builder.record_phase_success("parse", 2500);
68///
69/// // Record validate phase (3ms = 3000 microseconds)
70/// builder.record_phase_success("validate", 3000);
71///
72/// // Record execute phase (7ms = 7000 microseconds)
73/// builder.record_phase_success("execute", 7000);
74///
75/// // Finalize trace with result count
76/// let trace = builder.finish(true, None, Some(42))?;
77/// assert_eq!(trace.success, true);
78/// assert_eq!(trace.result_count, Some(42));
79/// println!("Query took {} microseconds", trace.total_duration_us);
80/// # Ok::<(), Box<dyn std::error::Error>>(())
81/// ```
82#[must_use = "call .finish() to construct the final value"]
83pub struct QueryTraceBuilder {
84    pub(crate) query_id: String,
85    pub(crate) query:    String,
86    pub(crate) phases:   Vec<QueryPhaseSpan>,
87    start:               Instant,
88}
89
90impl QueryTraceBuilder {
91    /// Create new query trace builder.
92    ///
93    /// # Arguments
94    ///
95    /// * `query_id` - Unique ID for query correlation
96    /// * `query` - Query string (will be truncated if very long)
97    pub fn new(query_id: &str, query: &str) -> Self {
98        let query_str = crate::utils::text::truncate_for_display(query, 500);
99
100        Self {
101            query_id: query_id.to_string(),
102            query:    query_str,
103            phases:   Vec::new(),
104            start:    Instant::now(),
105        }
106    }
107
108    /// Record a phase that completed successfully.
109    ///
110    /// # Arguments
111    ///
112    /// * `phase_name` - Name of phase (e.g., "parse", "validate")
113    /// * `duration_us` - Duration in microseconds
114    pub fn record_phase_success(&mut self, phase_name: &str, duration_us: u64) {
115        self.phases.push(QueryPhaseSpan {
116            phase: phase_name.to_string(),
117            duration_us,
118            success: true,
119            error: None,
120        });
121
122        debug!(phase = phase_name, duration_us = duration_us, "Query phase completed");
123    }
124
125    /// Record a phase that failed.
126    ///
127    /// # Arguments
128    ///
129    /// * `phase_name` - Name of phase (e.g., "parse", "validate")
130    /// * `duration_us` - Duration in microseconds
131    /// * `error` - Error message
132    pub fn record_phase_error(&mut self, phase_name: &str, duration_us: u64, error: &str) {
133        self.phases.push(QueryPhaseSpan {
134            phase: phase_name.to_string(),
135            duration_us,
136            success: false,
137            error: Some(error.to_string()),
138        });
139
140        warn!(
141            phase = phase_name,
142            duration_us = duration_us,
143            error = error,
144            "Query phase failed"
145        );
146    }
147
148    /// Finish tracing and create final trace record.
149    ///
150    /// # Arguments
151    ///
152    /// * `success` - Whether query executed successfully
153    /// * `error` - Optional error message
154    /// * `result_count` - Number of results returned (if applicable)
155    ///
156    /// # Errors
157    ///
158    /// Currently infallible; reserved for future extension (e.g., sink flush failures).
159    ///
160    /// # Returns
161    ///
162    /// Complete query execution trace
163    pub fn finish(
164        self,
165        success: bool,
166        error: Option<&str>,
167        result_count: Option<usize>,
168    ) -> Result<QueryExecutionTrace> {
169        let total_duration_us = u64::try_from(self.start.elapsed().as_micros()).unwrap_or(u64::MAX);
170
171        Ok(QueryExecutionTrace {
172            query_id: self.query_id.clone(),
173            query: self.query,
174            phases: self.phases,
175            total_duration_us,
176            success,
177            error: error.map(|e| e.to_string()),
178            result_count,
179        })
180    }
181
182    /// Get query ID for logging/correlation.
183    #[must_use]
184    pub fn query_id(&self) -> &str {
185        &self.query_id
186    }
187
188    /// Get current elapsed time in microseconds.
189    #[must_use]
190    pub fn elapsed_us(&self) -> u64 {
191        u64::try_from(self.start.elapsed().as_micros()).unwrap_or(u64::MAX)
192    }
193}
194
195impl QueryExecutionTrace {
196    /// Get average phase duration in microseconds.
197    #[must_use]
198    pub fn average_phase_duration_us(&self) -> u64 {
199        if self.phases.is_empty() {
200            0
201        } else {
202            self.phases.iter().map(|p| p.duration_us).sum::<u64>() / self.phases.len() as u64
203        }
204    }
205
206    /// Get slowest phase.
207    #[must_use]
208    pub fn slowest_phase(&self) -> Option<&QueryPhaseSpan> {
209        self.phases.iter().max_by_key(|p| p.duration_us)
210    }
211
212    /// Get trace as log-friendly string.
213    ///
214    /// Suitable for structured logging or monitoring dashboards.
215    #[must_use]
216    pub fn to_log_string(&self) -> String {
217        let status = if self.success { "success" } else { "error" };
218        let phases_str = self
219            .phases
220            .iter()
221            .map(|p| format!("{}={}us", p.phase, p.duration_us))
222            .collect::<Vec<_>>()
223            .join(" ");
224
225        let error_str = self.error.as_ref().map(|e| format!(" error={}", e)).unwrap_or_default();
226
227        format!(
228            "query_id={} status={} total={}us phases=[{}]{}",
229            self.query_id, status, self.total_duration_us, phases_str, error_str
230        )
231    }
232}
233
234/// Create a tracing span for query execution.
235///
236/// Automatically enters a span and returns it for use with `.in_scope()`.
237///
238/// # Example
239///
240/// ```rust,no_run
241/// use fraiseql_core::runtime::query_tracing::create_query_span;
242///
243/// # fn example() {
244/// let span = create_query_span("query_123", "{ user { id } }");
245/// let _enter = span.enter();
246/// // Tracing will be recorded within this scope
247/// # }
248/// ```
249pub fn create_query_span(query_id: &str, query_text: &str) -> tracing::Span {
250    span!(
251        Level::DEBUG,
252        "query_execute",
253        query_id = query_id,
254        query = truncate_query(query_text, 100),
255    )
256}
257
258/// Create a tracing span for a specific phase.
259///
260/// # Arguments
261///
262/// * `phase_name` - Name of phase (e.g., "parse", "validate", "execute")
263/// * `query_id` - Query ID for correlation
264pub fn create_phase_span(phase_name: &str, query_id: &str) -> tracing::Span {
265    span!(Level::DEBUG, "query_phase", phase = phase_name, query_id = query_id)
266}
267
268/// Truncate query string to at most `max_len` bytes for logging.
269///
270/// Char-boundary-safe wrapper over [`crate::utils::text::truncate_for_display`].
271pub(crate) fn truncate_query(query: &str, max_len: usize) -> String {
272    crate::utils::text::truncate_for_display(query, max_len)
273}