Skip to main content

Logging

Rust provides multiple logging crates that can be easily integrated into your applications for various logging levels, custom outputs, and more.

Log Messages

Log messages are categorized into different levels, such as debug, info, warn, error, etc., each serving a unique purpose in monitoring your application's state. Rust's log crate provides macros to generate these messages, while libraries like env_logger help you control their output.

Log a debug message to the console

[dependencies]
log = "0.4.22"
env_logger = "0.11.5"

The log crate provides logging utilities. The env_logger crate configures logging via an environment variable. The log::debug! macro works like other std::fmt formatted strings.

fn execute_query(query: &str) {
log::debug!("Executing query: {}", query);
}

fn main() {
env_logger::init();

execute_query("DROP TABLE students");
}

No output prints when running this code. By default, the log level is error, and any lower levels are dropped.

Set the RUST_LOG environment variable to print the message:

$ RUST_LOG=debug cargo run

Cargo prints debugging information then the following line at the very end of the output:

DEBUG:main: Executing query: DROP TABLE students

Log an error message to the console

[dependencies]
log = "0.4.22"
env_logger = "0.11.5"

Proper error handling considers exceptions exceptional. Here, an error logs to stderr with log's convenience macro log::error!.

fn execute_query(_query: &str) -> Result<(), &'static str> {
Err("I'm afraid I can't do that")
}

fn main() {
env_logger::init();

let response = execute_query("DROP TABLE students");
if let Err(err) = response {
log::error!("Failed to execute query: {}", err);
}
}

Log to stdout instead of stderr

[dependencies]
log = "0.4.22"
env_logger = "0.11.5"

Creates a custom logger configuration using the Builder::target to set the target of the log output to Target::Stdout.

use env_logger::{Builder, Target};

fn main() {
Builder::new()
.target(Target::Stdout)
.init();

log::error!("This error has been printed to Stdout");
}

Log messages with a custom logger

[dependencies]
log = "0.4.22"
env_logger = "0.11.5"

Implements a custom logger ConsoleLogger which prints to stdout. In order to use the logging macros, ConsoleLogger implements the log::Log trait and log::set_logger installs it.

use log::{Record, Level, Metadata, LevelFilter, SetLoggerError};

static CONSOLE_LOGGER: ConsoleLogger = ConsoleLogger;

struct ConsoleLogger;

impl log::Log for ConsoleLogger {
fn enabled(&self, metadata: &Metadata) -> bool {
metadata.level() <= Level::Info
}

fn log(&self, record: &Record) {
if self.enabled(record.metadata()) {
println!("Rust says: {} - {}", record.level(), record.args());
}
}

fn flush(&self) {}
}

fn main() -> Result<(), SetLoggerError> {
log::set_logger(&CONSOLE_LOGGER)?;
log::set_max_level(LevelFilter::Info);

log::info!("hello log");
log::warn!("warning");
log::error!("oops");
Ok(())
}

Log to the Unix syslog

[dependencies]
syslog = "7.0.0"
log = "0.4.22"

Logs messages to UNIX syslog. Initializes logger backend with syslog::init. syslog::Facility records the program submitting the log entry's classification, log::LevelFilter denotes allowed log verbosity and Option<&str> holds optional application name.

use syslog::{Facility, Error};

fn main() -> Result<(), Error> {
syslog::init(Facility::LOG_USER,
log::LevelFilter::Debug,
Some("My app name"))?;
log::debug!("this is a debug {}", "message");
log::error!("this is an error!");
Ok(())
}

Configuration

Enable log levels per module

[dependencies]
log = "0.4.22"
env_logger = "0.11.5"

Creates two modules foo and nested foo::bar with logging directives controlled separately with RUST_LOG environmental variable.

mod foo {
mod bar {
pub fn run() {
log::warn!("[bar] warn");
log::info!("[bar] info");
log::debug!("[bar] debug");
}
}

pub fn run() {
log::warn!("[foo] warn");
log::info!("[foo] info");
log::debug!("[foo] debug");
bar::run();
}
}

fn main() {
env_logger::init();
log::warn!("[root] warn");
log::info!("[root] info");
log::debug!("[root] debug");
foo::run();
}

RUST_LOG environment variable controls [env_logger][env_logger] output. Module declarations take comma separated entries formatted like path::to::module=log_level. Run the test application as follows:

RUST_LOG="warn,test::foo=info,test::foo::bar=debug" ./test

Sets the default log::Level to warn, module foo and module foo::bar to info and debug.

WARN:test: [root] warn
WARN:test::foo: [foo] warn
INFO:test::foo: [foo] info
WARN:test::foo::bar: [bar] warn
INFO:test::foo::bar: [bar] info
DEBUG:test::foo::bar: [bar] debug

Use a custom environment variable to set up logging

[dependencies]
log = "0.4.22"
env_logger = "0.11.5"

Builder configures logging.

