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?
-
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. -
axum::Server
: This is how we run the server on127.0.0.1:3000
, making the app accessible in the browser or via HTTP requests. -
#[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 withnext.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 thetracing
crate that logs messages at the “info” level. You can use different levels likedebug!
,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 usingtracing
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!