1use crate::driver::{ExecResponse, Rows};
15use crate::stmt::Value;
16
17use std::fmt::Write;
18use std::time::{Duration, Instant};
19
20pub const TARGET: &str = "toasty::query";
22
23const MAX_PARAMS: usize = 32;
25
26const MAX_STR_LEN: usize = 128;
28
29const MAX_LIST_ITEMS: usize = 8;
31
32#[derive(Debug, Clone, Copy)]
38pub struct QueryLogConfig {
39 pub params: bool,
42
43 pub slow_statement_threshold: Option<Duration>,
46}
47
48impl Default for QueryLogConfig {
49 fn default() -> Self {
50 Self {
51 params: false,
52 slow_statement_threshold: Some(Duration::from_secs(1)),
53 }
54 }
55}
56
57#[derive(Debug)]
64pub struct QueryLog<'a> {
65 config: QueryLogConfig,
66 system: &'static str,
67 query_text: Option<&'a str>,
68 operation: Option<&'a str>,
69 collection: Option<&'a str>,
70 params: Option<String>,
71 rows: Option<u64>,
72 start: Instant,
73}
74
75impl<'a> QueryLog<'a> {
76 pub fn sql<'v>(
78 config: &QueryLogConfig,
79 system: &'static str,
80 sql: &'a str,
81 params: impl IntoIterator<Item = &'v Value>,
82 ) -> Self {
83 Self {
84 config: *config,
85 system,
86 query_text: Some(sql),
87 operation: None,
88 collection: None,
89 params: render_params(config, params),
90 rows: None,
91 start: Instant::now(),
92 }
93 }
94
95 pub fn operation(
97 config: &QueryLogConfig,
98 system: &'static str,
99 operation: &'a str,
100 collection: Option<&'a str>,
101 ) -> Self {
102 Self {
103 config: *config,
104 system,
105 query_text: None,
106 operation: Some(operation),
107 collection,
108 params: None,
109 rows: None,
110 start: Instant::now(),
111 }
112 }
113
114 pub fn rows(&mut self, rows: u64) {
118 self.rows = Some(rows);
119 }
120
121 pub fn finish(self, result: &crate::Result<ExecResponse>) {
123 let elapsed = self.start.elapsed();
124 let duration_ms = elapsed.as_secs_f64() * 1e3;
125 let rows = self.rows.or(match result {
126 Ok(response) => match &response.values {
127 Rows::Count(count) => Some(*count),
128 _ => None,
129 },
130 Err(_) => None,
131 });
132 let slow = self
133 .config
134 .slow_statement_threshold
135 .is_some_and(|threshold| elapsed >= threshold);
136
137 let error = result.as_ref().err().map(tracing::field::display);
138 let message = match (slow, result) {
139 (_, Err(_)) => "query failed",
140 (true, Ok(_)) => "slow query",
141 (false, Ok(_)) => "query executed",
142 };
143
144 let system = tracing::field::display(self.system);
148 let statement = self.query_text.map(tracing::field::display);
149 let operation = self.operation.map(tracing::field::display);
150 let collection = self.collection.map(tracing::field::display);
151 let params = self.params.as_deref().map(tracing::field::display);
152
153 if slow {
156 tracing::warn!(
157 target: "toasty::query",
158 duration_ms,
159 rows,
160 error,
161 db.system = system,
162 db.statement = statement,
163 db.operation = operation,
164 db.collection = collection,
165 db.params = params,
166 "{message}"
167 );
168 } else {
169 tracing::debug!(
170 target: "toasty::query",
171 duration_ms,
172 rows,
173 error,
174 db.system = system,
175 db.statement = statement,
176 db.operation = operation,
177 db.collection = collection,
178 db.params = params,
179 "{message}"
180 );
181 }
182 }
183}
184
185fn render_params<'v>(
192 config: &QueryLogConfig,
193 params: impl IntoIterator<Item = &'v Value>,
194) -> Option<String> {
195 let enabled = tracing::event_enabled!(target: "toasty::query", tracing::Level::DEBUG)
196 || tracing::event_enabled!(target: "toasty::query", tracing::Level::WARN);
197 if !config.params || !enabled {
198 return None;
199 }
200
201 let mut out = String::from("[");
202 for (i, value) in params.into_iter().enumerate() {
203 if i > 0 {
204 out.push_str(", ");
205 }
206 if i == MAX_PARAMS {
207 out.push_str("...");
208 break;
209 }
210 render_value(&mut out, value);
211 }
212 out.push(']');
213 Some(out)
214}
215
216fn render_value(out: &mut String, value: &Value) {
217 let _ = match value {
218 Value::Null => {
219 out.push_str("NULL");
220 Ok(())
221 }
222 Value::Bool(v) => write!(out, "{v}"),
223 Value::I8(v) => write!(out, "{v}"),
224 Value::I16(v) => write!(out, "{v}"),
225 Value::I32(v) => write!(out, "{v}"),
226 Value::I64(v) => write!(out, "{v}"),
227 Value::U8(v) => write!(out, "{v}"),
228 Value::U16(v) => write!(out, "{v}"),
229 Value::U32(v) => write!(out, "{v}"),
230 Value::U64(v) => write!(out, "{v}"),
231 Value::F32(v) => write!(out, "{v}"),
232 Value::F64(v) => write!(out, "{v}"),
233 Value::Uuid(v) => write!(out, "{v}"),
234 Value::String(s) => {
235 render_str(out, s);
236 Ok(())
237 }
238 Value::Bytes(b) => write!(out, "<{} bytes>", b.len()),
239 Value::List(items) => {
240 out.push('[');
241 for (i, item) in items.iter().enumerate() {
242 if i > 0 {
243 out.push_str(", ");
244 }
245 if i == MAX_LIST_ITEMS {
246 out.push_str("...");
247 break;
248 }
249 render_value(out, item);
250 }
251 out.push(']');
252 Ok(())
253 }
254 other => write!(out, "{other:?}"),
255 };
256}
257
258fn render_str(out: &mut String, s: &str) {
259 let total = s.chars().count();
260 if total <= MAX_STR_LEN {
261 let _ = write!(out, "{s:?}");
262 } else {
263 let prefix: String = s.chars().take(MAX_STR_LEN).collect();
264 let _ = write!(out, "{prefix:?}...({total} chars)");
265 }
266}
267
268#[cfg(test)]
269mod tests {
270 use super::*;
271
272 fn rendered(value: Value) -> String {
273 let mut out = String::new();
274 render_value(&mut out, &value);
275 out
276 }
277
278 #[test]
279 fn renders_scalars_bare() {
280 assert_eq!(rendered(Value::Null), "NULL");
281 assert_eq!(rendered(Value::Bool(true)), "true");
282 assert_eq!(rendered(Value::I64(-42)), "-42");
283 assert_eq!(rendered(Value::F64(1.5)), "1.5");
284 }
285
286 #[test]
287 fn renders_strings_quoted_and_truncated() {
288 assert_eq!(rendered(Value::String("hi".into())), "\"hi\"");
289
290 let long = "x".repeat(500);
291 let out = rendered(Value::String(long));
292 assert!(out.starts_with('"'));
293 assert!(out.ends_with("...(500 chars)"));
294 assert!(out.len() < 200);
295 }
296
297 #[test]
298 fn renders_bytes_as_length() {
299 assert_eq!(rendered(Value::Bytes(vec![0; 16])), "<16 bytes>");
300 }
301
302 #[test]
303 fn caps_list_items() {
304 let list = Value::List((0..20).map(Value::I64).collect());
305 assert_eq!(rendered(list), "[0, 1, 2, 3, 4, 5, 6, 7, ...]");
306 }
307}