How to Use Axum Middleware for Logging in Rust Web Apps

In web development, logging plays a crucial role in tracking application behavior, detecting issues, and analyzing performance. In this tutorial, we’ll explore how to leverage Axum (a fast and lightweight web framework for Rust) to create a middleware that handles logging in your app. This will allow you to see logs directly in your terminal for real-time feedback.

Why use middleware for logging?

Middleware in Axum is a great way to handle tasks like logging because it intercepts HTTP requests and responses, making it an ideal point to log information about incoming requests, outgoing responses, and even errors. This way, you can efficiently track the flow of your application without cluttering up the core logic.

In this article, we’ll go through a hands-on example, gradually building up a simple web server, and adding logging step-by-step.


Step 1: Set Up Your Project

Let’s start by creating a new Rust project.

cargo new axum_logging_example
cd axum_logging_example

Add Dependencies

In your Cargo.toml, add the necessary dependencies for Axum and Tokio (for asynchronous runtime) along with tracing and tracing-subscriber for logging.

[dependencies]
axum = "0.6"
tokio = { version = "1", features = ["full"] }
tracing = "0.1"
tracing-subscriber = "0.3"

After this, run cargo build to fetch the dependencies.


Step 2: Basic Axum Setup

Let’s create a minimal Axum application. In your src/main.rs file, write the following code:

use axum::{Router, routing::get};

#[tokio::main]
async fn main() {
    // Set up routing
    let app = Router::new().route("/", get(root));

    // Run the server on localhost:3000
    axum::Server::bind(&"127.0.0.1:3000".parse().unwrap())
        .serve(app.into_make_service())
        .await
        .unwrap();
}

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

What’s happening here?

  1. axum::Router: This is the core component that defines the routes for your web app. In this case, we’re just setting up a single route that responds to GET requests at / and returns a simple string.

  2. axum::Server: This is how we run the server on 127.0.0.1:3000, making the app accessible in the browser or via HTTP requests.

  3. #[tokio::main]: Since Axum is asynchronous, we need to use Tokio to handle asynchronous execution.

Running the Code

Now, run the server using cargo run:

cargo run

Go to http://127.0.0.1:3000/ in your browser, and you should see Hello, Axum!.


Step 3: Add Logging Middleware

Now let’s add logging to the application using Axum’s middleware. We’ll use the tracing crate to log every incoming request. Let’s modify the main.rs file:

use axum::{Router, routing::get, middleware};
use axum::http::Request;
use tracing::{info, Level};
use tracing_subscriber;

#[tokio::main]
async fn main() {
    // Initialize tracing subscriber for logging
    tracing_subscriber::fmt::init();

    // Set up routing with middleware
    let app = Router::new()
        .route("/", get(root))
        .layer(middleware::from_fn(log_request));

    // Run the server on localhost:3000
    axum::Server::bind(&"127.0.0.1:3000".parse().unwrap())
        .serve(app.into_make_service())
        .await
        .unwrap();
}

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

// This function logs incoming requests
async fn log_request<B>(req: Request<B>, next: axum::middleware::Next<B>) -> axum::response::Response {
    // Log the request details
    info!("Incoming request: {} {}", req.method(), req.uri());

    // Continue to the next middleware or handler
    next.run(req).await
}

Explanation of New Concepts

  • Tracing Subscriber: We initialize the tracing_subscriber::fmt::init() to configure how logs are displayed in the terminal. This sets up a default logging configuration to print logs to the terminal.

  • Middleware: The log_request function is where we create our custom middleware. We log every incoming request by extracting the HTTP method and URI, and then we pass the request to the next handler with next.run(req).await.

What’s happening here?

  • middleware::from_fn(log_request): This wraps the request handling with our custom logging function. For every request that hits our server, this middleware will log its method and URI.

  • info!: This is a macro from the tracing crate that logs messages at the “info” level. You can use different levels like debug!, warn!, error!, etc., depending on the severity.

Running the Code

After running cargo run, make a request to http://127.0.0.1:3000/. You should now see a log like this in your terminal:

INFO  axum_logging_example: incoming request: GET / HTTP/1.1

Step 4: Experiment and Extend

Challenge 1: Log HTTP Headers

Try modifying the log_request function to log the headers of the request. This will give you more insight into what’s being sent to your server.

Challenge 2: Add Response Logging

You can also log the response status code before it’s sent back to the client. Here’s an idea on how to do that:

async fn log_request<B>(req: Request<B>, next: axum::middleware::Next<B>) -> axum::response::Response {
    info!("Incoming request: {} {}", req.method(), req.uri());

    // Call the next handler to get the response
    let response = next.run(req).await;

    // Log the response status code
    info!("Response status: {}", response.status());

    response
}

Challenge 3: Use Different Logging Levels

Experiment with different logging levels such as debug!, warn!, and error!. Adjust the logging verbosity in tracing_subscriber::fmt by setting up filtering rules, such as only logging warnings and errors.


Recap and Conclusion

In this article, we:

  • Created a simple Axum application that serves a "Hello, Axum!" message.
  • Added logging middleware to capture and log every incoming request using the tracing crate.
  • Learned how to log request details like method and URI, as well as response status codes.
  • Explored ways to experiment with logging features, such as logging headers or adjusting the verbosity level.

Next Steps

If you're interested in further enhancing this project:

  • Explore Axum’s other middlewares for tasks like authentication or request validation.
  • Look into advanced logging techniques like structured logging with tracing and using tracing to track application performance.
  • Dive deeper into how Axum integrates with Tokio for async processing and error handling.

By integrating middleware into your Axum app, you now have a robust logging system that will help you monitor and debug your applications efficiently!