Skip to content
rusthacks.dev
Tooling

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.

3 min read
Summarize in AI
Flat illustration for the post “Log Levels and What to Log in a Rust Service Explained”, composed around errors.

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

LevelUse it forExample
erroran operation failed and someone should lookpayment provider returned 500 after retries
warnunexpected, handled, worth watchingretrying after timeout, deprecated header used
infonormal milestonesserver listening, request completed, job finished
debugdetails for diagnosingSQL parameters count, cache miss for key class
tracestep by stepeach 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

rust
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

rust
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 debug and keep a counter at info.
  • 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.

Also inWeb
Taggedlogging
Share

Keep reading

Get new posts by email

We send one email when a new post is published. No spam.

Unsubscribe in one click. No spam, ever.