Logging in Rust With the log Crate and Env Logger Explained
The log crate is a facade and env_logger is one backend. Set it up, pick levels by RUST_LOG, log with lazy arguments and learn why libraries only depend on log.

You added info!("started"), ran the program, and saw nothing. That is the most common first experience with Rust logging, and the cause is that log is a facade: the macros call into whatever backend you installed, and with none installed they are no-ops. This design is what lets libraries log without forcing a choice. This post sets up the common pair and shows the filter syntax. For richer needs, see structured logging with tracing.
Rust 1.90, log 0.4, env_logger 0.11.
Setup
[dependencies]
log = "0.4"
env_logger = "0.11"use log::{debug, error, info, warn};
fn main() {
env_logger::init();
info!("starting");
debug!("config loaded from {}", "app.toml");
warn!("cache is cold");
error!("could not reach {}", "db.example.test");
}RUST_LOG=debug cargo run[2026-11-03T09:12:01Z INFO demo] starting
[2026-11-03T09:12:01Z DEBUG demo] config loaded from app.toml
[2026-11-03T09:12:01Z WARN demo] cache is cold
[2026-11-03T09:12:01Z ERROR demo] could not reach db.example.testThe timestamp will differ. Without RUST_LOG, only the error! line appears.
The filter syntax
RUST_LOG | Meaning |
|---|---|
info | everything at info and above |
demo=debug | the demo target at debug, others at error |
warn,demo::db=trace | warn globally, trace for one module |
demo=debug,hyper=off | silence one noisy dependency |
The target defaults to the module path, so demo::db matches log calls from that module.
Levels
error for things that need attention, warn for recoverable surprises, info for lifecycle events, debug for developer detail, trace for very fine detail. What to log is a design question treated in the logging guidance in the tracing post; a good test for info is whether an operator reading it learns something.
Cost
debug!("payload: {:?}", expensive_to_format());The macro checks the level first and formats only if enabled, but the argument expression runs only inside that check too, because the arguments live inside the macro's branch. For a computation you would otherwise do anyway, no overhead. To guard an explicit computation use if log_enabled!(log::Level::Debug) { ... }.
Libraries
A library depends on log only:
[dependencies]
log = "0.4"It never calls env_logger::init(); that is the application's job. A library that initializes a backend panics when the application also does ("a logger has already been set").
Where to go next
log has no spans, no async awareness and limited structured fields. tracing supersedes it for services, and tracing-log bridges the two so dependencies that use log still show up. For command line tools, env_logger is plenty, and the --verbose flag convention from exit codes and CLI behavior maps naturally to levels. The crates are documented at docs.rs/log and docs.rs/env_logger.


