Basic how to log to a file in Rust with log4rs

In this article, we will take look at how you can use and configure log4rs to log to a file in your Rust program. Specifically, we will look at some of the options using YAML to configure the logging.

What is log4rs

From the crate description: “log4rs is a highly configurable logging framework modeled after Java’s Logback and log4j libraries.” For the log4rs crate’s page click here.

Why log4rs

What is good about log4rs? It offers an easy way to log to a file and it is highly configurable using a YAML config file, as well as through code. However, in this article, we will focus on using a YAML file to do the configuration.

Log4rs core concepts

There are a few concepts that we need to be familiar with to understand how to build up the configuration file, there are only a few used in basic logging. With this in mind, the concepts we will look at are log levels, encoders, appenders, and loggers.

Log levels

The common log levels are available:

  • trace
  • debug
  • info
  • warn
  • error

When setting a logger to a certain level it allows all levels above it to be logged as well.

Writing log messages is done using macros: trace!(), debug!(), info!(), warn!(), error!().

Encoders

We use encoders to configure the formatting of the output. The following encoders are available:

  • pattern: A simple pattern-based encoder.
  • json: An encoder which writes a JSON object.
  • writer: Implementations of the encode::Write trait.

We will only look at the pattern encoder in this article.

With the pattern encoder, we can format the log entry to not only contain the message we want to log but also additional information. For example, we can add a timestamp. Go to the documentation for more details on patterns here.

Appenders

The output channels are called “Appenders” in log4rs. The output types, or “kinds” are:

  • console: writes logging to stdout
  • file: writes logging to file, that grows indefinitely
  • rolling file: write logging to file. We have options to control max size of the file and whether or not old log files are archived, etc.

There are crates that add other output types for example log4rs-syslog, to log to syslog.

Loggers

A logger is a named logging entity. Therefore, log events can be targeted at specific loggers that are defined by string names. These loggers can override the root logging configuration. However, it is not necessary to configure a logger, you can either configure the root logger, a specific logger, or both of course. For instance, macros debug!(), info!(), etc. will log to root by default.

Logging examples

In this section, let’s look at a couple of logging examples in practice.

Let’s create a new project cargo new log4rs-logging-example. Then add the relevant dependencies to Cargo.toml:

[package]
name = "logging-to-file"
version = "0.1.0"
edition = "2018"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
log = "0.4"
log4rs = "1"

Basic stdout logging

First, a very simple example to log to the console/terminal.

Basic stdout config file

Create a log file in the root of the project, called logging_config.yaml and add the following lines:

appenders:
  my_stdout:
    kind: console
    encoder:
      pattern: "{h({d(%Y-%m-%d %H:%M:%S)(utc)} - {l}: {m}{n})}"
root:
  level: debug
  appenders:
    - my_stdout

The above code listing is showing that the appender, my_stdout, is defined. The appender name can be any string. The kind specifies the output type, in this case, console, which will write to stdout.

We have an encoder that specifies a pattern that indicates what each log line should contain. There are a lot of things going on here, let’s look at the meaning of the symbols:

  • {h()}: will highlight every text that is within the angle brackets (). The color that log4rs will is using depends on the log level.
  • {d()}: insert a formatted data time string into the message. Adding (utc) at the end there shows the time in UTC.
  • {l}: prints the debugging level for this message.
  • {m}: is the location of the message we intend to log.
  • {n}: is a new line.

A message “test” logged using the “info” level on 2021-08-25 15:19:22 with this pattern will look like:

2021-08-25 13:19:22 - INFO: test

Finally, we have the root configuration. As mentioned before, this configures the default loggers: which appenders are used and what the lowest debug level is.

Basic stdout code

Next, we will look at the code to load this configuration file, and how to log messages.

In main.rs add the following code:

use log::{debug, error, info, trace, warn};
use log4rs;

fn main() {
    log4rs::init_file("logging_config.yaml", Default::default()).unwrap();
    trace!("detailed tracing info");
    debug!("debug info");
    info!("relevant general info");
    warn!("warning this program doesn't do much");
    error!("error message here");
}

On line 5, we are reading in the configuration YAML and the rest of the lines are macros to invoke logging for the respective log levels.

The output will look like this:

log4rs logging output example

