Log Levels and What to Log in a Rust Service Explained
Choose log levels by who reads them and when: what belongs at error, warn, info, debug and trace in a Rust service, what never to log, and how much is too much.

Logs are written by the person who knows the code and read by someone at 3 a.m. who does not. Levels are the contract between them. Everything applies to Rust 1.90 with tracing; the same guidance holds with log.
What each level means
| Level | Use it for | Example |
|---|---|---|
error | an operation failed and someone should look | payment provider returned 500 after retries |
warn | unexpected, handled, worth watching | retrying after timeout, deprecated header used |
info | normal milestones | server listening, request completed, job finished |
debug | details for diagnosing | SQL parameters count, cache miss for key class |
trace | step by step | each loop iteration, bytes read |
The test for error is whether you would want an alert. A user typing a wrong password is not an error; it is a normal event, at info or debug, or a metric. A database being unreachable is an error. A background job retrying is a warn, until it gives up, which is an error.
One line per request
use tracing::info;
fn log_request(method: &str, path: &str, status: u16, ms: u64) {
info!(method, path, status, duration_ms = ms, "request completed");
}Fields are better than interpolating into the message, because a log system can filter status >= 500 or average duration_ms. Keep the message constant ("request completed") so it is groupable.
Log an error once
use tracing::error;
fn load() -> Result<String, std::io::Error> {
std::fs::read_to_string("/etc/app.toml")
}
fn handle() -> Result<(), String> {
let _cfg = load().map_err(|e| format!("loading config: {e}"))?;
Ok(())
}
fn main() {
tracing_subscriber::fmt::init();
if let Err(e) = handle() {
error!(error = %e, "request failed");
}
}Lower layers return the error with context; the top layer logs it with the full chain. If every layer logs and returns, one failure yields five lines and the count in your error graph is wrong. Context chains are in error handling.
What never goes in a log
- Passwords, tokens, API keys, session ids, authorization headers.
- Full request and response bodies, which can contain either of the above.
- Personal data beyond what the debugging needs (email addresses, addresses, payment details).
- Anything protected by regulation without a retention plan.
Techniques for redaction are in logging without leaking secrets. A Debug derive on a struct with a password field prints it; wrap secrets in a type whose Debug prints a placeholder (secrecy).
Volume
Logs cost storage and attention. A loop that logs per iteration at info produces a million lines an hour. Remedies:
- Move per-item detail to
debugand keep a counter atinfo. - Log the first occurrence and then a periodic summary of repeats.
- Use metrics for rates and distributions (Prometheus metrics) and logs for individual events.
- Filter per module at run time with
RUST_LOG, so debugging does not need a deploy.
Make it searchable
Use consistent field names across the codebase (user_id, not uid in one place and userId in another). Include a request or trace id on every line through a span (request ids). Output JSON in production so tools index fields, and human readable text locally.
A review checklist
1. Would anyone act on this error? 2. Is the message constant, with variable data in fields? 3. Is this logged once? 4. Could this line contain a secret or personal data? 5. How many lines will this produce per minute at peak?
The setup of the library itself is in tracing spans and events.


