Sfoglia il codice sorgente

日志配置第一版ok了

skyffire 6 mesi fa
parent
commit
6989dcb718
3 ha cambiato i file con 275 aggiunte e 2 eliminazioni
  1. 3 2
      .gitignore
  2. 67 0
      Cargo.toml
  3. 205 0
      src/utils/log_setup.rs

+ 3 - 2
.gitignore

@@ -1,4 +1,5 @@
 /target
-.idea
+.idea/
 Cargo.lock
-Config.json
+Config.json
+logs/

+ 67 - 0
Cargo.toml

@@ -6,3 +6,70 @@ edition = "2021"
 # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
 
 [dependencies]
+# 异步运行时,选择 tokio,并启用完整特性集(包含多线程运行时、net, time, fs, process, signal 等)
+# "full" 特性非常方便开发,生产环境可以根据实际需求裁剪
+tokio = { version = "1", features = ["full"] }
+
+# HTTP 客户端,用于 REST API 请求(获取交易对、下单等)
+# - "json": 启用 JSON 支持,方便发送和接收 JSON 数据
+# - "tokio-native-tls": 与 tokio 集成,支持 HTTPS (SSL/TLS),使用系统的 TLS 实现
+reqwest = { version = "0.11", features = ["json", "tokio-native-tls"] }
+
+# WebSocket 客户端,基于 tokio 构建,用于订阅 K 线和深度
+# - "tokio-native-tls": 与 tokio 集成,支持 WSS (SSL/TLS)
+tokio-tungstenite = { version = "0.21", features = ["tokio-native-tls"] }
+
+# futures 工具库,提供一些异步编程中常用的 trait 和工具
+# 包含了 Stream 的一些方法,例如 split 用于分离 WebSocket stream 的读写端
+futures-util = "0.3"
+
+# URL 解析和构建库,在处理交易所端点或配置代理时可能会用到
+url = "2"
+
+# 序列化和反序列化框架,用于将 Rust 结构体与各种数据格式相互转换
+# - "derive": 启用 derive 宏,方便自动实现 Serialize 和 Deserialize trait
+serde = { version = "1", features = ["derive"] }
+
+# serde 的 JSON 实现,用于处理交易所 API 的 JSON 数据和应用配置(如果使用 JSON 格式)
+serde_json = "1"
+
+# 日志和诊断框架,推荐使用 tracing,功能强大且适合异步应用
+tracing = "0.1"
+
+# tracing 的 subscriber 实现,用于配置日志输出格式、目的地等
+# - "env-filter": 允许通过 RUST_LOG 环境变量控制日志级别
+# - "fmt": 提供格式化输出到控制台
+# - "json": 可选,如果需要结构化 JSON 日志
+tracing-subscriber = { version = "0.3", features = ["json", "fmt", "env-filter", "registry", "time"] }
+
+# 用于日志文件按日期滚动
+tracing-appender = "0.2"
+
+# 简化错误处理的库,方便快速构建可链式调用的错误
+anyhow = "1"
+
+# 日期和时间处理
+chrono = "0.4"
+# 时区数据库,用于获取 Asia/Shanghai 时区
+chrono-tz = "0.8"
+# 确保 time crate 的特性被启用
+time = { version = "0.3", features = ["macros", "formatting", "parsing", "local-offset"] }
+
+
+# 用于定义自定义错误类型的库,与 anyhow 配合使用,让错误更具语义
+thiserror = "1"
+
+# 异步 SQL 数据库客户端,选择支持 tokio 和 SQLite 的版本,用于存储配置等
+# - "runtime-tokio": 使用 tokio 作为异步运行时
+# - "sqlite": 启用 SQLite 驱动
+# - "macros": 启用宏支持,用于 compile-time query checking (强烈推荐)
+# - "offline": 用于离线模式下的宏检查 (与 "macros" 配合使用)
+sqlx = { version = "0.7", features = ["runtime-tokio", "sqlite", "macros"] }
+
+# 可选项:更高级的配置管理库,如果配置结构比较复杂,可以考虑使用
+# config = "0.13"
+
+# =======================================================
+# 以下是一些在开发过程中可能会用到的devDependencies,只用于开发和测试,不包含在最终发布版本中
+# [dev-dependencies]
+# pretty_assertions = "1" # 用于在测试中提供更清晰的断言失败信息

+ 205 - 0
src/utils/log_setup.rs

@@ -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 进行清理
+    }
+}