Skip to main content

teaql_runtime/
log_formatter.rs

1use crate::event::RawAuditEvent;
2use teaql_core::TraceNode;
3
4/// Represents a log entry for SQL execution
5pub use crate::context::{SqlLogEntry, SqlLogOperation};
6
7/// A trait for defining how logs should be formatted before being output
8pub trait LogFormatter: Send + Sync {
9    /// Format an SQL log entry along with its trace chain
10    fn format_sql_log(&self, trace_chain: &[TraceNode], entry: &SqlLogEntry) -> String;
11    
12    /// Format an audit or mutation event log
13    fn format_audit_log(&self, event: &RawAuditEvent) -> String;
14}
15
16/// A human-readable log formatter, designed for developers and operators.
17/// Formats time, elapsed duration, and entity changes cleanly.
18pub 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
79/// A structured or debug formatter intended for machine consumption or fallback
80pub 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
104/// Factory pattern for instantiating the correct log formatter
105pub struct LogFormatterFactory;
106
107impl LogFormatterFactory {
108    /// Returns a singleton reference to the configured LogFormatter.
109    /// It dynamically switches based on the TEAQL_LOG_FORMAT environment variable.
110    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
123/// Manager that handles reading the endpoint environment variable and dispatching to the factory
124pub 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                // If they didn't sign the waiver, ignore the off request and fallthrough
141            }
142
143            if let Ok(val) = std::env::var("TEAQL_LOG_ENDPOINT") {
144                if val.is_empty() {
145                    None // Fallthrough to default
146                } 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}