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.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 if change.field.starts_with('_') {
57 continue;
58 }
59 let val = change.new_value.as_ref().map(|v| format!("{:?}", v)).unwrap_or_else(|| "null".to_string());
60 field_changes.push(format!("{}: {}", change.field, val));
61 }
62 let fields_part = if field_changes.is_empty() {
63 String::new()
64 } else {
65 format!(" {{{}}}", field_changes.join(", "))
66 };
67
68 let mut entity_id = "Unknown".to_string();
69 if let Some(vals) = &event.new_values {
70 if let Some(id_val) = vals.get("id") {
71 entity_id = format!("{:?}", id_val);
72 }
73 }
74
75 format!("[{}]-[AUDIT]-Entity [{}:{}] {:?}{}{}", ts, event.entity, entity_id, event.kind, trace_display, fields_part)
76 }
77}
78
79pub struct DebugReaderFormatter;
81
82impl DebugReaderFormatter {
83 fn format_trace_chain(&self, trace_chain: &[TraceNode]) -> String {
84 if trace_chain.is_empty() {
85 "(Trace: None)".to_string()
86 } else {
87 format!("(Trace: {})", trace_chain.iter().map(|n| n.comment.clone()).collect::<Vec<_>>().join(" -> "))
88 }
89 }
90}
91
92impl LogFormatter for DebugReaderFormatter {
93 fn format_sql_log(&self, trace_chain: &[TraceNode], entry: &SqlLogEntry) -> String {
94 let trace_str = self.format_trace_chain(trace_chain);
95 format!("[SQL_LOG] {} - Event: {:?}", trace_str, entry)
96 }
97
98 fn format_audit_log(&self, event: &RawAuditEvent) -> String {
99 let trace_str = self.format_trace_chain(&event.trace_chain);
100 format!("[AUDIT_LOG] {} - Event: {:?}", trace_str, event)
101 }
102}
103
104pub struct LogFormatterFactory;
106
107impl LogFormatterFactory {
108 pub fn get_formatter() -> &'static (dyn LogFormatter + Send + Sync) {
111 static FORMATTER: std::sync::OnceLock<Box<dyn LogFormatter + Send + Sync>> = std::sync::OnceLock::new();
112 FORMATTER.get_or_init(|| {
113 let format = std::env::var("TEAQL_LOG_FORMAT").unwrap_or_else(|_| "human".to_string());
114 if format == "json" || format == "debug" {
115 Box::new(DebugReaderFormatter)
116 } else {
117 Box::new(HumanReaderFormatter)
118 }
119 }).as_ref()
120 }
121}
122
123pub struct LogManager;
125
126static LOG_ENDPOINT: std::sync::OnceLock<Option<String>> = std::sync::OnceLock::new();
127static HEADER_WRITTEN: std::sync::Once = std::sync::Once::new();
128
129const EXTREME_TEST_FLAG: &str = "__i_agree_to_disable_runtime_trace_only_for_extreme_performance_testing";
130
131impl LogManager {
132 fn get_log_endpoint() -> Option<&'static str> {
133 LOG_ENDPOINT.get_or_init(|| {
134 let mode = std::env::var("TEAQL_TRACE_MODE").unwrap_or_default();
135 if mode == "off" {
136 let ack = std::env::var("TEAQL_TRACE_OFF_ACK").unwrap_or_default();
137 if ack == EXTREME_TEST_FLAG {
138 return Some("off".to_string());
139 }
140 }
142
143 if let Ok(val) = std::env::var("TEAQL_LOG_ENDPOINT") {
144 if val.is_empty() {
145 None } else {
147 Some(val)
148 }
149 } else {
150 None
151 }
152 .or_else(|| {
153 if let Ok(val) = std::env::var("TEAQL_DOMAIN") {
154 if !val.is_empty() {
155 return Some(format!("{}.log", val));
156 }
157 }
158 let exe_name = std::env::current_exe()
159 .ok()
160 .and_then(|p| p.file_name().map(|s| s.to_string_lossy().into_owned()))
161 .unwrap_or_else(|| "teaql".to_string());
162 Some(format!("{}.log", exe_name))
163 })
164 }).as_deref()
165 }
166
167 fn write_header_if_needed(endpoint: &str) {
168 if endpoint == "off" {
169 return;
170 }
171 HEADER_WRITTEN.call_once(|| {
172 let header = include_str!("log_header.txt");
173 if endpoint == "stdout" {
174 println!("{}", header);
175 } else {
176 if let Ok(mut file) = std::fs::OpenOptions::new().create(true).append(true).open(endpoint) {
177 use std::io::Write;
178 let _ = writeln!(file, "{}", header);
179 }
180 }
181 });
182 }
183
184 fn write_to_file(content: &str) {
185 if let Some(endpoint) = Self::get_log_endpoint() {
186 if endpoint == "off" {
187 return;
188 }
189
190 Self::write_header_if_needed(endpoint);
191
192 if endpoint == "stdout" {
193 println!("{}", content);
194 } else {
195 if let Ok(mut file) = std::fs::OpenOptions::new().create(true).append(true).open(endpoint) {
196 use std::io::Write;
197 let _ = writeln!(file, "{}", content);
198 }
199 }
200 }
201 }
202
203 pub fn write_sql_log(trace_chain: &[TraceNode], entry: &SqlLogEntry) {
204 if let Some(endpoint) = Self::get_log_endpoint() {
205 if endpoint == "off" {
206 return;
207 }
208 let content = LogFormatterFactory::get_formatter().format_sql_log(trace_chain, entry);
209 Self::write_to_file(&content);
210 }
211 }
212
213 pub fn write_audit_log(event: &RawAuditEvent) {
214 if let Some(endpoint) = Self::get_log_endpoint() {
215 if endpoint == "off" {
216 return;
217 }
218 let content = LogFormatterFactory::get_formatter().format_audit_log(event);
219 Self::write_to_file(&content);
220 }
221 }
222}