|
@@ -0,0 +1,205 @@
|
|
|
|
|
+// src/utils/log_setup.rs
|
|
|
|
|
+
|
|
|
|
|
+use tracing::Level;
|
|
|
|
|
+use tracing_subscriber::fmt::format::FmtSpan;
|
|
|
|
|
+use tracing_subscriber::fmt::time::OffsetTime;
|
|
|
|
|
+use tracing_subscriber::layer::SubscriberExt;
|
|
|
|
|
+use tracing_subscriber::util::SubscriberInitExt;
|
|
|
|
|
+use tracing_subscriber::{fmt, EnvFilter, Registry};
|
|
|
|
|
+
|
|
|
|
|
+use time::UtcOffset;
|
|
|
|
|
+use tracing_appender::non_blocking::WorkerGuard; // 导入 WorkerGuard
|
|
|
|
|
+use tracing_appender::rolling;
|
|
|
|
|
+
|
|
|
|
|
+/// 设置全局日志记录器
|
|
|
|
|
+///
|
|
|
|
|
+/// # 功能:
|
|
|
|
|
+/// - ... (其他功能描述不变) ...
|
|
|
|
|
+///
|
|
|
|
|
+/// # 返回:
|
|
|
|
|
+/// - `Ok(Vec<WorkerGuard>)`: 初始化成功,返回需要保持存活的 WorkerGuard 列表。
|
|
|
|
|
+/// 调用者必须持有这些 Guard,直到不再需要记录日志(通常是程序结束时)。
|
|
|
|
|
+/// 否则,日志可能不会被完全写入文件。
|
|
|
|
|
+/// - `Err(e)`: 初始化过程中发生错误。
|
|
|
|
|
+pub fn setup_logging() -> Result<Vec<WorkerGuard>, Box<dyn std::error::Error>> { // 返回类型改变
|
|
|
|
|
+ // 1. 设置时区
|
|
|
|
|
+ let shanghai_offset = UtcOffset::from_hms(8, 0, 0)?;
|
|
|
|
|
+ let timer = OffsetTime::new(
|
|
|
|
|
+ shanghai_offset,
|
|
|
|
|
+ time::format_description::parse(
|
|
|
|
|
+ "[year]-[month]-[day] [hour]:[minute]:[second].[subsecond digits:3]",
|
|
|
|
|
+ )?,
|
|
|
|
|
+ );
|
|
|
|
|
+
|
|
|
|
|
+ // 2. 配置日志级别过滤器
|
|
|
|
|
+ let env_filter = EnvFilter::builder()
|
|
|
|
|
+ .with_default_directive(Level::INFO.into())
|
|
|
|
|
+ .from_env_lossy();
|
|
|
|
|
+
|
|
|
|
|
+ // 准备一个 Vec 来收集 guards
|
|
|
|
|
+ let mut guards = Vec::new();
|
|
|
|
|
+
|
|
|
|
|
+ // 3. 配置日志文件输出
|
|
|
|
|
+ if !std::path::Path::new("logs").exists() {
|
|
|
|
|
+ std::fs::create_dir_all("logs")?;
|
|
|
|
|
+ println!("Created 'logs' directory for log files.");
|
|
|
|
|
+ }
|
|
|
|
|
+ let file_appender = rolling::daily("logs", "pin_trading_tool.log");
|
|
|
|
|
+ // 注意:现在我们将 guard 变量存储起来
|
|
|
|
|
+ let (non_blocking_file_writer, guard_file) = tracing_appender::non_blocking(file_appender);
|
|
|
|
|
+ guards.push(guard_file); // 将 guard 添加到列表中
|
|
|
|
|
+
|
|
|
|
|
+ let file_layer = fmt::layer()
|
|
|
|
|
+ .with_writer(non_blocking_file_writer)
|
|
|
|
|
+ .with_timer(timer.clone())
|
|
|
|
|
+ .with_target(true)
|
|
|
|
|
+ .with_file(true)
|
|
|
|
|
+ .with_line_number(true)
|
|
|
|
|
+ .with_thread_ids(true)
|
|
|
|
|
+ .with_span_events(FmtSpan::CLOSE)
|
|
|
|
|
+ .with_ansi(false)
|
|
|
|
|
+ .compact();
|
|
|
|
|
+
|
|
|
|
|
+ // 4. 配置控制台输出
|
|
|
|
|
+ // 注意:现在我们将 guard 变量存储起来
|
|
|
|
|
+ let (non_blocking_stdout_writer, guard_stdout) = tracing_appender::non_blocking(std::io::stdout());
|
|
|
|
|
+ guards.push(guard_stdout); // 将 guard 添加到列表中
|
|
|
|
|
+
|
|
|
|
|
+ let console_layer = fmt::layer()
|
|
|
|
|
+ .with_writer(non_blocking_stdout_writer)
|
|
|
|
|
+ .with_timer(timer)
|
|
|
|
|
+ .with_target(true)
|
|
|
|
|
+ .with_file(true)
|
|
|
|
|
+ .with_line_number(true)
|
|
|
|
|
+ .with_thread_ids(true)
|
|
|
|
|
+ .with_span_events(FmtSpan::CLOSE)
|
|
|
|
|
+ .pretty();
|
|
|
|
|
+
|
|
|
|
|
+ // 5. 组合图层并初始化
|
|
|
|
|
+ Registry::default()
|
|
|
|
|
+ .with(env_filter)
|
|
|
|
|
+ .with(file_layer)
|
|
|
|
|
+ .with(console_layer)
|
|
|
|
|
+ .try_init()?;
|
|
|
|
|
+
|
|
|
|
|
+ // 返回收集到的 guards
|
|
|
|
|
+ Ok(guards)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// --- 测试模块 ---
|
|
|
|
|
+#[cfg(test)]
|
|
|
|
|
+mod tests {
|
|
|
|
|
+ use super::*;
|
|
|
|
|
+ use chrono::{Local};
|
|
|
|
|
+ use chrono_tz::Asia::Shanghai;
|
|
|
|
|
+ use std::{fs, io::Read, path::Path, thread, time::Duration};
|
|
|
|
|
+
|
|
|
|
|
+ // // 辅助函数:清理日志文件 (保持不变)
|
|
|
|
|
+ // fn cleanup_log_file(filename: &str) {
|
|
|
|
|
+ // // ... (代码不变) ...
|
|
|
|
|
+ // if Path::new(filename).exists() {
|
|
|
|
|
+ // let _ = fs::remove_file(filename);
|
|
|
|
|
+ // println!("Cleaned up log file: {}", filename);
|
|
|
|
|
+ // }
|
|
|
|
|
+ // let log_dir = Path::new("logs");
|
|
|
|
|
+ // if log_dir.exists() {
|
|
|
|
|
+ // if let Ok(mut read_dir) = log_dir.read_dir() {
|
|
|
|
|
+ // if read_dir.next().is_none() { // 检查目录是否为空
|
|
|
|
|
+ // let _ = fs::remove_dir(log_dir);
|
|
|
|
|
+ // println!("Cleaned up empty log directory: logs");
|
|
|
|
|
+ // }
|
|
|
|
|
+ // }
|
|
|
|
|
+ // }
|
|
|
|
|
+ // }
|
|
|
|
|
+
|
|
|
|
|
+ #[test]
|
|
|
|
|
+ fn test_logging_setup_and_output() {
|
|
|
|
|
+ // --- 准备 ---
|
|
|
|
|
+ let now_shanghai = Local::now().with_timezone(&Shanghai);
|
|
|
|
|
+ let expected_log_filename = format!(
|
|
|
|
|
+ "logs/pin_trading_tool.log.{}",
|
|
|
|
|
+ now_shanghai.format("%Y-%m-%d")
|
|
|
|
|
+ );
|
|
|
|
|
+ println!("Expected log file: {}", expected_log_filename);
|
|
|
|
|
+
|
|
|
|
|
+ // // 测试开始前清理
|
|
|
|
|
+ // cleanup_log_file(&expected_log_filename);
|
|
|
|
|
+ //
|
|
|
|
|
+ // // 使用 RAII 清理,确保即使 panic 也会执行
|
|
|
|
|
+ // struct LogCleaner<'a>(&'a str);
|
|
|
|
|
+ // impl<'a> Drop for LogCleaner<'a> {
|
|
|
|
|
+ // fn drop(&mut self) {
|
|
|
|
|
+ // cleanup_log_file(self.0);
|
|
|
|
|
+ // println!("Log cleaner finished cleanup for: {}", self.0);
|
|
|
|
|
+ // }
|
|
|
|
|
+ // }
|
|
|
|
|
+ // let _cleaner = LogCleaner(&expected_log_filename);
|
|
|
|
|
+
|
|
|
|
|
+ // --- 执行 ---
|
|
|
|
|
+ // 1. 初始化日志系统并获取 guards
|
|
|
|
|
+ // 注意:全局日志记录器只能初始化一次。如果多个测试需要初始化,
|
|
|
|
|
+ // 需要使用 `serial_test` crate 或类似的机制来确保测试串行执行。
|
|
|
|
|
+ let setup_result = setup_logging();
|
|
|
|
|
+ assert!(setup_result.is_ok(), "Failed to setup logging: {:?}", setup_result.err());
|
|
|
|
|
+ let guards = setup_result.unwrap(); // <-- 获取 guards
|
|
|
|
|
+ println!("Logging setup successful, guards acquired.");
|
|
|
|
|
+
|
|
|
|
|
+ // 2. 记录日志消息
|
|
|
|
|
+ // 现在 guards 还存活,日志应该能被处理
|
|
|
|
|
+ let test_message = "这是测试信息\n这是测试信息换行"; // 使用唯一字符串便于查找
|
|
|
|
|
+ tracing::error!(target: "test_target", param = "value1", "错误消息内容 Error = {:?}", std::io::Error::new(std::io::ErrorKind::Other, "测试错误"));
|
|
|
|
|
+ tracing::warn!("警告信息。 Value = {}", 42);
|
|
|
|
|
+ tracing::info!(message = test_message);
|
|
|
|
|
+ tracing::debug!("调试信息 (可能被过滤)"); // 默认INFO级别,这个可能不显示
|
|
|
|
|
+ tracing::trace!("追踪信息 (可能被过滤)"); // 默认INFO级别,这个可能不显示
|
|
|
|
|
+
|
|
|
|
|
+ // 3. **显式丢弃 guards 以触发刷新**
|
|
|
|
|
+ // 在读取文件之前,确保所有缓冲的日志都已刷新。
|
|
|
|
|
+ // drop(guards) 会调用每个 WorkerGuard 的 drop 实现,这会负责 flush。
|
|
|
|
|
+ println!("Dropping logging guards to force flush...");
|
|
|
|
|
+ drop(guards); // <-- 在这里显式 drop guards
|
|
|
|
|
+ println!("Guards dropped.");
|
|
|
|
|
+
|
|
|
|
|
+ // 4. (可选)短暂等待,以防万一文件系统写入有延迟
|
|
|
|
|
+ // 在显式 drop guards 后,这个等待通常不再是必需的,但保留也无妨。
|
|
|
|
|
+ thread::sleep(Duration::from_millis(100)); // 可以尝试缩短或移除
|
|
|
|
|
+
|
|
|
|
|
+ // --- 验证 ---
|
|
|
|
|
+ // 5. 检查日志文件
|
|
|
|
|
+ println!("Checking log file: {}", expected_log_filename);
|
|
|
|
|
+ assert!(
|
|
|
|
|
+ Path::new(&expected_log_filename).exists(),
|
|
|
|
|
+ "Log file '{}' was not created.", expected_log_filename
|
|
|
|
|
+ );
|
|
|
|
|
+
|
|
|
|
|
+ let mut file = match fs::File::open(&expected_log_filename) {
|
|
|
|
|
+ Ok(f) => f,
|
|
|
|
|
+ Err(e) => panic!("Failed to open log file '{}': {}", expected_log_filename, e),
|
|
|
|
|
+ };
|
|
|
|
|
+ let mut contents = String::new();
|
|
|
|
|
+ match file.read_to_string(&mut contents) {
|
|
|
|
|
+ Ok(_) => (),
|
|
|
|
|
+ Err(e) => panic!("Failed to read log file '{}': {}", expected_log_filename, e),
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ println!("Log file content length: {}", contents.len());
|
|
|
|
|
+ // 打印一部分内容用于调试
|
|
|
|
|
+ // println!("Log content sample: {}", contents.chars().take(500).collect::<String>());
|
|
|
|
|
+
|
|
|
|
|
+ assert!(!contents.is_empty(), "Log file is empty. File: '{}'", expected_log_filename);
|
|
|
|
|
+
|
|
|
|
|
+ // 检查关键内容
|
|
|
|
|
+ assert!(contents.contains(test_message), "Log content missing info message");
|
|
|
|
|
+ assert!(contents.contains("ERROR"), "Log content missing ERROR level");
|
|
|
|
|
+ assert!(contents.contains("test_target"), "Log content missing target 'test_target'");
|
|
|
|
|
+ assert!(contents.contains("错误消息内容"), "Log content missing error message body");
|
|
|
|
|
+ assert!(contents.contains("WARN"), "Log content missing WARN level");
|
|
|
|
|
+ assert!(contents.contains("警告信息"), "Log content missing warn message");
|
|
|
|
|
+ assert!(contents.contains("INFO"), "Log content missing INFO level");
|
|
|
|
|
+ // 检查文件名和行号模式 (注意行号会变)
|
|
|
|
|
+ assert!(contents.contains("log_setup.rs:"), "Log content missing file/line info indicator ('log_setup.rs:')");
|
|
|
|
|
+
|
|
|
|
|
+ println!("Log content verified successfully.");
|
|
|
|
|
+ // _cleaner 会在函数结束时自动调用 drop 进行清理
|
|
|
|
|
+ }
|
|
|
|
|
+}
|