Notice that the message logged with trace!() is not shown. This is because we configured the logging level to be debug and above. The trace level is lower than debug so is not shown. If we change the level in the configuration under root to trace, it will be shown.

log4rs logging output at trace level

This is nice for basic logging but for a serious program, we want to learn how to use log4rs to log to a file in Rust.

Basic file logging

In this section, we will create a configuration for logging to a file.

Basic file logging configuration

We are going to update our logging configuration file with new lines for logging to a file:

appenders:
  my_stdout:
    kind: console
    encoder:
      pattern: "{h({d(%Y-%m-%d %H:%M:%S)(utc)} - {l}: {m}{n})}"
  my_file_logger:
    kind: file
    path: "log/my.log"
    encoder:
      pattern: "{d(%Y-%m-%d %H:%M:%S)(utc)} - {h({l})}: {m}{n}"
root:
  level: trace
  appenders:
    - my_stdout
    - my_file_logger

We added a new appender to the list of appenders called my_file_logger. The kind is file, this requires a path to be configured. Log4rs creates the path and the file if they do not exist already. The encoder and pattern are the same as with my_stdout.

Once we add this appender to the appenders list under root, the logs will be written to the file when we run the program.

With this configuration, log4rs will log to both stdout and to the file we specified. As mentioned in an earlier section, the log file will keep growing. Every time we log a message, log4rs adds it to the same log file and the log file keeps growing. To prevent the machine running our program to run out of disk space due to a giant log file that keeps getting bigger we can use rolling file logging.

Rolling file logging

The next step for better logging is to make sure our files don’t take up all the space on our machine.

Let’s add some code to better test the rolling file configuration, add the following on line 12:

   for i in 0..250 {
        info!("To test rolling file configurations we print this message in a loop. This is loop nr. {}", i);
    }

Every time we run the program now, the log file will grow by about 28kb in disk size. That isn’t much, of course, but, let’s say we don’t want a log file larger than 50kb. We configure this using a different kind of appender:

appenders:
  my_stdout:
    kind: console
    encoder:
      pattern: "{h({d(%Y-%m-%d %H:%M:%S)(utc)} - {l}: {m}{n})}"
  my_file_logger:
    kind: rolling_file
    path: "log/my.log"
    encoder:
      pattern: "{d(%Y-%m-%d %H:%M:%S)(utc)} - {h({l})}: {m}{n}"
    policy:
      trigger:
        kind: size
        limit: 50kb
      roller:
        kind: delete
root:
  level: trace
  appenders:
    - my_stdout
    - my_file_logger

The updated lines are highlighted. As mentioned the kind for my_file_logger was changed to rolling_file, and a policy section was added to configure the rules for the rolling file mechanism. In this case, the trigger is on a size of 50kb and the roller kind is delete. Meaning, that every time the log file reaches 50kb in size or above, log4rs deletes the file and creates a new one.

This is a fine strategy for keeping the logs clean and current. However, there are scenarios where you might want to look further back for debugging purposes.

Rolling file logging with fixed window

The final lesson we learn on how to log to a file in Rust with log4rs is, rolling file logging with a fixed window. If we configure the roller kind to be fixed_window instead of delete we can keep old log files around:

appenders:
  my_stdout:
    kind: console
    encoder:
      pattern: "{h({d(%Y-%m-%d %H:%M:%S)(utc)} - {l}: {m}{n})}"
  my_file_logger:
    kind: rolling_file
    path: "log/my.log"
    encoder:
      pattern: "{d(%Y-%m-%d %H:%M:%S)(utc)} - {h({l})}: {m}{n}"
    policy:
      trigger:
        kind: size
        limit: 50kb
      roller:
        kind: fixed_window
        base: 1
        count: 10
        pattern: "log/my{}.log"
root:
  level: trace
  appenders:
    - my_stdout
    - my_file_logger

I have highlighted the changed lines. Here the base determines the starting number, and count determines the maximum number of files. The pattern tells log4rs what the file should be named and where the number should go.

With this configuration, log4rs will log to my.log and if that file exceeds 50kb in size, log4rs will move the older lines to an archive file log/my1.log and then add the new lines to my.log. Log4rs will do this up to my10.log, the higher the number the older the logging lines will be.

