1use std::sync::{Arc, Mutex};
2use std::time::{Duration, SystemTime};
3
4use super::UserContext;
5use teaql_core::Value;
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub enum SqlLogOperation {
9 Select,
10 Insert,
11 Update,
12 Delete,
13 Recover,
14}
15
16impl SqlLogOperation {
17 pub fn is_select(self) -> bool {
18 matches!(self, Self::Select)
19 }
20
21 pub fn is_mutation(self) -> bool {
22 !self.is_select()
23 }
24}
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub struct SqlLogOptions {
28 pub select: bool,
29 pub mutation: bool,
30}
31
32impl Default for SqlLogOptions {
33 fn default() -> Self {
34 Self::all()
35 }
36}
37
38impl SqlLogOptions {
39 pub fn disabled() -> Self {
40 Self {
41 select: false,
42 mutation: false,
43 }
44 }
45
46 pub fn select_only() -> Self {
47 Self {
48 select: true,
49 mutation: false,
50 }
51 }
52
53 pub fn mutation_only() -> Self {
54 Self {
55 select: false,
56 mutation: true,
57 }
58 }
59
60 pub fn all() -> Self {
61 Self {
62 select: true,
63 mutation: true,
64 }
65 }
66
67 pub fn enabled_for(self, operation: SqlLogOperation) -> bool {
68 if operation.is_select() {
69 self.select
70 } else {
71 self.mutation
72 }
73 }
74}
75
76#[derive(Debug, Clone, PartialEq)]
77pub struct SqlLogEntry {
78 pub operation: SqlLogOperation,
79 pub comment: Option<String>,
80 pub purpose: Option<String>,
81 pub audit_reason: Option<String>,
82 pub trace_path: Vec<teaql_core::TraceNode>,
83 pub sql: String,
84 pub params: Vec<Value>,
85 pub debug_sql: String,
86 pub pretty_sql: String,
87 pub started_at: SystemTime,
88 pub ended_at: SystemTime,
89 pub elapsed: Duration,
90 pub result_count: Option<usize>,
91 pub result_type: Option<String>,
92 pub affected_rows: Option<u64>,
93 pub result_summary: String,
94}
95
96impl SqlLogEntry {
97 pub fn parameter_count(&self) -> usize {
103 self.params.len()
104 }
105}
106
107#[derive(Debug, Clone, PartialEq)]
108pub struct UnifiedLogEntry {
109 pub timestamp: SystemTime,
110 pub user_identifier: Option<String>,
111 pub trace_chain: Vec<teaql_core::TraceNode>,
112 pub payload: LogPayload,
113}
114
115#[derive(Debug, Clone, PartialEq)]
116#[allow(clippy::large_enum_variant)] pub enum LogPayload {
118 Sql(SqlLogEntry),
119 Info(InfoLogEntry),
120}
121
122#[derive(Debug, Clone, PartialEq)]
123pub struct InfoLogEntry {
124 pub message: String,
125}
126
127#[derive(Clone, Default)]
128pub struct UnifiedLogBuffer {
129 pub entries: Arc<Mutex<Vec<UnifiedLogEntry>>>,
130}
131
132impl UserContext {
133 pub fn with_sql_log_options(mut self, options: SqlLogOptions) -> Self {
134 self.sql_log_options = options;
135 self
136 }
137
138 pub fn set_sql_log_options(&mut self, options: SqlLogOptions) {
139 self.sql_log_options = options;
140 }
141
142 pub fn enable_select_sql_log(&mut self) {
143 self.sql_log_options.select = true;
144 }
145
146 pub fn enable_mutation_sql_log(&mut self) {
147 self.sql_log_options.mutation = true;
148 }
149
150 pub fn disable_select_sql_log(&mut self) {
151 self.sql_log_options.select = false;
152 }
153
154 pub fn disable_mutation_sql_log(&mut self) {
155 self.sql_log_options.mutation = false;
156 }
157
158 pub fn enable_all_sql_log(&mut self) {
159 self.sql_log_options = SqlLogOptions::all();
160 }
161
162 pub fn disable_sql_log(&mut self) {
163 self.sql_log_options = SqlLogOptions::disabled();
164 self.clear_sql_logs();
165 }
166
167 pub fn sql_log_options(&self) -> SqlLogOptions {
168 self.sql_log_options
169 }
170
171 pub fn sql_logs(&self) -> Vec<SqlLogEntry> {
172 self.sql_log_entries
173 .lock()
174 .map(|entries| entries.clone())
175 .unwrap_or_default()
176 }
177
178 pub fn clear_sql_logs(&self) {
179 if let Ok(mut entries) = self.sql_log_entries.lock() {
180 entries.clear();
181 }
182 }
183
184 pub(crate) fn record_metadata_log(&self, metadata: &teaql_data_service::ExecutionMetadata) {
185 let operation = match metadata.operation {
186 teaql_data_service::DataServiceOperation::Query => SqlLogOperation::Select,
187 teaql_data_service::DataServiceOperation::Insert => SqlLogOperation::Insert,
188 teaql_data_service::DataServiceOperation::Update => SqlLogOperation::Update,
189 teaql_data_service::DataServiceOperation::Delete => SqlLogOperation::Delete,
190 teaql_data_service::DataServiceOperation::Recover => SqlLogOperation::Update,
191 teaql_data_service::DataServiceOperation::Batch => SqlLogOperation::Update,
192 teaql_data_service::DataServiceOperation::Schema => SqlLogOperation::Update,
193 };
194 if !self.sql_log_options.enabled_for(operation) {
195 return;
196 }
197 let trace_path =
198 canonical_sql_trace_path(operation, &metadata.backend, &metadata.trace_chain);
199 let result_summary = metadata
200 .result_count
201 .map(|count| format!("{count} rows returned"))
202 .or_else(|| {
203 metadata
204 .affected_rows
205 .map(|affected| format!("{affected} rows affected"))
206 })
207 .unwrap_or_default();
208 let debug_sql = metadata.debug_query.as_deref().unwrap_or_default();
209 let sensitive_entry = SqlLogEntry {
210 operation,
211 comment: trace_value(&metadata.trace_chain, teaql_core::TraceKind::Comment)
212 .or_else(|| metadata.comment.clone()),
213 purpose: trace_value(&metadata.trace_chain, teaql_core::TraceKind::Purpose),
214 audit_reason: trace_value(&metadata.trace_chain, teaql_core::TraceKind::AuditReason),
215 trace_path: trace_path.clone(),
216 sql: metadata.parameterized_query.clone().unwrap_or_default(),
217 params: metadata.params.clone(),
218 pretty_sql: pretty_sql(debug_sql),
219 debug_sql: debug_sql.to_owned(),
220 started_at: metadata.started_at,
221 ended_at: metadata.ended_at,
222 elapsed: metadata
223 .ended_at
224 .duration_since(metadata.started_at)
225 .unwrap_or_default(),
226 result_count: metadata.result_count,
227 result_type: None,
228 affected_rows: metadata.affected_rows,
229 result_summary,
230 };
231 let mut safe_entry = sensitive_entry.clone();
235 safe_entry
239 .params
240 .iter_mut()
241 .for_each(|value| *value = Value::Null);
242 safe_entry.debug_sql.clear();
243 safe_entry.pretty_sql.clear();
244 self.append_sql_log(metadata.started_at, trace_path, safe_entry, sensitive_entry);
245 }
246
247 fn append_sql_log(
248 &self,
249 timestamp: SystemTime,
250 trace_path: Vec<teaql_core::TraceNode>,
251 safe_entry: SqlLogEntry,
252 sensitive_entry: SqlLogEntry,
253 ) {
254 if let Ok(mut entries) = self.sql_log_entries.lock() {
255 entries.push(safe_entry.clone());
256 }
257 if let Some(buffer) = self.get_resource::<UnifiedLogBuffer>()
258 && let Ok(mut entries) = buffer.entries.lock()
259 {
260 entries.push(UnifiedLogEntry {
261 timestamp,
262 user_identifier: self.user_identifier.clone(),
263 trace_chain: trace_path.clone(),
264 payload: LogPayload::Sql(safe_entry.clone()),
265 });
266 }
267 crate::log_formatter::LogManager::write_sql_log(&trace_path, &safe_entry);
268 crate::log_formatter::LogManager::write_sensitive_sql_log(&trace_path, &sensitive_entry);
269 }
270}
271
272fn trace_value(
273 trace_path: &[teaql_core::TraceNode],
274 kind: teaql_core::TraceKind,
275) -> Option<String> {
276 trace_path
277 .iter()
278 .rev()
279 .find(|node| node.kind == kind)
280 .map(|node| node.comment.clone())
281}
282
283fn canonical_sql_trace_path(
284 operation: SqlLogOperation,
285 backend: &str,
286 source: &[teaql_core::TraceNode],
287) -> Vec<teaql_core::TraceNode> {
288 use teaql_core::{TraceKind, TraceNode};
289
290 if source.iter().any(|node| node.kind == TraceKind::Operation)
291 && source.iter().any(|node| node.kind == TraceKind::Provider)
292 && source.iter().any(|node| node.kind == TraceKind::Sql)
293 {
294 return source
295 .iter()
296 .filter(|node| {
297 !matches!(
298 node.kind,
299 TraceKind::Comment | TraceKind::Purpose | TraceKind::AuditReason
300 )
301 })
302 .cloned()
303 .collect();
304 }
305 let operation_entity = source
306 .iter()
307 .find(|node| !node.entity_type.trim().is_empty())
308 .map(|node| node.entity_type.clone())
309 .unwrap_or_else(|| "unknown".to_owned());
310 let statement_entity = if operation.is_select() {
311 operation_entity.clone()
312 } else {
313 source
314 .iter()
315 .rev()
316 .find(|node| node.kind == TraceKind::Entity && !node.entity_type.trim().is_empty())
317 .map(|node| node.entity_type.clone())
318 .unwrap_or_else(|| operation_entity.clone())
319 };
320 let family = if operation.is_select() {
321 "query"
322 } else {
323 "mutation"
324 };
325 let statement = match operation {
326 SqlLogOperation::Select => "select",
327 SqlLogOperation::Insert => "insert",
328 SqlLogOperation::Update => "update",
329 SqlLogOperation::Delete => "delete",
330 SqlLogOperation::Recover => "recover",
331 };
332 let mut path = vec![TraceNode::typed(
333 TraceKind::Operation,
334 operation_entity,
335 None,
336 family,
337 )];
338 path.push(TraceNode::typed(
339 if operation.is_select() {
340 TraceKind::Request
341 } else {
342 TraceKind::Entity
343 },
344 statement_entity,
345 None,
346 "",
347 ));
348 path.extend(
349 source
350 .iter()
351 .filter(|node| node.kind == TraceKind::Relation)
352 .cloned(),
353 );
354 path.push(TraceNode::typed(
355 TraceKind::Provider,
356 if backend.trim().is_empty() {
357 "unknown"
358 } else {
359 backend
360 },
361 None,
362 "",
363 ));
364 path.push(TraceNode::typed(TraceKind::Sql, statement, None, ""));
365 path
366}
367
368fn pretty_sql(sql: &str) -> String {
369 let mut pretty = sql.to_owned();
370 for keyword in [
371 " FROM ",
372 " WHERE ",
373 " GROUP BY ",
374 " HAVING ",
375 " ORDER BY ",
376 " LIMIT ",
377 " OFFSET ",
378 " RETURNING ",
379 ] {
380 pretty = pretty.replace(keyword, &format!("\n{}", keyword.trim_start()));
381 }
382 pretty.replace(" AND ", "\n AND ")
383}