Using tower-http Middleware - Logging

1. Welcome and Icebreaker:

Have you ever debugged a bug that only happened in production? How did you find it?

Here’s a fun question to start: If you could log any real-world event (like a coffee spill or a cat walking on your keyboard), what would you want to see in your logs?

2. Outline the Session:

Agenda:

  1. Concept Explanation: Logging requests in Axum using tower-http.
  2. Live Coding: Add request logging middleware to your app.
  3. Tasks: Practice by customizing logging output.

Learning Outcomes:

  • Use tower-http’s logging middleware in Axum.
  • Configure and customize request logging.
  • Understand how to filter, format, and direct log output.

Expectations: Try changing the log format and see what information you can capture! Ask questions and experiment with the code.

3. Concept Explanation:

Logging is essential for monitoring, debugging, and maintaining web applications. In Rust’s Axum framework, the tower-http crate provides middleware such as TraceLayer for structured request and response logging. This middleware automatically logs details about each HTTP request and response, including method, path, status code, latency, and more.

To control how logs are output, you typically use the tracing ecosystem, especially the tracing-subscriber crate. This allows you to:

  • Set log levels (e.g., info, warn, error)
  • Format logs (e.g., JSON, pretty, with custom fields)
  • Filter logs by target or level
  • Direct logs to files or external systems

Why is this important?

  • Helps you understand how your app behaves in production.
  • Makes debugging easier by providing context for errors.
  • Enables monitoring and alerting based on log data.

Key Concepts:

  • Middleware: Software that sits between the request and your handler, able to log, modify, or reject requests.
  • Structured Logging: Logs with key-value pairs, making them easier to search and analyze.
  • Log Levels: Control verbosity (trace, debug, info, warn, error).

4. Live Coding Demonstration:

Add tower-http = { version = "0.6", features = ["trace"] } and tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] } alongside Axum 0.8 and Tokio 1. TraceLayer emits events at DEBUG by default, so enable the appropriate target/level to see them.

use axum::{Router, routing::get};
use tower_http::trace::TraceLayer;
use tracing_subscriber::EnvFilter;

async fn hello() -> &'static str {
    "Hello, Axum!"
}

#[tokio::main]
async fn main() {
    // Initialize tracing subscriber with custom log level and format
    tracing_subscriber::fmt()
        .with_env_filter(EnvFilter::new("warn,tower_http=debug"))
        .json() // Output logs as JSON (optional)
        .init();

    let app = Router::new()
        .route("/", get(hello))
        .layer(TraceLayer::new_for_http());
    let listener = tokio::net::TcpListener::bind("127.0.0.1:3000").await.unwrap();
    axum::serve(listener, app).await.unwrap();
}

Step-by-step explanation:

  • tracing_subscriber::fmt() sets up the logger.
  • .with_env_filter(EnvFilter::new("warn,tower_http=debug")) shows warnings globally and DEBUG events from tower_http, including the default TraceLayer events.
  • .json() outputs logs in JSON format (remove this for pretty text output).
  • TraceLayer::new_for_http() adds request/response logging middleware.

Custom Fields Example: You can add custom fields (like request ID) using tracing spans or layers. For example:

// Configure TraceLayer::make_span_with or use a handler's tracing span to
// record safe request metadata. Do not log Authorization or cookie headers.

Logging to a File: To log to a file, use the tracing-appender crate:

use tracing_appender::rolling;
// In main(), use a file writer instead of the console subscriber above:
let file_appender = rolling::daily("logs", "server.log");
tracing_subscriber::fmt()
    .with_writer(file_appender)
    .init();

Best Practices:

  • Use structured logging for better search and filtering.
  • Log both requests and responses for full visibility.
  • Avoid logging sensitive information (e.g., passwords, tokens).
  • Set appropriate log levels in production.

Common Pitfalls:

  • Forgetting to initialize the logger (no logs will appear).
  • Logging sensitive information by accident.
  • Overly verbose logs can make it hard to find important events.

5. Tasks

  • Change the log level to only show warnings and errors (try adjusting EnvFilter).
  • Add custom fields to your log output (e.g., user ID, request ID) using tracing spans.
  • Try logging to a file instead of the console using tracing-appender.
  • Experiment with log formats: try both pretty and JSON output.
  • Observe what happens if you forget to initialize the logger.