1use crate::event::RawAuditEvent;
2use teaql_core::TraceNode;
3
4pub use crate::context::{SqlLogEntry, SqlLogOperation};
6
7pub trait LogFormatter: Send + Sync {
9 fn format_sql_log(&self, trace_chain: &[TraceNode], entry: &SqlLogEntry) -> String;
11
12 fn format_audit_log(&self, event: &RawAuditEvent) -> String;
14}
15
16pub struct HumanReaderFormatter;
19
20impl HumanReaderFormatter {
21 fn format_trace_chain(&self, trace_chain: &[TraceNode]) -> String {
22 (!trace_chain.is_empty())
23 .then(|| {
24 trace_chain
25 .iter()
26 .enumerate()
27 .map(|(level, n)| {
28 format!("{}:{:?}:{}={}", level, n.kind, n.entity_type, n.comment)
29 })
30 .collect::<Vec<_>>()
31 .join(" -> ")
32 })
33 .unwrap_or_default()
34 }
35}
36
37impl LogFormatter for HumanReaderFormatter {
38 fn format_sql_log(&self, trace_chain: &[TraceNode], entry: &SqlLogEntry) -> String {
39 let ts = chrono::Utc::now().format("%Y-%m-%d %H:%M:%S%.3f");
40 let trace_str = self.format_trace_chain(trace_chain);
41 let trace_display = (!trace_str.is_empty())
42 .then(|| format!(" - [{}]", trace_str))
43 .unwrap_or_default();
44
45 let elapsed_us = (entry.elapsed.as_secs_f64() * 1_000_000.0).round() as u64;
46 let intent = format!(
47 "comment={:?} purpose={:?} auditReason={:?}",
48 entry.comment, entry.purpose, entry.audit_reason
49 );
50 format!(
51 "[{}]-[{:>5}µs]-[DEBUG]-SqlLogEntry{} - [{}] {}\n Parameterized SQL: {} params={:?}\n Debug SQL: {}",
52 ts,
53 elapsed_us,
54 trace_display,
55 entry.result_summary,
56 intent,
57 entry.sql.replace('\n', " "),
58 entry.params,
59 entry.debug_sql.replace('\n', " ")
60 )
61 }
62
63 fn format_audit_log(&self, event: &RawAuditEvent) -> String {
64 let ts = chrono::Utc::now().format("%Y-%m-%d %H:%M:%S%.3f");
65 let trace_str = self.format_trace_chain(&event.trace_chain);
66 let trace_display = (!trace_str.is_empty())
67 .then(|| format!(" (Trace: {})", trace_str))
68 .unwrap_or_default();
69
70 let mut field_changes = Vec::new();
71 for change in &event.changes {
72 if change.field.starts_with('_') {
73 continue;
74 }
75 let val = change
76 .new_value
77 .as_ref()
78 .map(|v| format!("{:?}", v))
79 .unwrap_or_else(|| "null".to_string());
80 field_changes.push(format!("{}: {}", change.field, val));
81 }
82 let fields_part = (!field_changes.is_empty())
83 .then(|| format!(" {{{}}}", field_changes.join(", ")))
84 .unwrap_or_default();
85
86 let mut entity_id = "Unknown".to_string();
87 if let Some(vals) = &event.new_values {
88 if let Some(id_val) = vals.get("id") {
89 entity_id = format!("{:?}", id_val);
90 }
91 }
92
93 format!(
94 "[{}]-[AUDIT]-Entity [{}:{}] {:?}{}{}",
95 ts, event.entity, entity_id, event.kind, trace_display, fields_part
96 )
97 }
98}
99
100pub struct DebugReaderFormatter;
102
103impl DebugReaderFormatter {
104 fn format_trace_chain(&self, trace_chain: &[TraceNode]) -> String {
105 match trace_chain.is_empty() {
106 true => "(Trace: None)".to_string(),
107 false => format!(
108 "(Trace: {})",
109 trace_chain
110 .iter()
111 .enumerate()
112 .map(|(level, n)| {
113 format!("{}:{:?}:{}={}", level, n.kind, n.entity_type, n.comment)
114 })
115 .collect::<Vec<_>>()
116 .join(" -> ")
117 ),
118 }
119 }
120}
121
122impl LogFormatter for DebugReaderFormatter {
123 fn format_sql_log(&self, trace_chain: &[TraceNode], entry: &SqlLogEntry) -> String {
124 let trace_str = self.format_trace_chain(trace_chain);
125 format!("[SQL_LOG] {} - Event: {:?}", trace_str, entry)
126 }
127
128 fn format_audit_log(&self, event: &RawAuditEvent) -> String {
129 let trace_str = self.format_trace_chain(&event.trace_chain);
130 format!("[AUDIT_LOG] {} - Event: {:?}", trace_str, event)
131 }
132}
133
134pub struct LogFormatterFactory;
136
137impl LogFormatterFactory {
138 pub fn get_formatter() -> &'static (dyn LogFormatter + Send + Sync) {
141 static FORMATTER: std::sync::OnceLock<Box<dyn LogFormatter + Send + Sync>> =
142 std::sync::OnceLock::new();
143 FORMATTER
144 .get_or_init(|| {
145 let format =
146 std::env::var("TEAQL_LOG_FORMAT").unwrap_or_else(|_| "human".to_string());
147 match format.as_str() {
148 "json" | "debug" => Box::new(DebugReaderFormatter),
149 _ => Box::new(HumanReaderFormatter),
150 }
151 })
152 .as_ref()
153 }
154}
155
156#[derive(Debug, Clone, Copy, PartialEq, Eq)]
157pub enum LogLevel {
158 Silent,
159 Summary,
160 Full,
161 FullWithPayload,
162}
163
164impl LogLevel {
165 pub fn parse(s: &str, default: LogLevel) -> Self {
166 match s {
167 "_silent" => LogLevel::Silent,
168 "_summary" => LogLevel::Summary,
169 "_full" => LogLevel::Full,
170 "_full_with_payload" => LogLevel::FullWithPayload,
171 _ => default,
172 }
173 }
174}
175
176pub struct LogConfig {
177 pub audit_level: LogLevel,
178 pub sql_level: LogLevel,
179 pub tool_level: LogLevel,
180 pub audit_entities: Option<Vec<String>>,
181 pub sql_tables: Option<Vec<String>>,
182 pub tool_focus: Option<Vec<String>>,
183}
184
185impl LogConfig {
186 pub fn load() -> Self {
187 let audit_level = LogLevel::parse(
188 &std::env::var("TEAQL_AUDIT_LOG").unwrap_or_default(),
189 LogLevel::Full,
190 );
191 let sql_level = LogLevel::parse(
192 &std::env::var("TEAQL_SQL_LOG").unwrap_or_default(),
193 LogLevel::Summary,
194 );
195 let tool_level = LogLevel::parse(
196 &std::env::var("TEAQL_TOOL_LOG").unwrap_or_default(),
197 LogLevel::Full,
198 );
199
200 let audit_entities = std::env::var("TEAQL_AUDIT_LOG_ENTITIES")
201 .ok()
202 .map(|s| s.split(',').map(|s| s.trim().to_string()).collect());
203 let sql_tables = std::env::var("TEAQL_SQL_LOG_TABLES")
204 .ok()
205 .map(|s| s.split(',').map(|s| s.trim().to_string()).collect());
206 let tool_focus = std::env::var("TEAQL_TOOL_LOG_FOCUS")
207 .ok()
208 .map(|s| s.split(',').map(|s| s.trim().to_string()).collect());
209
210 Self {
211 audit_level,
212 sql_level,
213 tool_level,
214 audit_entities,
215 sql_tables,
216 tool_focus,
217 }
218 }
219
220 pub fn should_log_audit(&self, entity: &str) -> bool {
221 if self.audit_level == LogLevel::Silent {
222 return false;
223 }
224 if let Some(entities) = &self.audit_entities {
225 if !entities.iter().any(|e| e.eq_ignore_ascii_case(entity)) {
226 return false;
227 }
228 }
229 true
230 }
231
232 pub fn should_log_sql(&self, sql: &str) -> bool {
233 if self.sql_level == LogLevel::Silent {
234 return false;
235 }
236 if let Some(tables) = &self.sql_tables {
237 let sql_lower = sql.to_ascii_lowercase();
238 if !tables
239 .iter()
240 .any(|t| sql_lower.contains(&t.to_ascii_lowercase()))
241 {
242 return false;
243 }
244 }
245 true
246 }
247
248 pub fn should_log_tool(&self, module: &str) -> bool {
249 if self.tool_level == LogLevel::Silent {
250 return false;
251 }
252 if let Some(focus) = &self.tool_focus {
253 if !focus.iter().any(|f| f.eq_ignore_ascii_case(module)) {
254 return false;
255 }
256 }
257 true
258 }
259}
260
261pub struct LogManager;
263
264static LOG_ENDPOINT: std::sync::OnceLock<Option<String>> = std::sync::OnceLock::new();
265static HEADER_WRITTEN: std::sync::Once = std::sync::Once::new();
266
267const EXTREME_TEST_FLAG: &str =
268 "__i_agree_to_disable_runtime_trace_only_for_extreme_performance_testing";
269
270impl LogManager {
271 pub fn config() -> &'static LogConfig {
272 static CONFIG: std::sync::OnceLock<LogConfig> = std::sync::OnceLock::new();
273 CONFIG.get_or_init(LogConfig::load)
274 }
275
276 fn get_log_endpoint() -> Option<&'static str> {
277 LOG_ENDPOINT
278 .get_or_init(|| {
279 let mode = std::env::var("TEAQL_TRACE_MODE").unwrap_or_default();
280 if mode == "off" {
281 let ack = std::env::var("TEAQL_TRACE_OFF_ACK").unwrap_or_default();
282 if ack == EXTREME_TEST_FLAG {
283 return Some("off".to_string());
284 }
285 }
287
288 std::env::var("TEAQL_LOG_ENDPOINT")
289 .ok()
290 .filter(|v| !v.is_empty())
291 .or_else(|| {
292 if let Ok(val) = std::env::var("TEAQL_DOMAIN") {
293 if !val.is_empty() {
294 return Some(format!("{}.log", val));
295 }
296 }
297 let exe_name = std::env::current_exe()
298 .ok()
299 .and_then(|p| p.file_name().map(|s| s.to_string_lossy().into_owned()))
300 .unwrap_or_else(|| "teaql".to_string());
301 Some(format!("{}.log", exe_name))
302 })
303 })
304 .as_deref()
305 }
306
307 fn write_header_if_needed(endpoint: &str) {
308 if endpoint == "off" {
309 return;
310 }
311 HEADER_WRITTEN.call_once(|| {
312 let header = include_str!("log_header.txt");
313 match endpoint {
314 "stdout" => println!("{}", header),
315 path => {
316 if let Ok(mut file) = std::fs::OpenOptions::new()
317 .create(true)
318 .append(true)
319 .open(path)
320 {
321 use std::io::Write;
322 let _ = writeln!(file, "{}", header);
323 }
324 }
325 }
326 });
327 }
328
329 fn write_to_file(content: &str) {
330 if let Some(endpoint) = Self::get_log_endpoint() {
331 if endpoint == "off" {
332 return;
333 }
334
335 Self::write_header_if_needed(endpoint);
336
337 match endpoint {
338 "stdout" => println!("{}", content),
339 path => {
340 if let Ok(mut file) = std::fs::OpenOptions::new()
341 .create(true)
342 .append(true)
343 .open(path)
344 {
345 use std::io::Write;
346 let _ = writeln!(file, "{}", content);
347 }
348 }
349 }
350 }
351 }
352
353 pub fn write_sql_log(trace_chain: &[TraceNode], entry: &SqlLogEntry) {
354 if !Self::config().should_log_sql(&entry.sql) {
355 return;
356 }
357 if let Some(endpoint) = Self::get_log_endpoint() {
358 if endpoint == "off" {
359 return;
360 }
361 let content = LogFormatterFactory::get_formatter().format_sql_log(trace_chain, entry);
362 Self::write_to_file(&content);
363 }
364 }
365
366 pub fn write_audit_log(event: &RawAuditEvent) {
367 if !Self::config().should_log_audit(&event.entity) {
368 return;
369 }
370 if let Some(endpoint) = Self::get_log_endpoint() {
371 if endpoint == "off" {
372 return;
373 }
374 let content = LogFormatterFactory::get_formatter().format_audit_log(event);
375 Self::write_to_file(&content);
376 }
377 }
378}