结构化日志不是把 println 换成 tracing::info
拼接字符串的日志只能被人眼读,带字段的日志才能被查询和聚合。字段、span、级别、以及别在热点路径上格式化,是四个真正要做的决定。
把 println! 换成 info! 之后,日志还是那行文本,只是多了一个时间戳和级别前缀。这不是结构化日志,只是换了个输出通道。
字段,不是字符串
结构化日志的核心是把值放在具名字段里,让机器能提取:
// 拼接字符串:日志系统只收到一行文本
info!("user {} logged in from {}", user, ip);
// 带字段:JSON 层把它们变成可查询的键
info!(user = %user, ip = %ip, "login");
第二种写法在 JSON 输出下是 {"user":"...","ip":"...","message":"login"};第一种只有 message,用户和 IP 粘在字符串里,想按用户聚合就得写正则。
span 负责「在做什么」,字段负责「是谁」
#[instrument] 给函数自动建一个 span,并把参数记进去。省事,但有两件事要注意:
#[instrument(skip(db), fields(order_id = %id))]
async fn charge(db: &Db, id: OrderId, amount: u64) -> Result<Receipt> {
// ...
}
skip(db)是必须的:连接池、请求体、大结构体不该进日志,Debug输出既慢又可能泄露fields(...)是补救:不记录参数,就记录你真正想要的那个标识符
级别是有语义的,不是语气
| 级别 | 含义 |
|---|---|
error |
需要人处理,或已经丢了数据 |
warn |
自动恢复了,但值得看一眼 |
info |
业务事件(登录、下单、部署) |
debug |
排障时想看到的状态 |
最常见的误用是把可恢复的情况写成 error,于是真的出错时没人在看。
代价与开关
字段是惰性的,级别没开时不会被格式化 —— 这是用 tracing 而不是自己拼字符串的主要收益之一。但要注意 debug!(...) 里如果调用了昂贵的函数(serde_json::to_string),参数表达式仍会在启用时求值,那就得用 enabled!(Level::DEBUG) 显式判断。
生产用 JSON、本地用带颜色的 pretty,由环境变量切换;测试里用 fmt().with_test_writer(),否则断言日志时什么也拿不到。
日志是写给三年后的自己看的数据库,字段就是它的列。

评论
…