# 🚀 Rust Logger **Rust Logger** 是一个专为微服务架构设计的高性能、异步、可扩展的 Rust 日志库。 它基于 `Tokio` 运行时,支持多输出源(控制台、文件、PostgreSQL),内置分布式追踪(TraceID)支持,并提供自动化的日志轮转与过期清理机制。 ## ✨ 核心特性 * **⚡ 全异步非阻塞**:使用 `tokio` 和 `channel` 异步写入,确保业务主线程不受 I/O 阻塞。 * **🗄️ PostgreSQL 集成**: * 支持 **自动按月分表**(Auto Partitioning),无需人工干预。 * 支持 `service_name` 区分多服务。 * 结构化存储,便于 SQL 审计与分析。 * **📄 文件日志管理**: * 支持 **按天 + 按大小 (100MB)** 自动轮转切割。 * 支持基于文件名的智能过期删除。 * **🔍 分布式追踪 (TraceID)**: * 基于 `Task Local` 的无侵入式上下文传递。 * 全链路日志关联,轻松定位并发请求问题。 * **🧹 自动清理 (Retention)**: * 后台协程自动清理过期数据库分区和旧日志文件。 ## 📂 项目结构 ```text rust_logger/ ├── Cargo.toml # 项目依赖配置 ├── tests/ # 集成测试 ├── src/ │ ├── lib.rs # 库入口 (导出宏与全局实例) │ ├── core.rs # 核心引擎 (Channel 管理与后台协程) │ ├── model.rs # 数据模型 (LogRecord, LogLevel) │ ├── context.rs # 链路追踪上下文 (Task Local) │ ├── cleaner.rs # 自动清理任务 (Retention Policy) │ └── outputs/ # 输出策略模块 │ ├── mod.rs # LogOutput Trait 定义 │ ├── console.rs # 控制台输出 │ ├── file.rs # 文件输出 (含轮转逻辑) │ └── postgres.rs # 数据库输出 (含自动分表逻辑) ``` ## 📦 安装说明 由于本项目托管于内部 Kellnr 仓库,请按照以下步骤配置。 ### 1. 配置 Registry 源 在项目根目录或全局的 `.cargo/config.toml` 中添加: ```toml [registries.kellnr] index = "sparse+https://kellnr.shay7sev.site/api/v1/crates/" ``` ### 2. 添加依赖 在你的 `Cargo.toml` 中添加: ```toml [dependencies] # 指定 registry 为私有源 rust_logger = { version = "0.1.0", registry = "kellnr" } tokio = { version = "1", features = ["full"] } sqlx = { version = "0.7", features = ["postgres", "runtime-tokio-native-tls"] } ``` ## 🛠️ 快速开始 ### 1. 数据库准备 在使用 PostgreSQL 输出前,请先创建基础表结构: ```sql CREATE TABLE app_logs ( id BIGSERIAL, service_name VARCHAR(50) NOT NULL, log_level VARCHAR(10) NOT NULL, message TEXT NOT NULL, module VARCHAR(100), trace_id VARCHAR(64), -- 支持 TraceID created_at TIMESTAMPTZ NOT NULL ) PARTITION BY RANGE (created_at); -- 建立索引 CREATE INDEX idx_logs_service_time ON app_logs(service_name, created_at); CREATE INDEX idx_logs_trace_id ON app_logs(trace_id); ``` ### 2. 初始化 Logger ```rust use rust_logger::{ Logger, LoggerConfig, LogLevel, LogCleaner, outputs::{ConsoleOutput, PostgresOutput, FileOutput}, set_global_logger, log_info }; use sqlx::postgres::PgPoolOptions; #[tokio::main] async fn main() -> anyhow::Result<()> { // 1. 连接数据库 let pool = PgPoolOptions::new().connect("postgres://...").await?; // 2. 初始化文件输出 let file_output = FileOutput::new("logs", "my-service").await?; // 3. 配置 Logger let config = LoggerConfig { min_level: LogLevel::INFO, outputs: vec![ Box::new(ConsoleOutput), Box::new(PostgresOutput::new(pool.clone())), Box::new(file_output), ], }; // 4. 启动全局 Logger (传入当前服务名) let logger = Logger::init("my-service", config); set_global_logger(logger); // 5. (可选) 启动自动清理任务:保留 7 天日志 LogCleaner::new(7) .with_db_cleanup(pool) .with_file_cleanup("logs", "my-service") .start(); // 6. 打印日志 log_info!("System started successfully!"); Ok(()) } ``` ## 🔍 使用 TraceID 进行链路追踪 在处理 HTTP 请求或复杂业务时,建议使用 `with_trace_id` 包裹逻辑。 ```rust use rust_logger::context::with_trace_id; use rust_logger::{log_info, log_error}; use uuid::Uuid; async fn handle_request() { let trace_id = Uuid::new_v4().to_string(); // 在此闭包内的所有日志都会自动带上 trace_id with_trace_id(trace_id, async { log_info!("收到请求"); process_payment().await; log_info!("请求处理完成"); }).await; } async fn process_payment() { // 无需手动传递 ID,自动获取上下文 log_info!("正在扣款..."); } ``` **日志输出示例:** ```text 2026-01-23 10:00:01 [INFO] [a1b2-c3d4...] - 收到请求 2026-01-23 10:00:02 [INFO] [a1b2-c3d4...] - 正在扣款... 2026-01-23 10:00:03 [INFO] [a1b2-c3d4...] - 请求处理完成 ```
### 实例:编写业务代码 (`order-service/src/main.rs`) 我们需要引入 `uuid` 来生成唯一的 ID。 **Cargo.toml:** ```toml [dependencies] # ... 其他依赖 uuid = { version = "1.0", features = ["v4"] } ``` **main.rs:** 在这个例子中,我们模拟**两个并发请求**同时进入系统。 ```rust use rust_logger::{ context::with_trace_id, // 核心:上下文管理器 log_info, log_error, set_global_logger, Logger, LoggerConfig, LogLevel, LogCleaner, outputs::{ConsoleOutput, PostgresOutput, FileOutput} }; use sqlx::postgres::PgPoolOptions; use std::time::Duration; use uuid::Uuid; use std::sync::Arc; #[tokio::main] async fn main() -> anyhow::Result<()> { // 1. 初始化 DB let db_url = std::env::var("DATABASE_URL").expect("DATABASE_URL must be set"); let pool = PgPoolOptions::new().connect(&db_url).await?; // 2. 初始化 Logger (同时输出到 Console, DB, File) let file_output = FileOutput::new("logs", "order-service").await?; let config = LoggerConfig { min_level: LogLevel::INFO, outputs: vec![ Box::new(ConsoleOutput), Box::new(PostgresOutput::new(pool.clone())), // DB Box::new(file_output), // File ], }; set_global_logger(Logger::init("order-service", config)); // 3. 启动清理任务 (可选) LogCleaner::new(7).with_db_cleanup(pool.clone()).start(); println!(">>> 服务启动,开始模拟并发请求...\n"); // 4. 模拟并发:同时处理两个用户的下单请求 // 场景: // - 用户 A (Alice) 下单成功 // - 用户 B (Bob) 下单失败 let request_a = tokio::spawn(mock_http_handler("Alice", "ITEM-001", true)); let request_b = tokio::spawn(mock_http_handler("Bob", "ITEM-999", false)); // 模拟库存不足 // 等待请求处理完成 let _ = tokio::join!(request_a, request_b); // 等待日志写入完毕 tokio::time::sleep(Duration::from_secs(1)).await; println!("\n>>> 模拟结束。请查看数据库或文件日志。"); Ok(()) } // 模拟 HTTP 请求入口 (Middleware 层) async fn mock_http_handler(user: &str, item_id: &str, is_success: bool) { // 1. 生成 TraceID (通常从 HTTP Header "X-Trace-ID" 获取,没有则生成) let trace_id = Uuid::new_v4().to_string(); // 2. 【关键】使用 with_trace_id 包裹整个业务逻辑 // 只要在这个闭包里,无论调用多深层的函数,log_info! 都会自动带上 ID with_trace_id(trace_id.clone(), async move { log_info!("收到下单请求: User={}, Item={}", user, item_id); // 进入业务逻辑层 process_order(user, item_id, is_success).await; log_info!("请求处理完毕,返回响应给 {}", user); }).await; } // 模拟业务逻辑层 (Service 层) async fn process_order(user: &str, item_id: &str, simulate_success: bool) { log_info!("正在检查库存: {}", item_id); tokio::time::sleep(Duration::from_millis(50)).await; // 模拟耗时 if simulate_success { log_info!("库存充足,开始扣款..."); payment_service(user, 100).await; log_info!("下单成功!订单号: ORDER-{}", Uuid::new_v4()); } else { log_error!("库存不足!商品 {} 缺货", item_id); log_info!("订单已取消"); } } // 模拟底层服务 (DAO/RPC 层) async fn payment_service(user: &str, amount: i32) { // 注意:这里我们没有传 trace_id 参数,但 log_info 依然能获取到它! log_info!("扣款成功: User={}, Amount=${}", user, amount); } ``` ### 测试:运行与证明 运行代码后,我们来看看 **为什么 TraceID 是分布式的救命稻草**。 #### 1. 如果没有 TraceID (灾难现场) 在控制台或文件中,由于是异步并发,日志会是**时间交错**的。你看到的可能是这样: ```text 2026-01-22 10:00:01 [INFO] 收到下单请求: User=Alice 2026-01-22 10:00:01 [INFO] 收到下单请求: User=Bob <-- 乱入 2026-01-22 10:00:02 [INFO] 正在检查库存: ITEM-001 2026-01-22 10:00:02 [INFO] 正在检查库存: ITEM-999 <-- 分不清谁是谁了 2026-01-22 10:00:03 [ERROR] 库存不足! 2026-01-22 10:00:03 [INFO] 库存充足,开始扣款... <-- 谁库存充足?谁不足? 2026-01-22 10:00:04 [INFO] 扣款成功 ``` **问题**:当 Bob 投诉说“我下单失败了”,你看着日志里的“扣款成功”会陷入沉思:到底扣没扣 Bob 的钱? #### 2. 使用 TraceID 后 (真相大白) 现在查看生成的文件日志 `logs/order-service-YYYY-MM-DD.log`,或者查看数据库。 **实际输出:** ```text [INFO] [550e8400-e29b...] - 收到下单请求: User=Alice [INFO] [a1b2c3d4-ffff...] - 收到下单请求: User=Bob [INFO] [550e8400-e29b...] - 正在检查库存: ITEM-001 [INFO] [a1b2c3d4-ffff...] - 正在检查库存: ITEM-999 [ERROR] [a1b2c3d4-ffff...] - 库存不足!商品 ITEM-999 缺货 [INFO] [a1b2c3d4-ffff...] - 订单已取消 [INFO] [550e8400-e29b...] - 库存充足,开始扣款... [INFO] [550e8400-e29b...] - 扣款成功: User=Alice, Amount=$100 ``` 虽然行还是交错的,但我们可以清楚地看到 ID `a1b2c3d4...` (Bob) 对应的是 **ERROR**,而 ID `550e8400...` (Alice) 对应的是 **扣款成功**。 --- ### 调试:如何追查问题 (Audit) 当生产环境出现问题时,正确的排查姿势如下: #### 场景:客服转来一个工单,用户 Bob 说 10:00 左右下单失败了。 **1. 模糊搜索定位 TraceID** 你不知道 TraceID,但知道时间和用户。 使用 `grep` 或 SQL: ```sql -- 查找 Bob 在那个时间点的第一条日志,为了拿 TraceID SELECT trace_id, message, created_at FROM app_logs WHERE message LIKE '%Bob%' AND created_at BETWEEN '2026-01-22 10:00:00' AND '2026-01-22 10:05:00' LIMIT 1; ``` **结果**:拿到 TraceID = `a1b2c3d4-ffff...` **2. 全链路还原 (The Magic Query)** 拿着这个 ID,查询该请求的一生。 ```sql SELECT created_at, log_level, module, message FROM app_logs WHERE trace_id = 'a1b2c3d4-ffff-...' ORDER BY created_at ASC; ``` **还原出的故事:** 1. `10:00:01` [INFO] 收到请求 (module: `mock_http_handler`) 2. `10:00:02` [INFO] 检查库存 ITEM-999 (module: `process_order`) 3. `10:00:03` [ERROR] **库存不足!** (module: `process_order`) <--- **找到原因了!** 4. `10:00:03` [INFO] 订单取消 **结论**:你可以自信地回复工单:“经排查,用户下单失败是因为商品 ITEM-999 此时没有库存了,系统正确拦截,未扣款。” --- ### 总结:TraceID 的三大好处 1. **解耦并发**:在高并发系统中(每秒几千个请求),TraceID 是唯一能把属于同一个请求的日志“聚合”在一起的键。 2. **跨越边界**:虽然本例只演示了一个服务,但 TraceID 通常会通过 HTTP Header 传给下游服务(Payment Service, Stock Service)。这样你在 Kibana/Grafana 里搜一个 TraceID,能看到跨越 5 个微服务的完整调用链。 3. **无侵入开发**:你的开发人员不需要在写 `payment_service` 时手动传递 `trace_id` 参数,他们只需要写 `log_info!`,库会自动完成剩下的工作。
## ⚙️ 高级配置 ### 数据库分区策略 * 库会自动检测当前月份,若 `app_logs_YYYY_MM` 表不存在,则自动创建。 * 无需 DBA 手动维护未来的分区表。 ### 文件轮转策略 * **按大小**:单文件超过 100MB 自动重命名归档。 * **按日期**:跨天后自动创建新日期的文件。 * **文件命名**:`{service_name}-{yyyy-mm-dd}.log` ## 🤝 贡献指南 1. Clone 本仓库。 2. 在 `tests/` 目录下编写集成测试。 3. 提交 Pull Request 前请运行 `cargo test` 确保所有测试通过。
👉 点击查看系统架构 UML 图 ## 🏗️ 架构设计 本库采用 **异步事件驱动 (Async Event-driven)** 架构,基于 **Active Object** 模式设计。 业务线程仅负责将日志通过 `MPSC Channel` 发送,由独立的后台协程负责实际的 I/O 写入,从而实现 **Zero-Blocking**(零阻塞)的业务性能。 ```mermaid graph TD A[Start] --> B[End] ```