Adding logging and tracing to an Axum App | Rust.
Published: 2023-02-02 17:22:50

---
title: Adding logging and tracing to an Axum App | Rust.
published: true
date: 2023-01-05 19:51:32 UTC
tags: 
canonical_url: https://carlosmv.hashnode.dev/adding-logging-and-tracing-to-an-axum-app-rust
---

DISCLAIMER: This is not a comprehensive article about logging or tracing.

The service we will build is really simple, with an endpoint and with examples showing logs in our command line, JSON logs, tracing, and how to generate log files.

For this task, we will use `Tracing`, `tracing-subscriber`, and `tower-http` packages.

cargo.toml

```
[dependencies]
axum = "0.6.1"
tokio = { version = "1.21.2", features = ["full"] }
tracing = "0.1.37"
tracing-subscriber = { version = "0.3.16", features = ["env-filter"]}
tower-http = { version = "0.3.4", features = ["trace"] }

```

### Logs

Here is a simple example of how to add logging.

main.rs

```
use axum:: {
    routing::{get},
    http::StatusCode,
    Router
};

use std::net::SocketAddr;
use tower_http::trace::{self, TraceLayer};
use tracing::Level;

#[tokio::main]
async fn main() {

    tracing_subscriber::fmt()
        .with_target(false)
        .compact()
        .init();

 let app = Router::new()
        .route("/", get(hello_world))
        .layer(
            TraceLayer::new_for_http()
                .make_span_with(trace::DefaultMakeSpan::new()
                    .level(Level::INFO))
                .on_response(trace::DefaultOnResponse::new()
                    .level(Level::INFO)),

);

    let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
    tracing::info!("listening on {}", addr);

    axum::Server::bind(&addr)
        .serve(app.into_make_service())
        .await
        .unwrap();
}

async fn hello_world() -> &'static str {
    "Hello World!"
}

```

According to the [documentation](https://docs.rs/tracing-subscriber/latest/tracing_subscriber/fmt/index.html), when we use `compact()` the output is optimized for short line lengths. Fields from the current span context are appended to the fields of the formatted event.

If we execute `cargo run` , and go to `locahost:3000`, we should see the following message in our terminal.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1672762084718/01686955-704f-4f37-92d1-46a8a778f926.png)

Now, let me explain a little about the role of Tower-http, Tracing, and Tracing-subscriber.

### Tracing

Tracing is a framework for instrumenting Rust programs to collect structured, event-based diagnostic information.

We can use `tracing` to:

- Emit distributed traces to an Open Telemetry collector.

- Debug your application with Tokio Console.

- Log to `stdout`, a log file or `journald` .

- Profile where your application is spending time.