Try running the program multiple times and see the effects of this configuration.

Logging to multiple files using loggers

We have yet to use the “loggers” concept that was mentioned in the “log4rs core concepts” section. This is a useful mechanism, to, for example, isolate a special event and log it to a specific file. So let’s look at an example now.

We will update the configuration YAML logging_config.yaml once again:

appenders:
  my_stdout:
    kind: console
    encoder:
      pattern: "{h({d(%Y-%m-%d %H:%M:%S)(utc)} - {l}: {m}{n})}"
  my_file_logger:
    kind: rolling_file
    path: "log/my.log"
    encoder:
      pattern: "{d(%Y-%m-%d %H:%M:%S)(utc)} - {h({l})}: {m}{n}"
    policy:
      trigger:
        kind: size
        limit: 50kb
      roller:
        kind: fixed_window
        base: 1
        count: 10
        pattern: "log/my{}.log"
  my_special_file_logger:
    kind: rolling_file
    path: "log/my_special.log"
    encoder:
      pattern: "{d(%Y-%m-%d %H:%M:%S)(utc)} - {h({l})}: {m}{n}"
    policy:
      trigger:
        kind: size
        limit: 50kb
      roller:
        kind: delete
root:
  level: trace
  appenders:
    - my_stdout
    - my_file_logger
loggers:
  special:
    level: info
    appenders:
      - my_special_file_logger
    additive: false

The highlights, show the added lines. We have added a new appender config which is set to log to the file log/my_special.log. Furthermore, this file will be deleted and start fresh once it reaches 50kb.

Notice that this appender has not been added to the root section. This means messages will by default not be logged using this appender. At the bottom of the configuration file, we added a loggers section and called the logger special. In the code, this name is used to target that particular logger.

On the last line, we have additive: false this means that appenders of the parent should not be associated with this logger. The parent in this case is root and the appenders are my_stdout and my_file_logger. That is to say, setting additive to true will send the messages targetted at special to those two appenders as well.

The way to target a logger in code is for example: info!(target: "<logger name>", "<log message>");. So, let’s add a line like this to our main.rs file:

use log::{debug, error, info, trace, warn};
use log4rs;

fn main() {
    log4rs::init_file("logging_config.yaml", Default::default()).unwrap();
    trace!("detailed tracing info");
    debug!("debug info");
    info!("relevant general info");
    warn!("warning this program doesn't do much");
    error!("error message here");

    for i in 0..250 {
        info!("To test rolling file configurations we print this message in a loop. This is loop nr. {}", i);
    }

    info!(target: "special", "this is info for a special event");
}

After running the project now, notice that our new log message does not appear in the terminal. However, a new log file has appeared in the log directory called my_special.log containing our new log message.

Conclusion

We have a good grasp of how to log to a file in Rust with log4rs. So now, we can log to stdout and to a file. On top of that, we learned how to prevent our machine from running out of disk space from the log files. Finally, we made use of loggers to put certain log messages in a separate log file.

Now we can use this knowledge to our advantage in our other projects. For example, it would be a great addition to our crypto triangle arbitrage dashboard backend project or a WebSocket backend project in general.

Comments (5)
  • Coming from a Java/Scala background, this is an excellent article on how to use a configurable real world logger. Thanks!

  • Hi Tim, thank you for awesome tutorial.

    Can you please tell me how to enable compression in log4rs version 0.13.0?

    Best regards.
    Milan

    • Hi Milan,
      Thank you for your message.
      I believe compression is on by default in version 0.13.0 according to the documentation:

      If you are using the file rotation in your configuration there is a known substantial performance issue so listen up! By default the gzip feature is enabled and when rolling files it will zip log archives automatically. This is a problem when the log archives are large as the zip happens in the main thread and will halt the process while the zip is completed. Be advised that the gzip feature will be removed from default features as of 1.0.

      https://github.com/estk/log4rs

      Is this not the case?

      • It is said that in the documentation but a few things need to be added.
        First in .toml:
        log4rs = { version =”0.13.0″, features = [“gzip”] }

        And the second thing in configuration file extension needs to be set:

        roller:
        kind: fixed_window
        base: 1
        count: 10
        pattern: “log/my{}.gz”

Leave a Reply

Your email address will not be published. Required fields are marked *