Files
rust-work/ssh-proxy/src/logger.rs
绝尘 f59ed9aae0 优化: ssh-proxy russh 迁移后代码整理
- 移除未使用的 async-trait 依赖
- 添加断线重连逻辑,会话失效时自动重连
- 修复 get_or_create_session TOCTOU 竞态条件
- 日志智能分级: 慢请求告警、退出码识别
- 用 time crate 替换手写日期计算 (删除40行)
- UTF-8 安全截断修复
- 同步优化 mysql-proxy 日志模块
2026-03-21 00:53:27 +08:00

196 lines
5.4 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
use serde::{Deserialize, Serialize};
use std::fs::{File, OpenOptions};
use std::io::Write;
use std::path::PathBuf;
use std::sync::Mutex;
/// 日志记录器
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;
// 慢请求告警: >3s 为 WARN, >10s 为 ERROR
// 注意: 后台命令(nohup)可能耗时很长但 exitCode=0不标记错误
if ms > 10000 && self.level == "INFO" {
self.level = "ERROR".to_string();
} else if ms > 3000 && self.level == "INFO" {
self.level = "WARN".to_string();
}
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);
// 只标记真正的错误 (>=2 或 127)
// exitCode: -1 (pkill未找到), 0 (成功), 1 (grep未匹配) 视为正常
if code >= 2 || code == 127 {
if self.level == "INFO" {
self.level = "ERROR".to_string();
}
}
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 {
time::OffsetDateTime::now_utc()
.format(&time::macros::format_description!("[year]-[month]-[day]T[hour]:[minute]:[second]"))
.unwrap_or_default()
}
fn truncate_string(s: &str, max_len: usize) -> String {
if s.chars().count() <= max_len {
s.to_string()
} else {
format!("{}... (truncated)", s.chars().take(max_len).collect::<String>())
}
}
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)
}
}