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 = 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 /// Record a phase that completed successfully.
113 ///
114 /// # Arguments
115 ///
116 /// * `phase_name` - Name of phase (e.g., "parse", "validate")
117 /// * `duration_us` - Duration in microseconds
118 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 /// Record a phase that failed.
130 ///
131 /// # Arguments
132 ///
133 /// * `phase_name` - Name of phase (e.g., "parse", "validate")
134 /// * `duration_us` - Duration in microseconds
135 /// * `error` - Error message
136 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 /// Finish tracing and create final trace record.
153 ///
154 /// # Arguments
155 ///
156 /// * `success` - Whether query executed successfully
157 /// * `error` - Optional error message
158 /// * `result_count` - Number of results returned (if applicable)
159 ///
160 /// # Errors
161 ///
162 /// Currently infallible; reserved for future extension (e.g., sink flush failures).
163 ///
164 /// # Returns
165 ///
166 /// Complete query execution trace
167 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 /// Get query ID for logging/correlation.
187 #[must_use]
188 pub fn query_id(&self) -> &str {
189 &self.query_id
190 }
191
192 /// Get current elapsed time in microseconds.
193 #[must_use]
194 pub fn elapsed_us(&self) -> u64 {
195 u64::try_from(self.start.elapsed().as_micros()).unwrap_or(u64::MAX)
196 }
197}
198
199impl QueryExecutionTrace {
200 /// Get average phase duration in microseconds.
201 #[must_use]
202 pub fn average_phase_duration_us(&self) -> u64 {
203 if self.phases.is_empty() {
204 0
205 } else {
206 self.phases.iter().map(|p| p.duration_us).sum::<u64>() / self.phases.len() as u64
207 }
208 }
209
210 /// Get slowest phase.
211 #[must_use]
212 pub fn slowest_phase(&self) -> Option<&QueryPhaseSpan> {
213 self.phases.iter().max_by_key(|p| p.duration_us)
214 }
215
216 /// Get trace as log-friendly string.
217 ///
218 /// Suitable for structured logging or monitoring dashboards.
219 #[must_use]
220 pub fn to_log_string(&self) -> String {
221 let status = if self.success { "success" } else { "error" };
222 let phases_str = self
223 .phases
224 .iter()
225 .map(|p| format!("{}={}us", p.phase, p.duration_us))
226 .collect::<Vec<_>>()
227 .join(" ");
228
229 let error_str = self.error.as_ref().map(|e| format!(" error={}", e)).unwrap_or_default();
230
231 format!(
232 "query_id={} status={} total={}us phases=[{}]{}",
233 self.query_id, status, self.total_duration_us, phases_str, error_str
234 )
235 }
236}
237
238/// Create a tracing span for query execution.
239///
240/// Automatically enters a span and returns it for use with `.in_scope()`.
241///
242/// # Example
243///
244/// ```rust,no_run
245/// use fraiseql_core::runtime::query_tracing::create_query_span;
246///
247/// # fn example() {
248/// let span = create_query_span("query_123", "{ user { id } }");
249/// let _enter = span.enter();
250/// // Tracing will be recorded within this scope
251/// # }
252/// ```
253pub fn create_query_span(query_id: &str, query_text: &str) -> tracing::Span {
254 span!(
255 Level::DEBUG,
256 "query_execute",
257 query_id = query_id,
258 query = truncate_query(query_text, 100),
259 )
260}
261
262/// Create a tracing span for a specific phase.
263///
264/// # Arguments
265///
266/// * `phase_name` - Name of phase (e.g., "parse", "validate", "execute")
267/// * `query_id` - Query ID for correlation
268pub fn create_phase_span(phase_name: &str, query_id: &str) -> tracing::Span {
269 span!(Level::DEBUG, "query_phase", phase = phase_name, query_id = query_id)
270}
271
272/// Truncate query string to specified length.
273///
274/// Useful for logging to avoid truncating long queries.
275pub(crate) fn truncate_query(query: &str, max_len: usize) -> String {
276 if query.len() > max_len {
277 format!("{}...", &query[..max_len])
278 } else {
279 query.to_string()
280 }
281}