According to the article [Getting started with Tracing](https://tokio.rs/tokio/topics/tracing), the `tracing` crate provides the API we will use to emit traces. The `tracing-subscriber` crate provides some basic utilities for forwarding those traces to external listeners(e.g., `stdout`).

Also, the same article mentions that _subscribers_ are types that process traces emitted by an application and its dependencies, and can perform metrics, monitoring for errors, and re-emitting traces to the outside world.

In our case, the `tracing` crate collects the events of our application, but we want to emit the traces and logs to our terminal, that is a task for the `tracing-subscriber` crate.

### Tower-http

According to its documentation, `tower-http` is a library that provides HTTP-specific middleware and utilities built on top of `tower`.

The documentation also mentions that all middleware uses the `http` and `http-body` crates as the HTTP abstractions. That means they are compatible with any library or framework that also uses those crates, such as hyper, tonic, and warp.

Axum is built with hyper, so we can use `tower-http` .

This library aims to provide a rich set of middleware for solving common problems when building HTTP clients and servers, like adding high-level tracing and logging to our applications. Also, automatically compress or decompress response bodies and automatically follow redirection responses.

For information about `tower-http`, in its [documentation](https://docs.rs/tower-http/latest/tower_http/) and this [article](https://tokio.rs/blog/2021-05-announcing-tower-http).

Now, coming back to our code.

```
let app = Router::new()
        .route("/", get(hello_world))
        .layer(
            TraceLayer::new_for_http()
                .make_span_with(trace::DefaultMakeSpan::new()
                    .level(Level::INFO))
                .on_response(trace::DefaultOnResponse::new()
                    .level(Level::INFO)),

);

```

Here, we create an instance of a Router. And use the `layer` method, which applies a `tower::Layer` to all routes in the router. It is used to add middleware to the routes.

We passed `TraceLayer` as an argument. Quoting the official documentation:

> `Layer` that adds high level tracing to a `Service`.

The "service" the quote is referring to is a tower service. Also, we use `new_for_http()` , it creates a new `TraceLayer` using `ServerErrorsAsFailures` which supports classifying regular HTTP responses based on the status code, according to the [documentation](https://docs.rs/tower-http/0.3.4/tower_http/trace/struct.TraceLayer.html#method.new_for_http).

To make this work, we have to convert our router instance to a tower service. And we did it when we used `into_make_service()`.

```
 axum::Server::bind(&addr)
        .serve(app.into_make_service())
        .await
        .unwrap();

```

Now we explain what `make_span_with()` and `on_response()` do.

The [documentation](https://docs.rs/tower-http/0.3.4/tower_http/trace/struct.TraceLayer.html#method.on_response) says that `on_response()` customizes what to do when a response has been produced. And `make_span_with()` customizes how to make `Span`s that all request handling will be wrapped in.

To these functions, we passed them the log level we want. Using the `level` method in both, `DefaulMakeSpan` and `DefaultOnResponse`, we set the level to `LEVEL::INFO`. The default is `LEVEL::DEBUG`.

For more details about `DefaultMakeSpan` and `DefaultOnResponse`, [here](https://docs.rs/tower-http/0.3.4/tower_http/trace/struct.DefaultMakeSpan.html) and [here](https://docs.rs/tower-http/0.3.4/tower_http/trace/struct.DefaultOnResponse.html) respectively.

### Pretty

```
#[tokio::main]
async fn main() {

    tracing_subscriber::fmt()
       .with_target(false)
       .pretty()
       .init();

    let app = Router::new()
        .route("/", get(hello_world))
        .layer(
            TraceLayer::new_for_http()
                .make_span_with(trace::DefaultMakeSpan::new().level(Level::INFO))
                .on_response(trace::DefaultOnResponse::new().level(Level::INFO)),
                );

    let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
    tracing::info!("listening on {}", addr);

    axum::Server::bind(&addr)
        .serve(app.into_make_service())
        .await
        .unwrap();
}

```

Pretty emits excessively pretty, multi-line logs, optimized for human readability, according to its documentation. Is intended to be used in local development and debugging, or for command-line applications, where readability and visual appeal are a priority.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1672849850330/0b485371-588b-4c93-bb59-de79402bfe7e.png)

### Json

To add JSON output, we have to modify our `Cargo.toml` file and add the feature `json` to `tracing_subscriber`.

cargo.toml

```
...
tracing-subscriber = { version = "0.3.16", features = ["env-filter", "json"]}
...


#[tokio::main]
async fn main() {

    tracing_subscriber::fmt()
       .with_target(false)
       .json()
       .init();

    let app = Router::new()
        .route("/", get(hello_world))
        .layer(
            TraceLayer::new_for_http()
                .make_span_with(trace::DefaultMakeSpan::new().level(Level::INFO))
                .on_response(trace::DefaultOnResponse::new().level(Level::INFO)),
                );

    let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
    tracing::info!("listening on {}", addr);

    axum::Server::bind(&addr)
        .serve(app.into_make_service())
        .await
        .unwrap();
}

```

Json outputs newline-delimited JSON logs. Is intended for production use with systems where structured logs are consumed as JSON by analysis and viewing tools.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1672850028262/14b32e1e-d191-48c4-9d4c-9be0db313f8e.png)

### Registry

```
...

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

#[tokio::main]
async fn main() {

    tracing_subscriber::registry()
        .with(tracing_subscriber::fmt::layer())
        .init();

    let app = Router::new()
        .route("/", get(hello_world))
        .layer(
            TraceLayer::new_for_http()
                .make_span_with(trace::DefaultMakeSpan::new().level(Level::INFO))
                .on_response(trace::DefaultOnResponse::new().level(Level::INFO)),
                );

    let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
    tracing::info!("listening on {}", addr);

    axum::Server::bind(&addr)
        .serve(app.into_make_service())
        .await
        .unwrap();
}

```

Quoting the [documentation](https://docs.rs/tracing-subscriber/latest/tracing_subscriber/registry/index.html):

> This module provides the `Registry` type, a `Subscriber` implementation which tracks per-span data and exposes it to `Layer`s. When a `Registry` is used as the base `Subscriber` of a `Layer` stack, the `layer::Context` type will provide methods allowing `Layer`s to look up span data stored in the registry. While `Registry` is a reasonable default for storing spans and events, other stores that implement `LookupSpan` and `Subscriber` themselves (with `SpanData` implemented by the per-span data they store) can be used as a drop-in replacement.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1672850404927/4b66c642-38f2-476f-982b-4709be5d157a.png)

### Creating log files

To create log files we have to add `tracing_appender` to our `Cargo.toml` file.

Cargo.toml

```
...
tracing-appender = "0.2"


use tracing::Level;
use tracing_appender::rolling;
use tracing_subscriber::fmt::writer::MakeWriterExt;

#[tokio::main]
async fn main() {

    let info_file = rolling::daily("./logs", "info").with_max_level(tracing::Level::INFO);

    tracing_subscriber::fmt()
      .with_writer(info_file)
      .with_ansi(false)
      .init();

  ...
}

```

I just modified this [example](https://github.com/tokio-rs/tracing/blob/master/examples/examples/appender-multifile.rs) from the _tracing_ repository.

Here we create a variable to define the rolling period and passed it the directory where we want our files to be stored and the name of the files. And we set the log level, in this case `INFO`.

Then we pass the variable `info_file` to `with_writer()` function.

`with_writer()` sets the `MakeWriter`, and it is used by `fmt::Layer` or `fmt::Subscriber` to print formatted text representations of Events.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1672856834548/a9717bd2-4564-4da5-a76d-36e3f64dbc27.png)

Now, if we set the level to _trace_, like this.

```
use tracing::Level;
use tracing_appender::rolling;
use tracing_subscriber::fmt::writer::MakeWriterExt;

#[tokio::main]
async fn main() {

    let info_file = rolling::daily("./logs", "info");

    tracing_subscriber::fmt()
      .with_writer(info_file)
      .with_max_level(tracing::Level::TRACE)
      .with_ansi(false)
      .init();

  ...
}

```

The file will look like this.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1672862044395/33a34b65-91eb-4565-90d9-3885d413dcbb.png)

### Conclusion

`Tracing` offers a lot of customization. And it was a challenge for me to find what examples will be the simplest for this article. Fortunately, tracing\_subscriber has good documentation and the repository has helpful examples. Also, I just use _info_ and _trace_ as log levels, but they are not the only ones, we can use _debug, warn, error,_ and _off_.

It was fun to write this article. I did it to learn more about tracing framework and tower-http crate and how they work together.

Thank you for taking the time to read this article.

If you have any recommendations about other packages, architectures, how to improve my code, my English, or anything; please leave a comment or contact me through [**Twitter**](https://twitter.com/Carlos_marcv), or [**LinkedIn**](https://www.linkedin.com/in/carlos-marcano-a2135a134/).

The source code is [**here**](https://github.com/carlosm27/axum-logging)

### Reference

[Getting started with Trancing.](https://tokio.rs/tokio/topics/tracing)

[Tracing documentation.](https://docs.rs/tracing/latest/tracing/)

[Tracing-subscriber documentation.](https://docs.rs/tracing-subscriber/latest/tracing_subscriber/)

[Tower-http documentation](https://docs.rs/tower-http/latest/tower_http/).

[Announcing tower-http.](https://tokio.rs/blog/2021-05-announcing-tower-http)

[tracing-appender documentation](https://docs.rs/tracing-appender/latest/tracing_appender/).

[appender-multifiles example](https://github.com/tokio-rs/tracing/blob/master/examples/examples/appender-multifile.rs).

[Issue #296: Where's the most basic example for HTTP request logging?](https://github.com/tower-rs/tower-http/issues/296)