use serde::{Deserialize, Serialize}; use std::fs::{File, OpenOptions}; use std::io::Write; use std::path::PathBuf; use std::sync::Mutex; use std::time::{SystemTime, UNIX_EPOCH}; /// 日志记录器 pub struct RequestLogger { log_file: Mutex>, enabled: bool, } impl RequestLogger { pub fn new(log_path: Option<&str>) -> Self { let (log_file, enabled) = if let Some(path) = log_path { let path = expand_path(path); if let Some(parent) = path.parent() { let _ = std::fs::create_dir_all(parent); } match OpenOptions::new() .create(true) .append(true) .open(&path) { Ok(file) => (Some(file), true), Err(e) => { eprintln!("[Logger] Failed to open log file {}: {}", path.display(), e); (None, false) } } } else { (None, false) }; Self { log_file: Mutex::new(log_file), enabled, } } pub fn is_enabled(&self) -> bool { self.enabled } /// 记录请求日志 pub fn log(&self, entry: &LogEntry) { if !self.enabled { return; } let json = match serde_json::to_string(entry) { Ok(j) => j, Err(e) => { eprintln!("[Logger] Failed to serialize log: {}", e); return; } }; if let Ok(mut file) = self.log_file.lock() { if let Some(ref mut f) = *file { let _ = writeln!(f, "{}", json); } } } } /// 日志条目 #[derive(Debug, Serialize, Deserialize)] pub struct LogEntry { pub timestamp: String, pub level: String, #[serde(rename = "type")] pub log_type: String, pub client: String, pub endpoint: String, #[serde(skip_serializing_if = "Option::is_none")] pub conn: Option, #[serde(skip_serializing_if = "Option::is_none")] pub server: Option, #[serde(skip_serializing_if = "Option::is_none")] pub sql: Option, #[serde(skip_serializing_if = "Option::is_none")] pub command: Option, #[serde(rename = "durationMs")] pub duration_ms: u64, #[serde(skip_serializing_if = "Option::is_none")] pub rows: Option, #[serde(skip_serializing_if = "Option::is_none")] #[serde(rename = "exitCode")] pub exit_code: Option, #[serde(skip_serializing_if = "Option::is_none")] pub error: Option, } impl LogEntry { pub fn new(endpoint: &str, client: &str) -> Self { Self { timestamp: current_timestamp(), level: "INFO".to_string(), log_type: "request".to_string(), client: client.to_string(), endpoint: endpoint.to_string(), conn: None, server: None, sql: None, command: None, duration_ms: 0, rows: None, exit_code: None, error: None, } } pub fn with_conn(mut self, conn: &str) -> Self { self.conn = Some(conn.to_string()); self } pub fn with_server(mut self, server: &str) -> Self { self.server = Some(server.to_string()); self } pub fn with_sql(mut self, sql: &str) -> Self { // 截断过长的 SQL self.sql = Some(truncate_string(sql, 1000)); self } pub fn with_command(mut self, command: &str) -> Self { // 截断过长的命令 self.command = Some(truncate_string(command, 500)); self } pub fn with_duration(mut self, ms: u64) -> Self { self.duration_ms = ms; self } pub fn with_rows(mut self, rows: usize) -> Self { self.rows = Some(rows); self } pub fn with_exit_code(mut self, code: i32) -> Self { self.exit_code = Some(code); self } pub fn with_error(mut self, error: &str) -> Self { self.level = "ERROR".to_string(); self.error = Some(error.to_string()); self } } fn current_timestamp() -> String { let now = SystemTime::now() .duration_since(UNIX_EPOCH) .unwrap_or_default(); let secs = now.as_secs(); let datetime = chrono_timestamp(secs); format!("{}Z", datetime) } fn chrono_timestamp(secs: u64) -> String { let days = secs / 86400; let remaining = secs % 86400; let hours = remaining / 3600; let minutes = (remaining % 3600) / 60; let seconds = remaining % 60; // 从 1970-01-01 开始计算日期 let mut year = 1970; let mut days_left = days; loop { let days_in_year = if is_leap_year(year) { 366 } else { 365 }; if days_left < days_in_year { break; } days_left -= days_in_year; year += 1; } let month_days = if is_leap_year(year) { [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] } else { [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] }; let mut month = 1; for &days_in_month in &month_days { if days_left < days_in_month { break; } days_left -= days_in_month; month += 1; } let day = days_left + 1; format!("{:04}-{:02}-{:02}T{:02}:{:02}:{:02}", year, month, day, hours, minutes, seconds) } fn is_leap_year(year: u64) -> bool { (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0) } fn truncate_string(s: &str, max_len: usize) -> String { if s.len() <= max_len { s.to_string() } else { format!("{}... (truncated)", &s[..max_len]) } } fn expand_path(path: &str) -> PathBuf { if path.starts_with('~') { let home = std::env::var("HOME") .or_else(|_| std::env::var("USERPROFILE")) .unwrap_or_default(); PathBuf::from(path.replacen('~', &home, 1)) } else { PathBuf::from(path) } }