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 = crate::utils::text::truncate_for_display(sql, 2000);
118
119 Self {
120 query_id: query_id.to_string(),
121 sql: truncated_sql,
122 param_count,
123 start: Instant::now(),
124 slow_threshold_us: None,
125 }
126 }
127
128 pub const fn with_slow_threshold(mut self, threshold_us: u64) -> Self {
130 self.slow_threshold_us = Some(threshold_us);
131 self
132 }
133
134 pub fn finish_success(self, rows_affected: Option<usize>) -> SqlQueryLog {
136 let duration_us = u64::try_from(self.start.elapsed().as_micros()).unwrap_or(u64::MAX);
137 let was_slow = self.slow_threshold_us.is_some_and(|t| duration_us > t);
138
139 let log = SqlQueryLog {
140 query_id: self.query_id.clone(),
141 sql: self.sql.clone(),
142 param_count: self.param_count,
143 duration_us,
144 rows_affected,
145 success: true,
146 error: None,
147 operation: SqlOperation::from_sql(&self.sql),
148 slow_threshold_us: self.slow_threshold_us,
149 was_slow,
150 };
151
152 if was_slow {
153 warn!(
154 query_id = %log.query_id,
155 operation = %log.operation,
156 duration_us = log.duration_us,
157 threshold_us = self.slow_threshold_us.unwrap_or(0),
158 "Slow SQL query detected"
159 );
160 } else {
161 debug!(
162 query_id = %log.query_id,
163 operation = %log.operation,
164 duration_us = log.duration_us,
165 params = log.param_count,
166 "SQL query executed"
167 );
168 }
169
170 log
171 }
172
173 pub fn finish_error(self, error: &str) -> SqlQueryLog {
175 let duration_us = u64::try_from(self.start.elapsed().as_micros()).unwrap_or(u64::MAX);
176
177 let log = SqlQueryLog {
178 query_id: self.query_id.clone(),
179 sql: self.sql.clone(),
180 param_count: self.param_count,
181 duration_us,
182 rows_affected: None,
183 success: false,
184 error: Some(error.to_string()),
185 operation: SqlOperation::from_sql(&self.sql),
186 slow_threshold_us: self.slow_threshold_us,
187 was_slow: false,
188 };
189
190 warn!(
191 query_id = %log.query_id,
192 operation = %log.operation,
193 duration_us = log.duration_us,
194 error = error,
195 "SQL query failed"
196 );
197
198 log
199 }
200}
201
202impl SqlQueryLog {
203 #[must_use]
205 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 #[must_use]
228 pub const fn is_slow(&self) -> bool {
229 self.was_slow
230 }
231
232 #[must_use]
234 pub fn duration_ms(&self) -> f64 {
235 #[allow(clippy::cast_precision_loss)]
236 {
239 self.duration_us as f64 / 1000.0
240 }
241 }
242}
243
244pub fn create_sql_span(query_id: &str, operation: SqlOperation) -> tracing::Span {
246 span!(
247 Level::DEBUG,
248 "sql_query",
249 query_id = query_id,
250 operation = %operation,
251 )
252}