结构化日志不是把 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(),否则断言日志时什么也拿不到。

日志是写给三年后的自己看的数据库,字段就是它的列。

← 返回文章列表

评论

…