日志系统
大约 6 分钟
日志系统 (tracing
)
一个健壮的日志系统是可观测性 (Observability) 的基石,对于应用的调试、性能监控和事后审计至关重要。我们的项目采用 Rust 生态中最先进的 tracing
框架来构建一个高性能、结构化、可配置的日志系统。
本文档将详细解析我们日志系统的设计理念、核心特性以及如何在代码中正确地使用它。
核心设计哲学
我们的日志系统遵循以下几个核心设计原则:
- 结构化 (Structured): 我们记录的不是简单的文本字符串,而是包含丰富上下文的 结构化事件。这使得日志更易于被机器解析、查询和分析(例如,通过 ELK Stack 或 Datadog)。
- 高性能 (High-Performance): 日志记录操作 绝不能 阻塞我们核心的应用逻辑。我们采用 异步、非阻塞 的 I/O 策略,确保在高并发下日志写入不会成为性能瓶颈。
- 可配置 (Configurable): 日志的级别、输出位置等所有行为都应通过配置文件进行管理,甚至可以在运行时通过环境变量进行动态调整,而无需修改代码。
- 开发者友好 (Developer-Friendly): 在开发环境中,日志应以易于阅读的、美化的格式输出到控制台;在生产环境中,则应以紧凑的、机器友好的格式写入文件。
核心特性详解
1. 结构化日志 (tracing
)
我们使用 tracing
替代了传统的 log
crate。tracing
允许我们记录带有键值对上下文的事件 (Events) 和跨越异步边界的跨度 (Spans)。
示例:
// 简单的信息日志
tracing::info!("用户登录成功");
// 带有结构化上下文的日志
tracing::info!(
user_id = user.id,
username = user.username,
ip_address = %client_ip,
"用户登录成功"
);
第二种方式记录的日志在后台可能看起来像这样(JSON 格式): {"timestamp":"...","level":"INFO","fields":{"user_id":123,"username":"alice","ip_address":"127.0.0.1"},"message":"用户登录成功"}
2. 双重输出:美化控制台与紧凑文件
我们的日志会同时输出到两个目的地,并采用不同的格式化策略:
控制台 (stdout):
- 格式: 使用
.pretty()
进行美化,带有颜色、多行显示,非常适合在本地开发时阅读。 - 目的: 方便开发者实时调试。
- 格式: 使用
日志文件:
- 格式: 使用
.compact()
生成紧凑的单行格式(通常是类 JSON 格式),不含颜色代码。 - 目的: 方便日志采集系统(如 Filebeat)进行收集和解析。
- 格式: 使用
3. 非阻塞 (异步) 日志写入
这是保障高性能的关键。我们使用 tracing_appender::non_blocking
将文件和控制台的写入操作包装起来。
- 工作原理: 当应用代码调用
tracing::info!
时,日志消息并不会直接写入文件或控制台。相反,它会被快速地发送到一个内存中的通道 (channel),然后应用代码立即继续执行。一个专门的后台工作线程会负责从这个通道中取出消息并执行实际的 I/O 写入操作。 - 优势: 彻底将耗时的 I/O 操作与我们对性能敏感的 Web 请求处理线程分离开来,避免了因为磁盘慢或控制台刷新延迟而导致 API 响应变慢。
4. 滚动日志文件 (rolling::hourly
)
为了防止单个日志文件无限增长导致磁盘空间耗尽和管理困难,我们采用了 按小时滚动 的策略。
- 行为:
tracing_appender::rolling::hourly
会自动根据当前时间创建日志文件。例如,日志会被写入到app.log.2023-10-28-10
、app.log.2023-10-28-11
这样的文件中。 - 优势: 便于日志归档、按时间范围查询和自动清理旧日志。
5. 动态日志级别 (EnvFilter
)
我们通过 EnvFilter
实现了两级日志级别控制:
- 基础级别 (From Config): 在
config.toml
中可以配置一个全局的基础日志级别(如INFO
)。应用启动时默认会使用这个级别。 - 运行时覆盖 (From Environment Variable): 你可以通过设置
RUST_LOG
环境变量来 临时覆盖 配置中的级别,而 无需重启应用。这在生产环境中排查特定模块的问题时极为有用。RUST_LOG=debug
: 将所有日志级别提升到DEBUG
。RUST_LOG=my_app::service=trace
: 只将my_app::service
模块的日志级别提升到最详细的TRACE
,而其他模块保持默认。
如何配置
所有日志相关的配置都在 config/config.toml
文件中的 [logger]
部分:
[logger]
# 日志级别: Trace, Debug, Info, Warn, Error
level = "Info"
# 日志文件存放目录
log_dir = "logs"
# 日志文件名前缀
file_name = "app.log"
如何在代码中使用
在应用的任何地方,你都可以使用 tracing
提供的宏来记录日志。
// 记录一个普通的信息事件
tracing::info!("开始处理订单创建请求");
// 记录一个带有上下文的警告
tracing::warn!(order_id = 12345, error = "库存不足", "创建订单失败");
// 在一个函数或代码块的开始创建一个 Span,它会自动记录进入和离开的时间
#[tracing::instrument]
async fn process_payment(amount: u64) {
tracing::debug!("正在处理支付...");
// ... 逻辑 ...
tracing::info!("支付处理完成");
}
// 在 Result 返回 Err 时记录错误
match result {
Ok(data) => Ok(data),
Err(e) => {
// 使用 error! 记录严重错误
tracing::error!(error = ?e, "一个无法恢复的错误发生");
Err(e)
}
}
最佳实践
- 使用结构化字段: 尽量为你的日志事件添加有意义的键值对字段,而不是将所有信息都塞进消息字符串里。
- 选择合适的级别:
ERROR
: 用于发生了严重错误,程序可能无法继续正常运行的情况。WARN
: 用于发生了预期之外的情况,但程序仍能继续处理的情况。INFO
: 用于记录应用生命周期中的重要事件,如启动、重要业务流程的完成。DEBUG
: 用于开发时调试代码逻辑。TRACE
: 用于最详细的、深入到函数级别的调试信息。
- 利用
instrument
: 对于重要的函数,特别是异步函数,使用#[tracing::instrument]
宏可以非常方便地追踪其执行流程和耗时。