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