Logging in Rust - How to Get Started
With so many different libraries at our disposal for outputting logs in Rust, it's difficult to know which one to choose. When println!
, dbg!
and eprintln!
don't cut it, having a way to structure your logs is extremely important, especially in production-grade applications. This article will help you gain insight on what the best log crate for your use case is when it comes to Rust logging.
How does logging work in Rust?
In short: loggers in Rust depend on a library to act as a "logging facade" - a crate which provides the logging API that the logger can work with. So for example, if we have a crate like log
that provides a logging implementation for us that we can use with a logger, we then will also need to add a crate that actually carries out the logging - for example, simple-logger
being one of many crates that can use log
. Some logging facades may only be able to be used by their own special logger - for example, tracing
either requires you to use the tracing-subscriber
crate, or otherwise implement your own custom type that implements tracing::Subscriber
.
Without further ado, let's start the Rust logging crate comparison!
log
log
is a crate that calls itself a "lightweight logging facade". The crate defines a logging facade as a library that "provides a single logging API that abstracts over the actual logging implementation" - essentially, this means that we'll need to run another library that provides the actual logging and then use this crate to provide the logging messages. Log is also maintained by the Rust core team and is probably the first crate you'll see on the Rust Cookbook, so there's that.
As taken from the GitHub repository, here's a simple example on how it can be used:
use log;
pub fn shave_the_yak(yak: &mut Yak) {
log::trace!("Commencing yak shaving");
loop {
match find_a_razor() {
Ok(razor) => {
log::info!("Razor located: {}", razor);
yak.shave(razor);
break;
}
Err(err) => {
log::warn!("Unable to locate a razor: {}, retrying", err);
}
}
}
}
It should be noted that log
is also compatible with a lot of logger crates - their GitHub repository alone lists over 20 and is a non-exhaustive list! If you're looking for a versatile logger, this is definitely for you. However, it's also not as powerful as some other crates so there's that to bear in mind.
For most common use cases, it's the easiest to use crate: you simply set the message level, then send your message!
A quick summary:
- Maintained by the official Rust team
- Works with nearly all logger crates
- Not as powerful as some other log facade crates
env-logger
env-logger is a simple Rust logger that's easy to use and is quite convenient for any small project where you want to implement logging but don't want something heavy-duty that will more than likely require a considerable amount of boilerplate. It's owned by the Rust CLI Working Group (WG), meaning it'll see long-term support which is great for us.
It can be set up in a one-line statement:
let logger = Logger::from_default_env();
Then you'd simply run your program from cargo like so, with the RUST_LOG
environment variable in front of the command:
# This command will run your program and only print out error messages from logs
RUST_LOG=ERROR cargo run
You can also hard-code your minimum log level in your application like so:
use env_logger::{Logger, Env};
let env = Env::new()
// filters out any messages that aren't at "info" log level or above
.filter_or("MY_LOG", "info")
// always use styles when printing
.write_style_or("MY_LOG_STYLE", "always");
let logger = Logger::from_env(env);
Have an overly verbose crate that loves spitting out logs? You can also set the log level for a specific dependency (this is in conjunction with the log
crate):
use env_logger::Builder;
use log::LevelFilter;
let mut builder = Builder::new();
builder.filter_module("path::to::module", LevelFilter::Info);
.unwrap();
For all of its convenience however, env-logger
does suffer from a couple of things that you might be looking for in a production-grade application: namely, that there is little documented functionality on writing your own pipe for logs which can make it quite tricky to implement, and it's also unclear whether this crate is thread-safe. Needless to say, it's an extremely useful crate for any quick and dirty logging!
A quick summary:
- Owned by the Rust CLI Working Group
- Simple to use and feels good to use
- Lack of documentation on more complex functionality like log appending/piping
- Some unclear issues on whether the crate is 100% thread-safe
log4rs
log4rs is a logging crate modeled after Java's log4j - a logging package that's probably one of the most deployed pieces of open source software. This crate requires a bit more setup than the others and configuration can be done with either a YAML file or programmatically. log4rs
is compatible with log
, which is great for us as it means we don't have to adopt a new paradigm just to use log4rs
.
If you wanted to create a config file to load in from, you'd set your YAML file up like this:
# set a refresh rate
refresh_rate: 30 seconds
# appenders
appenders:
# this appender will append to the console
stdout:
kind: console
# this appender will append to a log file
requests:
kind: file
path: "log/requests.log"
# this is a simple string encoder - this will be explained below
encoder:
pattern: "{d} - {m}{n}"
# the appender that prints to stdout will only print if the log level of the message is warn or above
root:
level: warn
appenders:
- stdout
# set minimum logging level - log messages below the mnimum won't be recorded
loggers:
app::backend::db:
level: info
app::requests:
level: info
appenders:
- requests
additive: false
The encoder can either use JSON encoding, or pattern encoding. Here we've decided to use pattern encoding, which follows similarly to the original log4j pattern but with Rust string formatting - you can check out more about how to format your encoder pattern here.
Then you can just initialise it when you're setting your program up, like so:
log4rs::init_file("log4rs.yml", Default::default()).unwrap();
You can also programatically create your configuration:
use log::LevelFilter;
use log4rs::append::console::ConsoleAppender;
use log4rs::append::file::FileAppender;
use log4rs::encode::pattern::PatternEncoder;
use log4rs::config::{Appender, Config, Logger, Root};
fn main() {
// set up ConsoleAppender to allow appending logs to the console (stdout)
let stdout = ConsoleAppender::builder().build();
// set up FileAppender to allow appending logs to a log file
let requests = FileAppender::builder()
.encoder(Box::new(PatternEncoder::new("{d} - {m}{n}")))
.build("log/requests.log")
.unwrap();
let config = Config::builder()
.appender(Appender::builder().build("stdout", Box::new(stdout)))
.appender(Appender::builder().build("requests", Box::new(requests)))
.logger(Logger::builder().build("app::backend::db", LevelFilter::Info))
.logger(Logger::builder()
.appender("requests")
.additive(false)
.build("app::requests", LevelFilter::Info))
.build(Root::builder().appender("stdout").build(LevelFilter::Warn))
.unwrap();
let handle = log4rs::init_config(config).unwrap();
// use handle to change logger configuration at runtime
}
You can also automatically archive your logs with log4rs
, which is great! This is a feature that must otherwise be manually implemented by yourself when it comes to most of if not all other logger crates, so having this feature built into the logger itself is a huge convenience. We can get started with setting it up by adding the following to a YAML configuration file (under "appenders"):
rolling_appender:
kind: rolling_file
path: log/foo.log
append: true
encoder:
kind: pattern
pattern: "{d} - {m}{n}"
policy:
kind: compound
trigger:
kind: size
limit: 10 mb
# upon reaching the max log size, the file simply gets deleted on successful roll
roller:
kind: delete
Now we have a policy that fills up the main log file, then appends it to an archived log file once the main log file reaches 10 megabytes' worth of logs, then deletes the currently active log file ready to receive more logs.
As you can see, log4rs
is an extremely versatile crate that works with the previously mentioned log
crate to provide powerful functionality with regards to logging in Rust, and is espespecially great if you're coming from a language like Java where you already understand the mental model and just want to find out how to do logging in Rust. However, in exchange for this you have to learn how to set the logger up and the setup itself is quite complicated compared to other logging crates, so bear that in mind.
Summary:
- Large all-in-one crate that can do it all
- Requires extensive boilerplate or a config file
- Easy to set up your own file appending for a log egress service
- Works with
log
tracing
tracing is a crate that calls itself "a framework for instrumenting Rust programs to collect structured, event-based diagnostic information", requiring its logger counterpart tracing-subscriber
to be used or a custom type that implements the tracing::Subscriber
function. Developed by the Tokio team, it's fully built up from the ground for async which is perfect for web applications with Rust logs.
tracing
uses the concept of "spans" which are used to record the flow of execution through a program. Events can happen inside or outside of a span and can also be used similarly to unstructured logging (ie, just recording the event any which way) but can also represent a point of time within a span. See below:
use tracing::Level;
// records an event outside of any span context:
tracing::event!(Level::DEBUG, "something happened");
// create the span while entering it
let span = tracing::span!(Level::INFO"my_span").entered();
// records an event within "my_span".
tracing::event!(Level::DEBUG, "something happened inside my_span");
Spans can form a tree structure, and the entire subtree is represented by its children - therefore, a parent span will always last as long as its longest-lived child span if not longer.
Because all of this can be a bit excessive, tracing
has also included the regular macros that would be in other log facade libraries for logging - namely, info!
, error!
, debug!
, warn!
and trace!
. There's also a span version of each of these macros - but if you're coming from log
and want to try tracing
out without getting lost in the complexity of trying to make sure everything is in a span, tracing's got your back.
use tracing;
tracing::debug!("Looks just like the log crate!");
tracing::info_span!("a more convenient version of creating spans!");
tracing-subscriber is the logger crate designed to work with tracing
by letting you define a logger that implements the Subscriber
trait from tracing
.
You can start a subscriber that takes the RUST_LOG
environment variable like so:
tracing_subscriber::registry()
.with(fmt::layer())
.with(EnvFilter::from_default_env())
.init();
You can also apply a hard-coded filter programatically:
use tracing_subscriber::filter::{EnvFilter, LevelFilter};
let my_filter = EnvFilter::builder()
.with_default_directive(LevelFilter::ERROR.into())
.from_env_lossy();
tracing_subscriber::registry()
.with(fmt::layer())
.with(filter)
.init();
You can also layer filters on top of each other! This is quite useful in case you want the effect of having multiple subscribers at the same time.
If you need to export your logs somewhere, there's also the tracing_appender crate. You would want to add this in with your tracing subscriber by using the .with_writer()
method, like so:
// create a file appender that rotates hourly
let file_appender = tracing_appender::rolling::hourly("/some/directory", "prefix.log");
// make the file appender non-blocking
// the guard exists to make sure buffered logs get flushed to output
let (non_blocking, _guard) = tracing_appender::non_blocking(file_appender);
// add the file appender to your tracing subscriber
tracing_subscriber::fmt()
.with_writer(non_blocking)
.init();
The non_blocking
writer is built with a type that implements std::io::Write
- so if you wanted to implement your own thing that implements std::io::Write
(say you want a logging express that automatically exports all your stuff to BetterStack or Datadog) - you'd want to try this. See below:
use std::io::Error;
struct TestWriter;
impl std::io::Write for TestWriter {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
let buf_len = buf.len();
println!("{:?}", buf);
Ok(buf_len)
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
let (non_blocking, _guard) = tracing_appender::non_blocking(TestWriter);
tracing_subscriber::fmt()
.with_writer(non_blocking)
.init();
As you can see, the tracing
family of crates offers a tonne of power in terms of what it can do and is robust enough for any web application and it's maintained by the Tokio team so it is sure to be supported for a long time. However, using it requires learning about how tracing
works as it uses concepts that are not utilised in other logging crates - so you'll be locked in if you need to migrate from the crate for whatever reason and you're using spans. However, if you're asking yourself "what is the best logger crate in Rust", you can't go wrong with tracing
crates in terms of how powerful this family of crates is.
Summary:
- Requires some learning about spans, etc to utilise fully
- Maintained by the Tokio team so more than likely will see LTS
- Split crates means you don't have to install things you aren't going to use
- Probably the most complex system to use on the list due to the way it's built
Conclusions
Thanks for reading! Now that we're at the end, I hope you have a better understanding of logging in Rust. With so many logging crates it's difficult to figure out which one you should use, but hopefully this article has provided some clarity into which crate is the best Rust logger for your use case.
Did you like this article? Be sure to give us a star on GitHub!