1use std::time::Instant;
11
12use serde::{Deserialize, Serialize};
13use tracing::{Level, debug, span, warn};
14
15#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct SqlQueryLog {
18 pub query_id: String,
20
21 pub sql: String,
23
24 pub param_count: usize,
26
27 pub duration_us: u64,
29
30 pub rows_affected: Option<usize>,
32
33 pub success: bool,
35
36 pub error: Option<String>,
38
39 pub operation: SqlOperation,
41
42 pub slow_threshold_us: Option<u64>,
44
45 pub was_slow: bool,
47}
48
49#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
51#[non_exhaustive]
52pub enum SqlOperation {
53 Select,
55 Insert,
57 Update,
59 Delete,
61 Other,
63}
64
65impl SqlOperation {
66 pub fn from_sql(sql: &str) -> Self {
68 let trimmed = sql.trim_start().to_uppercase();
69
70 if trimmed.starts_with("SELECT") {
71 SqlOperation::Select
72 } else if trimmed.starts_with("INSERT") {
73 SqlOperation::Insert
74 } else if trimmed.starts_with("UPDATE") {
75 SqlOperation::Update
76 } else if trimmed.starts_with("DELETE") {
77 SqlOperation::Delete
78 } else {
79 SqlOperation::Other
80 }
81 }
82}
83
84impl std::fmt::Display for SqlOperation {
85 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
86 match self {
87 SqlOperation::Select => write!(f, "SELECT"),
88 SqlOperation::Insert => write!(f, "INSERT"),
89 SqlOperation::Update => write!(f, "UPDATE"),
90 SqlOperation::Delete => write!(f, "DELETE"),
91 SqlOperation::Other => write!(f, "OTHER"),
92 }
93 }
94}
95
96#[must_use = "call .finish_success() or .finish_error() to construct the final value"]
98pub struct SqlQueryLogBuilder {
99 query_id: String,
100 sql: String,
101 param_count: usize,
102 start: Instant,
103 slow_threshold_us: Option<u64>,
104}
105
106impl SqlQueryLogBuilder {
107 pub fn new(query_id: &str, sql: &str, param_count: usize) -> Self {
114 let truncated_sql = if sql.len() > 2000 {
115 format!("{}...", &sql[..2000])
116 } else {
117 sql.to_string()
118 };
119
120 Self {
121 query_id: query_id.to_string(),
122 sql: truncated_sql,
123 param_count,
124 start: Instant::now(),
125 slow_threshold_us: None,
126 }
127 }
128
129 pub const fn with_slow_threshold(mut self, threshold_us: u64) -> Self {
131 self.slow_threshold_us = Some(threshold_us);
132 self
133 }
134
135 pub fn finish_success(self, rows_affected: Option<usize>) -> SqlQueryLog {
137 let duration_us = u64::try_from(self.start.elapsed().as_micros()).unwrap_or(u64::MAX);
138 let was_slow = self.slow_threshold_us.is_some_and(|t| duration_us > t);
139
140 let log = SqlQueryLog {
141 query_id: self.query_id.clone(),
142 sql: self.sql.clone(),
143 param_count: self.param_count,
144 duration_us,
145 rows_affected,
146 success: true,
147 error: None,
148 operation: SqlOperation::from_sql(&self.sql),
149 slow_threshold_us: self.slow_threshold_us,
150 was_slow,
151 };
152
153 if was_slow {
154 warn!(
155 query_id = %log.query_id,
156 operation = %log.operation,
157 duration_us = log.duration_us,
158 threshold_us = self.slow_threshold_us.unwrap_or(0),
159 "Slow SQL query detected"
160 );
161 } else {
162 debug!(
163 query_id = %log.query_id,
164 operation = %log.operation,
165 duration_us = log.duration_us,
166 params = log.param_count,
167 "SQL query executed"
168 );
169 }
170
171 log
172 }
173
174 pub fn finish_error(self, error: &str) -> SqlQueryLog {
176 let duration_us = u64::try_from(self.start.elapsed().as_micros()).unwrap_or(u64::MAX);
177
178 let log = SqlQueryLog {
179 query_id: self.query_id.clone(),
180 sql: self.sql.clone(),
181 param_count: self.param_count,
182 duration_us,
183 rows_affected: None,
184 success: false,
185 error: Some(error.to_string()),
186 operation: SqlOperation::from_sql(&self.sql),
187 slow_threshold_us: self.slow_threshold_us,
188 was_slow: false,
189 };
190
191 warn!(
192 query_id = %log.query_id,
193 operation = %log.operation,
194 duration_us = log.duration_us,
195 error = error,
196 "SQL query failed"
197 );
198
199 log
200 }
201}
202
203impl SqlQueryLog {
204 pub fn to_log_string(&self) -> String {
206 if self.success {
207 format!(
208 "SQL {} query (query_id={}, duration_us={}, params={}, rows={:?})",
209 self.operation,
210 self.query_id,
211 self.duration_us,
212 self.param_count,
213 self.rows_affected
214 )
215 } else {
216 format!(
217 "SQL {} query FAILED (query_id={}, duration_us={}, error={})",
218 self.operation,
219 self.query_id,
220 self.duration_us,
221 self.error.as_ref().unwrap_or(&"Unknown".to_string())
222 )
223 }
224 }
225
226 pub const fn is_slow(&self) -> bool {
228 self.was_slow
229 }
230
231 pub fn duration_ms(&self) -> f64 {
233 #[allow(clippy::cast_precision_loss)]
234 {
237 self.duration_us as f64 / 1000.0
238 }
239 }
240}
241
242pub fn create_sql_span(query_id: &str, operation: SqlOperation) -> tracing::Span {
244 span!(
245 Level::DEBUG,
246 "sql_query",
247 query_id = query_id,
248 operation = %operation,
249 )
250}
251
252#[cfg(test)]
253mod tests {
254 use std::{thread, time::Duration};
255
256 use super::*;
257
258 #[test]
259 fn test_sql_operation_detection() {
260 assert_eq!(SqlOperation::from_sql("SELECT * FROM users"), SqlOperation::Select);
261 assert_eq!(SqlOperation::from_sql(" select id from users"), SqlOperation::Select);
262 assert_eq!(SqlOperation::from_sql("INSERT INTO users VALUES (1)"), SqlOperation::Insert);
263 assert_eq!(SqlOperation::from_sql("UPDATE users SET id=1"), SqlOperation::Update);
264 assert_eq!(SqlOperation::from_sql("DELETE FROM users"), SqlOperation::Delete);
265 assert_eq!(SqlOperation::from_sql("CREATE TABLE users (id INT)"), SqlOperation::Other);
266 }
267
268 #[test]
269 fn test_sql_operation_display() {
270 assert_eq!(SqlOperation::Select.to_string(), "SELECT");
271 assert_eq!(SqlOperation::Insert.to_string(), "INSERT");
272 assert_eq!(SqlOperation::Update.to_string(), "UPDATE");
273 assert_eq!(SqlOperation::Delete.to_string(), "DELETE");
274 assert_eq!(SqlOperation::Other.to_string(), "OTHER");
275 }
276
277 #[test]
278 fn test_builder_success() {
279 let builder = SqlQueryLogBuilder::new("query_1", "SELECT * FROM users", 0);
280 let log = builder.finish_success(Some(10));
281
282 assert!(log.success);
283 assert_eq!(log.query_id, "query_1");
284 assert_eq!(log.operation, SqlOperation::Select);
285 assert_eq!(log.rows_affected, Some(10));
286 assert!(log.error.is_none());
287 }
289
290 #[test]
291 fn test_builder_error() {
292 let builder = SqlQueryLogBuilder::new("query_1", "SELECT * FROM nonexistent", 0);
293 let log = builder.finish_error("Table not found");
294
295 assert!(!log.success);
296 assert_eq!(log.error, Some("Table not found".to_string()));
297 assert!(log.rows_affected.is_none());
298 }
299
300 #[test]
301 fn test_query_truncation() {
302 let long_query = "a".repeat(3000);
303 let builder = SqlQueryLogBuilder::new("query_1", &long_query, 0);
304 let log = builder.finish_success(None);
305
306 assert!(log.sql.len() < 3000);
307 assert!(log.sql.ends_with("..."));
308 }
309
310 #[test]
311 fn test_slow_query_detection() {
312 let builder =
313 SqlQueryLogBuilder::new("query_1", "SELECT * FROM users", 0).with_slow_threshold(100);
314
315 let log = builder.finish_success(Some(5));
316
317 assert!(!log.was_slow);
319 }
320
321 #[test]
322 fn test_slow_query_warning() {
323 let builder =
324 SqlQueryLogBuilder::new("query_1", "SELECT * FROM users", 0).with_slow_threshold(1);
325
326 thread::sleep(Duration::from_micros(100));
328 let log = builder.finish_success(Some(5));
329
330 assert!(log.was_slow);
331 }
332
333 #[test]
334 fn test_log_string_success() {
335 let builder = SqlQueryLogBuilder::new("query_1", "SELECT * FROM users", 5);
336 let log = builder.finish_success(Some(10));
337
338 let log_str = log.to_log_string();
339 assert!(log_str.contains("SELECT"));
340 assert!(log_str.contains("query_1"));
341 assert!(log_str.contains("params=5"));
342 assert!(log_str.contains("rows=Some(10)"));
343 }
344
345 #[test]
346 fn test_log_string_error() {
347 let builder = SqlQueryLogBuilder::new("query_1", "SELECT * FROM users", 0);
348 let log = builder.finish_error("Connection timeout");
349
350 let log_str = log.to_log_string();
351 assert!(log_str.contains("FAILED"));
352 assert!(log_str.contains("Connection timeout"));
353 }
354
355 #[test]
356 fn test_duration_ms() {
357 let builder = SqlQueryLogBuilder::new("query_1", "SELECT * FROM users", 0);
358 thread::sleep(Duration::from_millis(10));
359 let log = builder.finish_success(None);
360
361 let ms = log.duration_ms();
362 assert!(ms >= 10.0);
363 }
364
365 #[test]
366 fn test_serialization() {
367 let builder = SqlQueryLogBuilder::new("query_1", "SELECT * FROM users", 3);
368 let log = builder.finish_success(Some(25));
369
370 let json = serde_json::to_string(&log).expect("serialize should work");
371 let restored: SqlQueryLog = serde_json::from_str(&json).expect("deserialize should work");
372
373 assert_eq!(restored.query_id, log.query_id);
374 assert_eq!(restored.operation, log.operation);
375 assert_eq!(restored.rows_affected, log.rows_affected);
376 }
377
378 #[test]
379 fn test_all_operations() {
380 let operations = vec![
381 SqlOperation::Select,
382 SqlOperation::Insert,
383 SqlOperation::Update,
384 SqlOperation::Delete,
385 SqlOperation::Other,
386 ];
387
388 for op in operations {
389 let builder = SqlQueryLogBuilder::new("query_1", "SELECT 1", 0);
390 let mut log = builder.finish_success(None);
391 log.operation = op;
392
393 assert_eq!(log.operation, op);
394 let log_str = log.to_log_string();
395 assert!(log_str.contains(&op.to_string()));
396 }
397 }
398
399 #[test]
400 fn test_param_count() {
401 let builder =
402 SqlQueryLogBuilder::new("query_1", "SELECT * FROM users WHERE id = ? AND name = ?", 2);
403 let log = builder.finish_success(Some(1));
404
405 assert_eq!(log.param_count, 2);
406 assert!(log.to_log_string().contains("params=2"));
407 }
408}