Step-by-Step Guide to Adding Logging to an Actix Web Application

Photo by Sharad Bhat on Unsplash

Step-by-Step Guide to Adding Logging to an Actix Web Application

I recently started working on a project that uses Actix, it is the first time I touch this web framework. And I have an issue that every time I start the server didn't show me a message that the server is running or when an endpoint is called.

Probably I have this issue because I'm used to using other web frameworks in other languages that have a default logging feature.

So I started to write this article just to learn how to add logging to an Actix application and have a resource to use in case I face the same problem again.

Requirements

  • Rust installed

Cargo.toml

[dependencies]
actix-web = "4"
tracing-actix-web = "0.7"
tracing = "0.1"
log = "0.4"
env_logger = "0.9"

main file

First, we start writing a minimum web server example that just shows a "Hello World!" message. And then, add logging to see when the server is called.

use actix_web::{get, App, HttpResponse, HttpServer, Responder};

#[get("/")]
async fn hello() -> impl Responder {
    HttpResponse::Ok().body("Hello world!")
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {

    HttpServer::new(|| {
        App::new()
            .service(hello)   
    })
    .bind(("127.0.0.1", 8080))?
    .run()
    .await
}

Now, we run the cargo run command to start the server.

use actix_web::{get, App, HttpResponse, HttpServer, Responder};
use actix_web::middleware::Logger;
use env_logger::Env;

#[get("/")]
async fn hello() -> impl Responder {
    HttpResponse::Ok().body("Hello world!")
}

#[get("/hi")]
async fn hi() -> impl Responder {
    HttpResponse::Ok().body("Greetings")
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {

   env_logger::init_from_env(Env::default().default_filter_or("info"));
    HttpServer::new(|| {
        App::new()
            .service(hello)
            .service(hi)
            .wrap(Logger::default())


    })
    .bind(("127.0.0.1", 8080))?
    .run()
    .await
}

Here, Logging is implemented as a middleware. It is common to register a logging middleware as the first middleware for the application. Logging middleware must be registered for each application.

The env_logger::init_from_env(Env::default().default_filter_or("info")); line initializes the logger with the default log level of "info", which will output logs to the console in a standard format.

.wrap(Logger::default()) attaches the Logger middleware to the app, which logs requests and responses automatically.

The Logger middleware uses the standard log crate to log information. You should enable logger for the actix_web package to see access log (env_logger or similar).

Create Logger middleware with the specified format. Default Logger can be created with the default method, it uses the default format:

  %a %t "%r" %s %b "%{Referer}i" "%{User-Agent}i" %T

Format

  • %% The percent sign

  • %a Remote IP-address (IP-address of proxy if using reverse proxy)

  • %t Time when the request was started to process

  • %P The process ID of the child that serviced the request

  • %r First line of request

  • %s Response status code

  • %b Size of response in bytes, including HTTP headers

  • %T Time taken to serve the request, in seconds with floating fraction in .06f format

  • %D Time taken to serve the request, in milliseconds

  • %{FOO}i request.headers['FOO']

  • %{FOO}o response.headers['FOO']

  • %{FOO}e os.environ['FOO']

Adding Logging Files

Also, just for curiosity, I was looking for a way to generate logging files. But the solution is a little bit different, without using Actix middleware.

We have to add simplelog to the Cargo.toml file

Cargo.toml

...
simplelog = "0.6"

main file

use log::{info, error};
use simplelog::{CombinedLogger, Config, LevelFilter, WriteLogger};
use std::fs::File;

...

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    let log_file = File::create("./app.log").unwrap();
    CombinedLogger::init(
        vec![
            WriteLogger::new(
                LevelFilter::Debug,
                Config::default(),
                log_file,
            ),

            WriteLogger::new(
                LevelFilter::Debug, 
                Config::default(), 
                std::io::stdout()),
        ]
    ).unwrap();


    HttpServer::new(|| {
        App::new()
            .service(hello)
            .service(hi)

    })
    .bind(("127.0.0.1", 8080))?
    .run()
    .await
}

This solution uses the simplelog crate, which is a crate that does not aim to provide a rich set of features, nor to provide the best logging solution. It aims to be a maintainable, easy-to-integrate facility for small to medium-sized projects, that find env_logger to be somewhat lacking in features. In those cases, simplelog should provide an easy alternative, according to its documentation.

I have to remove the code that uses Actix middleware to show the logs in the command line. Because it uses env_logger, and the main thread panicked, I got the following message:

"thread 'main' panicked at 'env_logger::init_from_env should not be called after logger initialized: SetLoggerError(())', C:\Users\gate1.cargo\registry\src\github.com-1ecc6299db9ec823\env_logger-0.9.3\src\lib.rs:1222:10".

So, I decide only to use simplelog, for this example for generating logging files and showing logs in the command line.

Tracing

To add tracing to the Actix web server, we will use tracing-actix-web crate.

Tracing-actix-web

According to its documentation, tracing-actix-web provides TracingLogger, a middleware to collect telemetry data from applications built on top of the Actix-web framework.

main file

use actix_web::{get, App, HttpResponse, HttpServer, Responder};
use actix_web::middleware::Logger;
use env_logger::Env;
use tracing_actix_web::TracingLogger;

#[get("/")]
async fn hello() -> impl Responder {
    HttpResponse::Ok().body("Hello world!")
}

#[get("/hi")]
async fn hi() -> impl Responder {
    HttpResponse::Ok().body("Greetings")
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {

   env_logger::init_from_env(Env::default().default_filter_or("trace")); 

    HttpServer::new(|| {
        App::new()
            .wrap(TracingLogger::default())
            .service(hello)
            .service(hi)


    })
    .bind(("127.0.0.1", 8080))?
    .run()
    .await
}

For tracing we just add TracingLogger::default() and wrap it using the wrap() function to use it as a middleware.

TracingLogger is a middleware to capture structured diagnostics when processing an HTTP request.

TracingLogger is designed as a drop-in replacement of actix-web’s Logger.

As you can see, I remove Actix's Logger middleware.

We can use this crate to add instrumentation to our application and be collected with OpenTelemetry for further analysis.

To know more about this crate you can read "Are we observable yet?", it provides an in-depth introduction to the crate and the problems it solves within the bigger picture of observability.

Conclusion

Before writing this article I just want to add simple loggings to my Actix application. But, in the journey, I was more curious about what more I can add. I was hoping to find a crate or an All-in-One solution, but I didn't find it, it doesn't mean that doesn't exist. Probably I would find one if I do deep research or have better skills in Rust.

However, I think this allows more flexibility and customization. If we want just to show logs in the command line, using Logger middleware is fine. If we want to collect the telemetry for further analysis, TracingLogger. Write the logs in a file, simplelog.

Sometimes having a unique library with a lot of features that we don't want or are not going to use, just adds unnecessary dependencies.

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, or LinkedIn.

The source code is here.

Resources

Actix Documentation.

Actix Middleware documentation.

tracing-actix-web documentation.

simplelog crate documentation.

Are we observable yet? An introduction to Rust Telemetry.