RustArena

Logging, tracing and configuration

It's 3 a.m., orders are failing in production, and all you have is what the service wrote down. Was it one customer or all of them? Which attempt failed? Code that's easy to operate answers those questions from its logs, and refuses to start with a bad setting instead of misbehaving for hours.

orderd processes a batch of orders: it prices them, rejects suspicious ones and charges customers, retrying when the payment gateway times out. The logic works. The logging is println! debugging, and there's no configuration at all. You'll fix both.

You'll practice: tracing events, spans and fields, #[instrument], tracing-subscriber with EnvFilter and JSON output, layered configuration with serde and TOML, validation, and testing what your code logs.

Background

Events, spans and fields

tracing is the standard for logging in Rust (Tokio, axum, SQLx and most big crates use it). It has two building blocks:

Fields make logs structured: key = value pairs that a machine can filter on (order_id=4) instead of words buried in a sentence. %x records a value with Display, ?x with Debug, and a bare total_cents uses the variable of that name.

#[instrument(skip_all, fields(order_id = order.id, customer = %order.customer))]
pub fn process_order(order: &Order, config: &Config) -> Outcome { ... }

let result = info_span!("charge", attempt).in_scope(|| gateway_charge(customer, attempt));
warn!(error = %err, "payment attempt failed");

#[instrument] wraps a function in a span named after it. By default it records every argument; skip_all plus explicit fields(...) keeps big or secret values (passwords, whole configs) out of your logs.

Pick levels the way teams do: ERROR means someone should look, WARN means unexpected but handled, INFO marks normal business milestones, DEBUG is detail for developers, and TRACE is everything.

Subscribers: where logs go

tracing only emits; a subscriber decides what to keep and how to write it. With tracing-subscriber you stack layers on a registry:

Logs go to stderr; stdout is for the program's real output (here, the summary line), so orderd > summary.txt works. Turn color codes off when stderr isn't a terminal, or your log files fill with \x1b[32m.

Configuration

Settings come in layers, each overriding the one before: defaults, then a TOML file (ORDERD_CONFIG names it), then environment variables (how containers and CI pass settings). The result is checked once, at startup, and turned into a typed Config. If you hold a Config, every value in it is valid, and nobody re-checks it deep in the code.

Testing logs

tests/telemetry.rs installs a small Layer that records every event with the fields of its spans, runs the batch, and checks the result. Read it: it's how you'd test logging in a real codebase.

The logging contract

What Level Fields Where
span process_batch service, orders (count) process_batch
span process_order order_id, customer process_order
span charge attempt one per payment attempt
event "priced line" DEBUG sku, quantity, line_cents each line of an order
event "order rejected" WARN reason empty or over-limit orders
event "payment attempt failed" WARN error inside its charge span
event "order accepted" INFO total_cents
event "payment failed" ERROR error after the last attempt, or a permanent error
event "batch finished" INFO accepted, rejected, failed, revenue_cents end of the batch

Your tasks

  1. Configuration. Implement from_sources in src/config.rs (the doc comment lists every key, variable and rule). Run cargo test --test config.
  2. Subscriber. Implement telemetry::init in src/telemetry.rs: an EnvFilter defaulting to info, then a text or JSON fmt layer writing to stderr.
  3. Instrumentation. In src/orders.rs, replace every println!/eprintln! with the spans and events in the table. Run cargo test --test telemetry.
  4. Run it every way an operator would:
cargo run
RUST_LOG=debug cargo run
RUST_LOG=warn ORDERD_LOG_FORMAT=json cargo run
RUST_LOG=info,orderd::orders=debug cargo run
ORDERD_CONFIG=config/orderd.toml ORDERD_PAYMENT_MAX_ATTEMPTS=2 cargo run
ORDERD_PAYMENT_MAX_ATTEMPTS=0 cargo run; echo "exit code $?"
cargo test
bash ../../tools/grade.sh 10-observability

What the grader checks

Hints

Going further