This blog post is part of a series I’m developing documenting my evolution as a Rust developer. Currently I’m a newbie, but I’m making progress.

Note

As a newbie, please feel free in the comments to elaborate on what I might be doing wrong, could do better or is not “canonical” Rust.

Logging is obviously a key aspect of a production-ready application. While one could use println! or dbg! or similar Rust macros to achieve something similar, they are not really a replacement for a real logging framework. In fact, particularly for (long running) “services” as opposed to CLIs, many architecture/coding standards prohibit use of the equivalent to println! in whatever language you’re using. I myself have set such standards on projects I’ve lead.

So as I’ve been teaching myself Rust, I was naturally interested in what the Rust ecosystem has in terms of logging capabilities.

The first crate to mention is the log crate. This library provides a standard logging facade API that is then combined with an actual logging implementation provided by a separate crate that you as the developer can select per your requirements. This is similar to commons-logging or SLF4J in the Java world. To use log in Rust,

cargo add log

or add it manually, but in either case your Cargo.toml will end up with

[dependencies]
log = "0.4.21"

To add logging to your application, you then use one of the macros to log at

  • trace!
  • debug!
  • info!
  • warn!
  • error!

There is also a generic log! macro which takes the log level as a parameter. To use these macros just include them in your code, e.g.

use log::info;
 
fn main() {
    info!("Hello, world!");
}

If you run this now, this is what you’d see:

$ cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.00s
     Running `target/debug/rust-examples`

Hey, where’s my log message? Well, as I mentioned, the log crate just provide the API. To actually do something you need a log implementation. (Returning to the Java example, this is the same as needing to include, e.g. Logback as an implementation when using SLF4J as the API.)

Looking at the log create documentation you’ll see a lot of different possible logging implementations that you can select from. Choose one based on your requirements. For this blog I am going to pick a sophisticated implementation that’s inspired by Logback and provides a lot of configuration options. It is called log4rs.

$ cargo add log4rs
    Updating crates.io index
      Adding log4rs v1.3.0 to dependencies.
             Features:
             ...

(I am truncating the output, which lists all the features that are available.)

[dependencies]
log = "0.4.21"
log4rs = "1.3.0"

This is not all that is required though. If you were to run the application now, you’d still not see any output. This is because log4rs requires configuration. This can be done either via a YAML configuration file, or programmatically. You can read the log4rs documentation for the details, but to briefly summarize, log4rs (like Logback) has the notion of loggers, appenders and encoders. An appender is something that writes (appends) to a log. Encoders define how the log messages are formatted when written. Examples of appenders include a console appender (to stdout) and a file appender. Encoders could be a regex pattern encoder or a json encoder. Here is a very basic log4rs configuration file.

appenders:
  console:
    kind: console
    encoder:
      pattern: "{d(%+)(local)} [{t}] {h({l})} {M}:{m}{n}"
 
root:
  appenders:
    - console

You then need to load this configuration and initialize log4rs, like so:

use log::info;
 
fn main() {
    log4rs::init_file("log_config.yaml", Default::default()).unwrap();
    info!("Hello, world!");
}

Finally you will now see a log message, formatted according to the pattern specified in the configuration file.

$ cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.02s
     Running `target/debug/rust-examples`
2024-04-07T14:37:36.838882622-07:00 [rust_examples] INFO rust_examples:Hello, world!
 

If you change the configuration file to use the JSON encoder like so

appenders:
  console:
    kind: console
    encoder:
      kind: json
 
root:
  appenders:
    - console

Now you’ll see a JSON formatted equivalent (I’ve run the output through jq to make it more readable):

$ cargo run | jq
    Finished dev [unoptimized + debuginfo] target(s) in 0.05s
     Running `target/debug/rust-examples`
{
  "time": "2024-04-07T14:45:09.715994247-07:00",
  "level": "INFO",
  "message": "Hello, world!",
  "module_path": "rust_examples",
  "file": "src/main.rs",
  "line": 5,
  "target": "rust_examples",
  "thread": "main",
  "thread_id": 140185055148352,
  "mdc": {}
}

There is a lot more I could show here, such as using a file (or rolling file) appender, different possibilities with the encoders, having multiple loggers e.g. maybe a rolling file appender for everything but a console appender that filters to allow only error level messages; or maybe different file appenders for different modules. This is also only one of many available implementations that work with the log facade crate.

Stay tuned for more blogs as I continue my Rust journey. Thanks for reading!