Structured logging is not println replaced by tracing::info
Log lines built by concatenation can only be read by humans. Fields make them queryable. Fields, spans, levels and knowing when not to format are the four real decisions.
Replace println! with info! and you still have a line of text, now with a timestamp and a level. That is not structured logging; it is a different output channel.
Fields, not strings
The point of structured logging is putting values into named fields so machines can extract them:
// 拼接字符串:日志系统只收到一行文本
info!("user {} logged in from {}", user, ip);
// 带字段:JSON 层把它们变成可查询的键
info!(user = %user, ip = %ip, "login");
The second form serialises to {"user":"...","ip":"...","message":"login"}. The first has only message, with the user and IP glued into it, so grouping by user means writing a regular expression over your logs.
Spans say what, fields say who
#[instrument] builds a span for the function and records its arguments. Convenient, with two caveats:
#[instrument(skip(db), fields(order_id = %id))]
async fn charge(db: &Db, id: OrderId, amount: u64) -> Result<Receipt> {
// ...
}
skip(db)is mandatory: connection pools, request bodies and large structs do not belong in logs.Debugoutput is slow and can leakfields(...)is the remedy: if you skip the arguments, record the one identifier you actually want
Levels carry meaning, not tone
| Level | Meaning |
|---|---|
error |
A human must act, or data was lost |
warn |
Recovered automatically, but worth a look |
info |
Business events: login, order, deploy |
debug |
State you want while troubleshooting |
The most common misuse is filing recoverable conditions under error, after which nobody watches the errors.
The cost, and the switch
Fields are lazy: they are not formatted when the level is off. That is one of the main reasons to use tracing instead of hand-built strings. But if a debug! call takes an expensive argument such as serde_json::to_string, the expression is still evaluated when the level is enabled, and you need an explicit enabled!(Level::DEBUG) guard.
JSON in production, coloured pretty output locally, switched by an environment variable. In tests, fmt().with_test_writer(), or asserting on log output collects nothing.
Logs are a database written for your future self. Fields are its columns.

Comments
…