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 #[must_use]
68 pub fn from_sql(sql: &str) -> Self {
69 let trimmed = sql.trim_start().to_uppercase();
70
71 if trimmed.starts_with("SELECT") {
72 SqlOperation::Select
73 } else if trimmed.starts_with("INSERT") {
74 SqlOperation::Insert
75 } else if trimmed.starts_with("UPDATE") {
76 SqlOperation::Update
77 } else if trimmed.starts_with("DELETE") {
78 SqlOperation::Delete
79 } else {
80 SqlOperation::Other
81 }
82 }
83}
84
85impl std::fmt::Display for SqlOperation {
86 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
87 match self {
88 SqlOperation::Select => write!(f, "SELECT"),
89 SqlOperation::Insert => write!(f, "INSERT"),
90 SqlOperation::Update => write!(f, "UPDATE"),
91 SqlOperation::Delete => write!(f, "DELETE"),
92 SqlOperation::Other => write!(f, "OTHER"),
93 }
94 }
95}
96
97#[must_use = "call .finish_success() or .finish_error() to construct the final value"]
99pub struct SqlQueryLogBuilder {
100 query_id: String,
101 sql: String,
102 param_count: usize,
103 start: Instant,
104 slow_threshold_us: Option<u64>,
105}
106
107impl SqlQueryLogBuilder {
108 pub fn new(query_id: &str, sql: &str, param_count: usize) -> Self {
115 let truncated_sql = if sql.len() > 2000 {
116 format!("{}...", &sql[..2000])
117 } else {
118 sql.to_string()
119 };
120
121 Self {
122 query_id: query_id.to_string(),
123 sql: truncated_sql,
124 param_count,
125 start: Instant::now(),
126 slow_threshold_us: None,
127 }
128 }
129
130 pub const fn with_slow_threshold(mut self, threshold_us: u64) -> Self {
132 self.slow_threshold_us = Some(threshold_us);
133 self
134 }
135
136 pub fn finish_success(self, rows_affected: Option<usize>) -> SqlQueryLog {
138 let duration_us = u64::try_from(self.start.elapsed().as_micros()).unwrap_or(u64::MAX);
139 let was_slow = self.slow_threshold_us.is_some_and(|t| duration_us > t);
140
141 let log = SqlQueryLog {
142 query_id: self.query_id.clone(),
143 sql: self.sql.clone(),
144 param_count: self.param_count,
145 duration_us,
146 rows_affected,
147 success: true,
148 error: None,
149 operation: SqlOperation::from_sql(&self.sql),
150 slow_threshold_us: self.slow_threshold_us,
151 was_slow,
152 };
153
154 if was_slow {
155 warn!(
156 query_id = %log.query_id,
157 operation = %log.operation,
158 duration_us = log.duration_us,
159 threshold_us = self.slow_threshold_us.unwrap_or(0),
160 "Slow SQL query detected"
161 );
162 } else {
163 debug!(
164 query_id = %log.query_id,
165 operation = %log.operation,
166 duration_us = log.duration_us,
167 params = log.param_count,
168 "SQL query executed"
169 );
170 }
171
172 log
173 }
174
175 pub fn finish_error(self, error: &str) -> SqlQueryLog {
177 let duration_us = u64::try_from(self.start.elapsed().as_micros()).unwrap_or(u64::MAX);
178
179 let log = SqlQueryLog {
180 query_id: self.query_id.clone(),
181 sql: self.sql.clone(),
182 param_count: self.param_count,
183 duration_us,
184 rows_affected: None,
185 success: false,
186 error: Some(error.to_string()),
187 operation: SqlOperation::from_sql(&self.sql),
188 slow_threshold_us: self.slow_threshold_us,
189 was_slow: false,
190 };
191
192 warn!(
193 query_id = %log.query_id,
194 operation = %log.operation,
195 duration_us = log.duration_us,
196 error = error,
197 "SQL query failed"
198 );
199
200 log
201 }
202}
203
204impl SqlQueryLog {
205 #[must_use]
207 pub fn to_log_string(&self) -> String {
208 if self.success {
209 format!(
210 "SQL {} query (query_id={}, duration_us={}, params={}, rows={:?})",
211 self.operation,
212 self.query_id,
213 self.duration_us,
214 self.param_count,
215 self.rows_affected
216 )
217 } else {
218 format!(
219 "SQL {} query FAILED (query_id={}, duration_us={}, error={})",
220 self.operation,
221 self.query_id,
222 self.duration_us,
223 self.error.as_ref().unwrap_or(&"Unknown".to_string())
224 )
225 }
226 }
227
228 #[must_use]
230 pub const fn is_slow(&self) -> bool {
231 self.was_slow
232 }
233
234 #[must_use]
236 pub fn duration_ms(&self) -> f64 {
237 #[allow(clippy::cast_precision_loss)]
238 {
241 self.duration_us as f64 / 1000.0
242 }
243 }
244}
245
246pub fn create_sql_span(query_id: &str, operation: SqlOperation) -> tracing::Span {
248 span!(
249 Level::DEBUG,
250 "sql_query",
251 query_id = query_id,
252 operation = %operation,
253 )
254}