Production-Grade Logging in Rust Applications
By: Ecky Putrady
Tags: Programming, Software Engineering, Software Development, Rustlang, Rust

Logging in Rust Applications

In the previous section, we’ve learned what to achieve by having an effective log. In this section, we’ll put logging that adheres to the best practices above in a Rust application. The application that we are going to modify is an authentication module that I built for my other article. We will write some logs when:

A user successfully logs into the system A user fails to log into the system Reading data from database fails

Introduction to Rust logging libraries

There are many libraries for logging in Rust. However, the one that we’re going to use is tracing . The documentation of this library is very helpful to understand how to use it. I encourage you to give it a read, as I won’t detail much about the library here.

The core concepts of tracing library are spans, events, and subscribers. A span represents a period of time where events take place. A pan can be nested within another span. We can put data into the span as the context of the span. Events that are correlated to that span will then share the context. A practical example of this would be to put Request ID in the span’s context and produce various events within that span. This way, the events can be correlated together using the Request ID as they share the same span.

Subscribers are notified whenever an event takes place or whenever a span is entered or exited. Subscribers can then do whatever they want with these notifications. Obviously, the main use case is to write these notifications as logs somewhere. The tracing library itself has no implementation of subscribers. This is so that library authors can write logs using tracing while letting the library users decide what to do with the logs. Fret not, the basic subscribers are available as another library that you can just import and use.

In addition to tracing, we will also use these libraries:

tracing-appender

This library provides a subscriber that does non-blocking writes. Writing logs to stdout or a file is an I/O operation. If we block on an I/O operation, our CPU will sit idle. By having it as a non-blocking operation, our CPU can do other things. This will lead to better application throughput. tracing-futures

This library provides compatibility with async/await. tracing-subscriber

This library provides some helper functions to build subscribers. tracing-actix-web

This library provides integration with Actix web, one of the leading libraries for building web servers in Rust. One of the important things that this library does is to generate a Request ID for each request. tracing-bunyan-formatter

This library formats the log as JSON according to Bunyan format. tracing-subscriber is actually able to format the log as JSON. However, I find Bunyan format to be better and is more easily queried in Elastic Stack. tracing-log

tracing is not the only library that handles logging. Rust has it's own official logging library called log . It, however, has some limitations that make producing the logs we want to be difficult, hence we don't use it. However, many libraries are using log . tracing-log allows logs from log to be forwarded to tracing 's subscribers.

Writing the logs

Now let’s start our implementation. In case you have not read my other article noted above, I’m writing the important structs and traits here for your convenience.

Next we implement the logging on the AuthServiceImpl as follows:

The important bits regarding logging are on the implementation of login function. We see that we added #[instrument(skip(self, credential), fields(username = %credential.username))] .

Create a new span with the function name (“login” in this case) as the span name. Do not put the self and credential parameter into the span's context. Obviously, we don’t want password in credential to be logged. Add a new field username with the value of credential.username into the span's context. We will use the username as User ID in this case.

Next, we see info! and error! macros being used. What they do is produce events with INFO and ERROR severity level respectively and also put the given string as the log message. Since #[instrument] that decorates this function produces a span, the events produced by those macros will be correlated to that span. Effectively, this means the event can be correlated to the username field in the span. This is the reason we don't put username in the log message.

Next, we put logs in PostgresCredentialRepo . We are mainly interested to know when the database query fails for any reason.

Similarly, we have #[instrument] and error! here as well. Notice how we can display the error in the message by using {} and passing any type that implements Display trait.

Finally, we need to configure the subscribers. Without subscribers, the logging will do nothing. What we want to do is to print it to stdout with JSON format. We configure this in main.rs .

The main function starts with LogTracer::init() . This comes from tracing-log library. This one-liner basically redirects the logs from log library to tracing 's subscribers.

Next, we set up the subscriber. This is a multi-step process.

Next, we set up BunyanFormattingLayer . This one comes from tracing-bunyan-formatter library. It requires the app name and a writer. For the app name, we build it by combining the package name and the package version defined in Cargo.toml . For the writer, we set up a non-blocking stdout writer using tracing_appener::non_blocking function. This function comes from tracing-appender library.

The subscriber is then created by creating Registry (from tracing-subscriber library). We register multiple layers of functionalities in this registry. First, we create an EnvFilter . The purpose of this layer is to filter out logs with severity level below INFO. In the next layer, we add JsonStorageLayer and the bunyan_formatting_layer that we've just set up. These two layers are what enable the logs to be written as JSON.

Finally, the global default subscriber is set to the subscriber that we've just created. With the subscriber set up, we'll then be able to see that the logs are printed to stdout in JSON format.

Another important bit is the use of TracingLogger . This comes from tracing-actix-web . It enables Request ID generation and logging among other useful stuff that comes with it.

Seeing the logs in action

When we run the app and send a login request, we will see the following logs (formatted so that they are easily read by a human):

Similarly, when we purposefully turn off mySQL database and send a login request, we will get the following logs:

Tips: If you find the log above too verbose, as it logs entering and exiting the span, you can configure the span severity level to DEBUG like so:

#[instrument(level = "debug")]

Since we’ve configured to filter out logs lower than INFO, those logs won’t be printed.

Let’s double-check whether those logs adhere to the best practices above: