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