Files
rust_logger/README.md
2026-01-23 10:48:36 +08:00

450 lines
15 KiB
Markdown
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.

# 🚀 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...] - 请求处理完成
```
<details>
### 实例:编写业务代码 (`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!`库会自动完成剩下的工作
</details>
## ⚙️ 高级配置
### 数据库分区策略
* 库会自动检测当前月份,若 `app_logs_YYYY_MM` 表不存在,则自动创建。
* 无需 DBA 手动维护未来的分区表。
### 文件轮转策略
* **按大小**:单文件超过 100MB 自动重命名归档。
* **按日期**:跨天后自动创建新日期的文件。
* **文件命名**`{service_name}-{yyyy-mm-dd}.log`
## 🤝 贡献指南
1. Clone 本仓库。
2.`tests/` 目录下编写集成测试。
3. 提交 Pull Request 前请运行 `cargo test` 确保所有测试通过。
## 🏗️ 架构设计
本库采用 **异步事件驱动 (Async Event-driven)** 架构,基于 **Active Object** 模式设计。
业务线程仅负责将日志通过 `MPSC Channel` 发送,由独立的后台协程负责实际的 I/O 写入,从而实现 **Zero-Blocking**(零阻塞)的业务性能。
```mermaid
classDiagram
class LogLevel {
<<enumeration>>
DEBUG
INFO
WARNING
ERROR
FATAL
}
class LogRecord {
+String service_name
+DateTime timestamp
+LogLevel level
+String message
+String module
+Option<String> trace_id
}
%% 接口定义:强调 Send + Sync 约束
class LogOutput {
<<interface>>
<<Send + Sync>>
+write(record: LogRecord) Future
}
%% 具体实现
class PostgresOutput {
-PgPool pool
+write(record)
}
class ConsoleOutput {
+write(record)
}
%% 业务线程持有的 Logger (Producer)
class Logger {
<<Thread-Safe>>
<<Shared via Arc>>
-mpsc::Sender~LogRecord~ tx
-LogLevel min_level
+log(level, msg)
}
%% 后台异步任务 (Consumer)
class BackgroundWorker {
<<Active Object>>
<<Running in tokio::spawn>>
-mpsc::Receiver~LogRecord~ rx
-Vec~Box~LogOutput~~ outputs
+run()
}
%% 关系描述
LogOutput <|.. PostgresOutput
LogOutput <|.. ConsoleOutput
%% 关键的线程安全机制MPSC Channel
Logger "1" o-- "1" `mpsc::Sender` : Owns
BackgroundWorker "1" o-- "1" `mpsc::Receiver` : Owns
%% 逻辑流
ClientThread ..> Logger : 1. Calls log() (Non-blocking)
Logger ..> `mpsc::Sender` : 2. Sends Record
`mpsc::Sender` ..> `mpsc::Receiver` : 3. Channel Transfer (Thread-Safe)
`mpsc::Receiver` ..> BackgroundWorker : 4. Receives Record
BackgroundWorker --> LogOutput : 5. Serialized Writes
```