- mysql-proxy: MySQL HTTP 代理,连接池复用 - ssh-proxy: SSH HTTP 代理,会话复用 - mysql-cli: 轻量级 MySQL CLI 工具 功能特性: - 延迟初始化,启动快 - CLI 和 HTTP API 双模式 - 请求日志支持 - 错误友好提示 - JSON 极简输出格式
229 lines
5.9 KiB
Rust
229 lines
5.9 KiB
Rust
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<Option<File>>,
|
|
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<String>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub server: Option<String>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub sql: Option<String>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub command: Option<String>,
|
|
#[serde(rename = "durationMs")]
|
|
pub duration_ms: u64,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub rows: Option<usize>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
#[serde(rename = "exitCode")]
|
|
pub exit_code: Option<i32>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub error: Option<String>,
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|