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 Middleware documentation.
tracing-actix-web documentation.