3 minute read

Summary

Plain string logs become hard to follow once a request crosses async work. Spans let the service record scope and relationships.

Request logs should keep field names such as method, path, status, latency, and requestId consistent so they can be searched and aggregated. Headers, tokens, passwords, and full request bodies should be excluded from default log fields.

Curriculum Position

  • Series: Rust Service to Production
  • Previous post: Separating config files and environment variables
  • Next post: Handling validation and bad requests
  • Expansion criteria: before publication, add the example repository, commands, tool versions, and failure logs that fit this post’s scope.

Document Info / Environment

  • Written date: 2026-05-04
  • Verification date: 2026-05-05
  • Document type: tutorial
  • Test environment: No direct execution test. The code below is a design target to verify in the example repository by checking request log fields and sensitive-data exclusion.
  • Tested versions: No runtime versions pinned. On the verification date, this was checked against tracing, tracing-subscriber 0.3.23, and tower-http 0.6.8 documentation.
  • Evidence level: official documentation, original project documentation

Problem Statement

When an API runs asynchronously, one request can cross several functions and await points. Plain string logs make it hard to connect those events back to one request.

This post covers structured logging with tracing. The goal is not prettier log text. The goal is request-scoped fields and spans that can be searched in operation.

Verified Facts

  • tracing documentation describes spans as periods of time with a beginning and end that represent execution context. Events represent points in time and may occur inside spans. Evidence: tracing crate documentation
  • tracing documentation describes structured fields on spans and events using field_name = field_value. That lets the service record method, path, and status_code as fields instead of burying them in strings. Evidence: tracing crate documentation
  • tracing-subscriber fmt documentation provides a subscriber that records spans and events to stdout, and it supports runtime filtering through RUST_LOG with EnvFilter. Evidence: tracing-subscriber fmt
  • tracing-subscriber formatter documentation describes JSON output as newline-delimited JSON logs intended for production systems that consume structured logs as JSON. Evidence: tracing-subscriber fmt
  • tower-http TraceLayer documentation describes middleware that adds high-level tracing to an HTTP service and can record latency and response status in callbacks. Evidence: tower-http TraceLayer

Reproduction Steps

There is no direct execution result yet. Before publication, add tracing-subscriber and TraceLayer to the minimal API example and record the fields produced by one request.

cargo add tracing
cargo add tracing-subscriber --features env-filter,json
cargo add tower-http --features trace

Initialize tracing at startup.

use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter};

fn init_tracing() {
    let filter = EnvFilter::try_from_default_env()
        .unwrap_or_else(|_| EnvFilter::new("info,tower_http=info"));

    tracing_subscriber::registry()
        .with(filter)
        .with(tracing_subscriber::fmt::layer().json())
        .init();
}

Attach an HTTP request tracing layer to the router.

use axum::Router;
use tower_http::trace::TraceLayer;

fn build_router() -> Router {
    Router::new()
        // routes...
        .layer(TraceLayer::new_for_http())
}

Before publication, verify with:

$env:RUST_LOG="info,tower_http=trace"
cargo run
curl.exe http://127.0.0.1:3000/health

Observations

  • This document currently contains no actual log output.
  • The success condition is that one request produces searchable fields for method, path, status, and latency.
  • Failure conditions include logging authorization, cookie, set-cookie, passwords, tokens, or entire request bodies by default.

Verification Checklist

  • Are log field names consistent across requests?
  • Can log verbosity be changed with RUST_LOG?
  • Can request start and response completion be connected through the same span or matching fields?
  • Are status and latency recorded as fields instead of only in a message string?
  • Are headers and bodies not logged wholesale?
  • Is there a plan to connect request ids or trace ids between error responses and logs?

Interpretation

Operational logs are both text for people and data for systems. A message like request finished is readable, but it is weak for status aggregation or latency search.

Logging every value is risky in the other direction. Headers and bodies may contain secrets or personal data, so default fields should start with the operational minimum: method, path template, status, latency, and request id.

Limitations

  • This post does not yet include actual log output or executed command results.
  • The example is the beginning of structured logging; OpenTelemetry trace export and metrics export are handled in later observability posts.
  • TraceLayer::new_for_http() defaults should not be assumed to satisfy every organization’s log schema.
  • Header logging is excluded by default and should be added narrowly with an allowlist when needed.
  • Before publication, add an example repository, commands, versions, and failure logs.

References

Change Log

  • 2026-05-04: Initial Rust Service to Production curriculum draft.
  • 2026-05-05: Separated tracing span/event evidence, JSON formatter, TraceLayer scope, and sensitive logging limitations.

댓글남기기