Builder::from_env parses MY_APP_LOG environment variable contents in the form of RUST_LOG syntax. Then, Builder::init initializes the logger.

use env_logger::Builder;

fn main() {
Builder::from_env("MY_APP_LOG").init();

log::info!("informational message");
log::warn!("warning message");
log::error!("this is an error {}", "message");
}

Include timestamp in log messages

[dependencies]
log = "0.4.22"
env_logger = "0.11.5"
chrono = "0.4.38"

Creates a custom logger configuration with Builder. Each log entry calls Local::now to get the current DateTime in local timezone and uses DateTime::format with strftime::specifiers to format a timestamp used in the final log.

The example calls Builder::format to set a closure which formats each message text with timestamp, Record::level and body (Record::args).

use std::io::Write;
use chrono::Local;
use env_logger::Builder;
use log::LevelFilter;

fn main() {
Builder::new()
.format(|buf, record| {
writeln!(buf,
"{} [{}] - {}",
Local::now().format("%Y-%m-%dT%H:%M:%S"),
record.level(),
record.args()
)
})
.filter(None, LevelFilter::Info)
.init();

log::warn!("warn");
log::info!("info");
log::debug!("debug");
}

stderr output will contain

2017-05-22T21:57:06 [WARN] - warn
2017-05-22T21:57:06 [INFO] - info

Log messages to a custom location

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

[log4rs] configures log output to a custom location. [log4rs] can use either an external YAML file or a builder configuration.

Create the log configuration with log4rs::append::file::FileAppender. An appender defines the logging destination. The configuration continues with encoding using a custom pattern from log4rs::encode::pattern. Assigns the configuration to log4rs::config::Config and sets the default log::LevelFilter.

use log::LevelFilter;
use log4rs::{
append::file::FileAppender,
config::{Appender, Config, Root},
encode::pattern::PatternEncoder,
};

fn main() -> Result<(), Box<dyn std::error::Error>> {
let logfile = FileAppender::builder()
.encoder(Box::new(PatternEncoder::new("{l} - {m}\n")))
.build("log/output.log")?;

let config = Config::builder()
.appender(Appender::builder().build("logfile", Box::new(logfile)))
.build(Root::builder().appender("logfile").build(LevelFilter::Info))?;

log4rs::init_config(config)?;

log::info!("Hello, world!");

Ok(())
}

Async Logging with tokio-tracing

In asynchronous applications, especially those using tokio, it's important to use logging tools that work seamlessly with async tasks. The tracing crate provides structured logging and diagnostics specifically for async Rust applications.

Set up Tokio and Tracing

First, add the necessary dependencies:

[dependencies]
tokio = { version = "1.40.0", features = ["full"] }
tracing = "0.1.40"
tracing-subscriber = "0.3.18"

The tracing crate provides structured, async-aware logging capabilities. tracing-subscriber is used to manage subscribers that handle log output, and tokio enables async runtimes.

Logging with Tracing in an Async Context

To use tracing in an async tokio runtime, set up a tracing subscriber to handle log output and instrument async functions:

#[tokio::main]
async fn main() {
// Set up a tracing subscriber that logs to stdout
tracing_subscriber::fmt::init();

// Call an async function that will generate log output
perform_task("Ben").await;
}

#[tracing::instrument]
async fn perform_task(name: &str) {
tracing::info!("Performing an important async task");
}

In this example:

  • The #[instrument] attribute automatically generates structured logs, capturing input arguments and other context.
  • tracing_subscriber::fmt::init() sets up a subscriber that logs to stdout.

Logging Context in Async Functions

In async Rust, capturing context with spans is essential for logging in distributed, concurrent environments. For example:

#[tokio::main]
async fn main() {
tracing_subscriber::fmt::init();
task1().await;
}

#[tracing::instrument]
async fn task1() {
tracing::info!("Starting task1");
task2().await;
}

#[tracing::instrument]
async fn task2() {
tracing::info!("Starting task2");
}

Each function call logs its span, allowing tracing to display structured logs showing which async tasks executed and when.

Capturing Function Return Values

You can also capture return values in async functions by setting ret to true. This is useful for debugging functions that return futures:

#[tokio::main]
async fn main() {
tracing_subscriber::fmt::init();
let result = compute(5).await;
println!("Result: {}", result);
}

#[tracing::instrument(ret)]
async fn compute(input: u32) -> u32 {
input * 2
}

Customizing Field Logging with #[instrument]

By default, #[instrument] logs all function arguments, but you can customize which arguments or fields to include or exclude. Use the fields argument to control the log output.

For example, you can explicitly log only specific fields:

#[tokio::main]
async fn main() {
tracing_subscriber::fmt::init();
greet("Ben", 42).await;
}

#[tracing::instrument(fields(user = name), skip(name))]
async fn greet(name: &str, age: u32) {
tracing::info!("Saying hello");
}