use crate::event::RawAuditEvent;
use teaql_core::TraceNode;
pub use crate::context::{SqlLogEntry, SqlLogOperation};
pub trait LogFormatter: Send + Sync {
fn format_sql_log(&self, trace_chain: &[TraceNode], entry: &SqlLogEntry) -> String;
fn format_audit_log(&self, event: &RawAuditEvent) -> String;
}
pub struct HumanReaderFormatter;
impl HumanReaderFormatter {
fn format_trace_chain(&self, trace_chain: &[TraceNode]) -> String {
(!trace_chain.is_empty())
.then(|| {
trace_chain
.iter()
.map(|n| n.comment.clone())
.collect::<Vec<_>>()
.join(" -> ")
})
.unwrap_or_default()
}
}
impl LogFormatter for HumanReaderFormatter {
fn format_sql_log(&self, trace_chain: &[TraceNode], entry: &SqlLogEntry) -> String {
let ts = chrono::Utc::now().format("%Y-%m-%d %H:%M:%S%.3f");
let trace_str = self.format_trace_chain(trace_chain);
let trace_display = (!trace_str.is_empty())
.then(|| format!(" - [{}]", trace_str))
.unwrap_or_default();
let elapsed_us = (entry.elapsed.as_secs_f64() * 1_000_000.0).round() as u64;
format!(
"[{}]-[{:>5}µs]-[DEBUG]-SqlLogEntry{} - [{}]\n {}",
ts,
elapsed_us,
trace_display,
entry.result_summary,
entry.pretty_sql.replace('\n', " ")
)
}
fn format_audit_log(&self, event: &RawAuditEvent) -> String {
let ts = chrono::Utc::now().format("%Y-%m-%d %H:%M:%S%.3f");
let trace_str = self.format_trace_chain(&event.trace_chain);
let trace_display = (!trace_str.is_empty())
.then(|| format!(" (Trace: {})", trace_str))
.unwrap_or_default();
let mut field_changes = Vec::new();
for change in &event.changes {
if change.field.starts_with('_') {
continue;
}
let val = change
.new_value
.as_ref()
.map(|v| format!("{:?}", v))
.unwrap_or_else(|| "null".to_string());
field_changes.push(format!("{}: {}", change.field, val));
}
let fields_part = (!field_changes.is_empty())
.then(|| format!(" {{{}}}", field_changes.join(", ")))
.unwrap_or_default();
let mut entity_id = "Unknown".to_string();
if let Some(vals) = &event.new_values {
if let Some(id_val) = vals.get("id") {
entity_id = format!("{:?}", id_val);
}
}
format!(
"[{}]-[AUDIT]-Entity [{}:{}] {:?}{}{}",
ts, event.entity, entity_id, event.kind, trace_display, fields_part
)
}
}
pub struct DebugReaderFormatter;
impl DebugReaderFormatter {
fn format_trace_chain(&self, trace_chain: &[TraceNode]) -> String {
match trace_chain.is_empty() {
true => "(Trace: None)".to_string(),
false => format!(
"(Trace: {})",
trace_chain
.iter()
.map(|n| n.comment.clone())
.collect::<Vec<_>>()
.join(" -> ")
),
}
}
}
impl LogFormatter for DebugReaderFormatter {
fn format_sql_log(&self, trace_chain: &[TraceNode], entry: &SqlLogEntry) -> String {
let trace_str = self.format_trace_chain(trace_chain);
format!("[SQL_LOG] {} - Event: {:?}", trace_str, entry)
}
fn format_audit_log(&self, event: &RawAuditEvent) -> String {
let trace_str = self.format_trace_chain(&event.trace_chain);
format!("[AUDIT_LOG] {} - Event: {:?}", trace_str, event)
}
}
pub struct LogFormatterFactory;
impl LogFormatterFactory {
pub fn get_formatter() -> &'static (dyn LogFormatter + Send + Sync) {
static FORMATTER: std::sync::OnceLock<Box<dyn LogFormatter + Send + Sync>> =
std::sync::OnceLock::new();
FORMATTER
.get_or_init(|| {
let format =
std::env::var("TEAQL_LOG_FORMAT").unwrap_or_else(|_| "human".to_string());
match format.as_str() {
"json" | "debug" => Box::new(DebugReaderFormatter),
_ => Box::new(HumanReaderFormatter),
}
})
.as_ref()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LogLevel {
Silent,
Summary,
Full,
FullWithPayload,
}
impl LogLevel {
pub fn parse(s: &str, default: LogLevel) -> Self {
match s {
"_silent" => LogLevel::Silent,
"_summary" => LogLevel::Summary,
"_full" => LogLevel::Full,
"_full_with_payload" => LogLevel::FullWithPayload,
_ => default,
}
}
}
pub struct LogConfig {
pub audit_level: LogLevel,
pub sql_level: LogLevel,
pub tool_level: LogLevel,
pub audit_entities: Option<Vec<String>>,
pub sql_tables: Option<Vec<String>>,
pub tool_focus: Option<Vec<String>>,
}
impl LogConfig {
pub fn load() -> Self {
let audit_level = LogLevel::parse(
&std::env::var("TEAQL_AUDIT_LOG").unwrap_or_default(),
LogLevel::Full,
);
let sql_level = LogLevel::parse(
&std::env::var("TEAQL_SQL_LOG").unwrap_or_default(),
LogLevel::Summary,
);
let tool_level = LogLevel::parse(
&std::env::var("TEAQL_TOOL_LOG").unwrap_or_default(),
LogLevel::Full,
);
let audit_entities = std::env::var("TEAQL_AUDIT_LOG_ENTITIES")
.ok()
.map(|s| s.split(',').map(|s| s.trim().to_string()).collect());
let sql_tables = std::env::var("TEAQL_SQL_LOG_TABLES")
.ok()
.map(|s| s.split(',').map(|s| s.trim().to_string()).collect());
let tool_focus = std::env::var("TEAQL_TOOL_LOG_FOCUS")
.ok()
.map(|s| s.split(',').map(|s| s.trim().to_string()).collect());
Self {
audit_level,
sql_level,
tool_level,
audit_entities,
sql_tables,
tool_focus,
}
}
pub fn should_log_audit(&self, entity: &str) -> bool {
if self.audit_level == LogLevel::Silent {
return false;
}
if let Some(entities) = &self.audit_entities {
if !entities.iter().any(|e| e.eq_ignore_ascii_case(entity)) {
return false;
}
}
true
}
pub fn should_log_sql(&self, sql: &str) -> bool {
if self.sql_level == LogLevel::Silent {
return false;
}
if let Some(tables) = &self.sql_tables {
let sql_lower = sql.to_ascii_lowercase();
if !tables
.iter()
.any(|t| sql_lower.contains(&t.to_ascii_lowercase()))
{
return false;
}
}
true
}
pub fn should_log_tool(&self, module: &str) -> bool {
if self.tool_level == LogLevel::Silent {
return false;
}
if let Some(focus) = &self.tool_focus {
if !focus.iter().any(|f| f.eq_ignore_ascii_case(module)) {
return false;
}
}
true
}
}
pub struct LogManager;
static LOG_ENDPOINT: std::sync::OnceLock<Option<String>> = std::sync::OnceLock::new();
static HEADER_WRITTEN: std::sync::Once = std::sync::Once::new();
const EXTREME_TEST_FLAG: &str =
"__i_agree_to_disable_runtime_trace_only_for_extreme_performance_testing";
impl LogManager {
pub fn config() -> &'static LogConfig {
static CONFIG: std::sync::OnceLock<LogConfig> = std::sync::OnceLock::new();
CONFIG.get_or_init(LogConfig::load)
}
fn get_log_endpoint() -> Option<&'static str> {
LOG_ENDPOINT
.get_or_init(|| {
let mode = std::env::var("TEAQL_TRACE_MODE").unwrap_or_default();
if mode == "off" {
let ack = std::env::var("TEAQL_TRACE_OFF_ACK").unwrap_or_default();
if ack == EXTREME_TEST_FLAG {
return Some("off".to_string());
}
}
std::env::var("TEAQL_LOG_ENDPOINT")
.ok()
.filter(|v| !v.is_empty())
.or_else(|| {
if let Ok(val) = std::env::var("TEAQL_DOMAIN") {
if !val.is_empty() {
return Some(format!("{}.log", val));
}
}
let exe_name = std::env::current_exe()
.ok()
.and_then(|p| p.file_name().map(|s| s.to_string_lossy().into_owned()))
.unwrap_or_else(|| "teaql".to_string());
Some(format!("{}.log", exe_name))
})
})
.as_deref()
}
fn write_header_if_needed(endpoint: &str) {
if endpoint == "off" {
return;
}
HEADER_WRITTEN.call_once(|| {
let header = include_str!("log_header.txt");
match endpoint {
"stdout" => println!("{}", header),
path => {
if let Ok(mut file) = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(path)
{
use std::io::Write;
let _ = writeln!(file, "{}", header);
}
}
}
});
}
fn write_to_file(content: &str) {
if let Some(endpoint) = Self::get_log_endpoint() {
if endpoint == "off" {
return;
}
Self::write_header_if_needed(endpoint);
match endpoint {
"stdout" => println!("{}", content),
path => {
if let Ok(mut file) = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(path)
{
use std::io::Write;
let _ = writeln!(file, "{}", content);
}
}
}
}
}
pub fn write_sql_log(trace_chain: &[TraceNode], entry: &SqlLogEntry) {
if !Self::config().should_log_sql(&entry.sql) {
return;
}
if let Some(endpoint) = Self::get_log_endpoint() {
if endpoint == "off" {
return;
}
let content = LogFormatterFactory::get_formatter().format_sql_log(trace_chain, entry);
Self::write_to_file(&content);
}
}
pub fn write_audit_log(event: &RawAuditEvent) {
if !Self::config().should_log_audit(&event.entity) {
return;
}
if let Some(endpoint) = Self::get_log_endpoint() {
if endpoint == "off" {
return;
}
let content = LogFormatterFactory::get_formatter().format_audit_log(event);
Self::write_to_file(&content);
}
}
}