teaql_runtime/
log_formatter.rs1use crate::event::RawAuditEvent;
2use teaql_core::TraceNode;
3
4pub use crate::context::SqlLogEntry;
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.iter().map(|n| n.comment.clone()).collect::<Vec<_>>().join(" -> ")
26 }
27 }
28}
29
30impl LogFormatter for HumanReaderFormatter {
31 fn format_sql_log(&self, trace_chain: &[TraceNode], entry: &SqlLogEntry) -> String {
32 let ts = chrono::Utc::now().format("%Y-%m-%d %H:%M:%S%.3f");
33 let trace_str = self.format_trace_chain(trace_chain);
34 let trace_display = if trace_str.is_empty() {
35 "".to_string()
36 } else {
37 format!(" - [{}]", trace_str)
38 };
39
40 let elapsed_us = (entry.elapsed.as_secs_f64() * 1_000_000.0).round() as u64;
41 format!("[{}]-[{:>5}µs]-[DEBUG]-SqlLogEntry{} - [{}]\n {}",
42 ts, elapsed_us, trace_display, entry.result_summary, entry.pretty_sql.replace('\n', " "))
43 }
44
45 fn format_audit_log(&self, event: &RawAuditEvent) -> String {
46 let ts = chrono::Utc::now().format("%Y-%m-%d %H:%M:%S%.3f");
47 let trace_str = self.format_trace_chain(&event.trace_chain);
48 let trace_display = if trace_str.is_empty() {
49 String::new()
50 } else {
51 format!(" (Trace: {})", trace_str)
52 };
53
54 let mut field_changes = Vec::new();
55 for change in &event.changes {
56 let val = change.new_value.as_ref().map(|v| format!("{:?}", v)).unwrap_or_else(|| "null".to_string());
57 field_changes.push(format!("{}: {}", change.field, val));
58 }
59 let fields_part = if field_changes.is_empty() {
60 String::new()
61 } else {
62 format!(" {{{}}}", field_changes.join(", "))
63 };
64
65 let mut entity_id = "Unknown".to_string();
66 if let Some(vals) = &event.new_values {
67 if let Some(id_val) = vals.get("id") {
68 entity_id = format!("{:?}", id_val);
69 }
70 }
71
72 format!("[{}]-[AUDIT]-Entity [{}:{}] {:?}{}{}", ts, event.entity, entity_id, event.kind, trace_display, fields_part)
73 }
74}
75
76pub struct DebugReaderFormatter;
78
79impl DebugReaderFormatter {
80 fn format_trace_chain(&self, trace_chain: &[TraceNode]) -> String {
81 if trace_chain.is_empty() {
82 "(Trace: None)".to_string()
83 } else {
84 format!("(Trace: {})", trace_chain.iter().map(|n| n.comment.clone()).collect::<Vec<_>>().join(" -> "))
85 }
86 }
87}
88
89impl LogFormatter for DebugReaderFormatter {
90 fn format_sql_log(&self, trace_chain: &[TraceNode], entry: &SqlLogEntry) -> String {
91 let trace_str = self.format_trace_chain(trace_chain);
92 format!("[SQL_LOG] {} - Event: {:?}", trace_str, entry)
93 }
94
95 fn format_audit_log(&self, event: &RawAuditEvent) -> String {
96 let trace_str = self.format_trace_chain(&event.trace_chain);
97 format!("[AUDIT_LOG] {} - Event: {:?}", trace_str, event)
98 }
99}
100
101pub struct LogFormatterFactory;
103
104impl LogFormatterFactory {
105 pub fn get_formatter() -> &'static (dyn LogFormatter + Send + Sync) {
108 static FORMATTER: std::sync::OnceLock<Box<dyn LogFormatter + Send + Sync>> = std::sync::OnceLock::new();
109 FORMATTER.get_or_init(|| {
110 let format = std::env::var("TEAQL_LOG_FORMAT").unwrap_or_else(|_| "human".to_string());
111 if format == "json" || format == "debug" {
112 Box::new(DebugReaderFormatter)
113 } else {
114 Box::new(HumanReaderFormatter)
115 }
116 }).as_ref()
117 }
118}
119
120pub struct LogManager;
122
123static LOG_ENDPOINT: std::sync::OnceLock<Option<String>> = std::sync::OnceLock::new();
124
125impl LogManager {
126 fn get_log_endpoint() -> Option<&'static str> {
127 LOG_ENDPOINT.get_or_init(|| {
128 std::env::var("TEAQL_LOG_ENDPOINT").ok().filter(|s| !s.is_empty())
129 }).as_deref()
130 }
131
132 fn write_to_file(content: &str) {
133 if let Some(endpoint) = Self::get_log_endpoint() {
134 if let Ok(mut file) = std::fs::OpenOptions::new().create(true).append(true).open(endpoint) {
135 use std::io::Write;
136 let _ = writeln!(file, "{}", content);
137 }
138 }
139 }
140
141 pub fn write_sql_log(trace_chain: &[TraceNode], entry: &SqlLogEntry) {
142 if Self::get_log_endpoint().is_none() {
143 return;
144 }
145 let content = LogFormatterFactory::get_formatter().format_sql_log(trace_chain, entry);
146 Self::write_to_file(&content);
147 }
148
149 pub fn write_audit_log(event: &RawAuditEvent) {
150 if Self::get_log_endpoint().is_none() {
151 return;
152 }
153 let content = LogFormatterFactory::get_formatter().format_audit_log(event);
154 Self::write_to_file(&content);
155 }
156}