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