13 KiB
🚀 Rust Logger
Rust Logger 是一个专为微服务架构设计的高性能、异步、可扩展的 Rust 日志库。
它基于 Tokio 运行时,支持多输出源(控制台、文件、PostgreSQL),内置分布式追踪(TraceID)支持,并提供自动化的日志轮转与过期清理机制。
✨ 核心特性
- ⚡ 全异步非阻塞:使用
tokio和channel异步写入,确保业务主线程不受 I/O 阻塞。 - 🗄️ PostgreSQL 集成:
- 支持 自动按月分表(Auto Partitioning),无需人工干预。
- 支持
service_name区分多服务。 - 结构化存储,便于 SQL 审计与分析。
- 📄 文件日志管理:
- 支持 按天 + 按大小 (100MB) 自动轮转切割。
- 支持基于文件名的智能过期删除。
- 🔍 分布式追踪 (TraceID):
- 基于
Task Local的无侵入式上下文传递。 - 全链路日志关联,轻松定位并发请求问题。
- 基于
- 🧹 自动清理 (Retention):
- 后台协程自动清理过期数据库分区和旧日志文件。
📂 项目结构
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 中添加:
[registries.kellnr]
index = "sparse+https://kellnr.shay7sev.site/api/v1/crates/"
2. 添加依赖
在你的 Cargo.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 输出前,请先创建基础表结构:
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
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 包裹逻辑。
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!("正在扣款...");
}
日志输出示例:
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:
[dependencies]
# ... 其他依赖
uuid = { version = "1.0", features = ["v4"] }
main.rs: 在这个例子中,我们模拟两个并发请求同时进入系统。
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 (灾难现场)
在控制台或文件中,由于是异步并发,日志会是时间交错的。你看到的可能是这样:
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,或者查看数据库。
实际输出:
[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:
-- 查找 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,查询该请求的一生。
SELECT created_at, log_level, module, message
FROM app_logs
WHERE trace_id = 'a1b2c3d4-ffff-...'
ORDER BY created_at ASC;
还原出的故事:
10:00:01[INFO] 收到请求 (module:mock_http_handler)10:00:02[INFO] 检查库存 ITEM-999 (module:process_order)10:00:03[ERROR] 库存不足! (module:process_order) <--- 找到原因了!10:00:03[INFO] 订单取消
结论:你可以自信地回复工单:“经排查,用户下单失败是因为商品 ITEM-999 此时没有库存了,系统正确拦截,未扣款。”
总结:TraceID 的三大好处
- 解耦并发:在高并发系统中(每秒几千个请求),TraceID 是唯一能把属于同一个请求的日志“聚合”在一起的键。
- 跨越边界:虽然本例只演示了一个服务,但 TraceID 通常会通过 HTTP Header 传给下游服务(Payment Service, Stock Service)。这样你在 Kibana/Grafana 里搜一个 TraceID,能看到跨越 5 个微服务的完整调用链。
- 无侵入开发:你的开发人员不需要在写
payment_service时手动传递trace_id参数,他们只需要写log_info!,库会自动完成剩下的工作。
⚙️ 高级配置
数据库分区策略
- 库会自动检测当前月份,若
app_logs_YYYY_MM表不存在,则自动创建。 - 无需 DBA 手动维护未来的分区表。
文件轮转策略
- 按大小:单文件超过 100MB 自动重命名归档。
- 按日期:跨天后自动创建新日期的文件。
- 文件命名:
{service_name}-{yyyy-mm-dd}.log
🤝 贡献指南
- Clone 本仓库。
- 在
tests/目录下编写集成测试。 - 提交 Pull Request 前请运行
cargo test确保所有测试通过。
👉 点击查看系统架构 UML 图
🏗️ 架构设计
本库采用 异步事件驱动 (Async Event-driven) 架构,基于 Active Object 模式设计。
业务线程仅负责将日志通过 MPSC Channel 发送,由独立的后台协程负责实际的 I/O 写入,从而实现 Zero-Blocking(零阻塞)的业务性能。
graph TD
A[Start] --> B[End]