Introduction: Why logging is critical for Rust applications
Logging is not just about writing events to a file. In a production environment, high-quality logs allow you to quickly diagnose errors, track performance, and analyze system behavior. Rust, being a systems programming language, offers flexible and performant tools for this task. In this article, we will cover the best practices for logging in Rust: from choosing a crate to configuring structured logs and rotation.
1. Basics: the log and env_logger crates
The Rust ecosystem offers a modular approach. The log crate provides an abstraction (facade), while a separate crate, such as env_logger or pretty_env_logger, provides the concrete implementation (backend).
Installation and setup
Add the dependencies to Cargo.toml:
[dependencies]log = "0.4"env_logger = "0.10"Basic example
use log::{info, warn, error};
fn main() { env_logger::init();
info!("Application started"); let user_id = 42; warn!("User {} not found in cache", user_id); error!("Database connection error: {:?}", "timeout");}Important: By default, logs are not displayed until you set the RUST_LOG environment variable. Run the program like this:
RUST_LOG=info cargo runLogging levels
The log crate supports five standard levels (from most to least priority):
error— critical errors requiring immediate attention.warn— warnings about potential problems.info— general information about execution progress.debug— debugging information for developers.trace— extremely detailed events (often used in loops).
Filtering by modules: RUST_LOG=my_crate=debug,other_crate=info.
2. Advanced logging: the tracing crate
For complex applications (web servers, microservices), it is recommended to use the tracing crate. It provides the concept of spans (time intervals) and structured logging with context.
Installing tracing
[dependencies]tracing = "0.1"tracing-subscriber = "0.3"Example with spans
use tracing::{info, instrument, span, Level};use tracing_subscriber;
fn main() { tracing_subscriber::fmt::init();
let parent_span = span!(Level::INFO, "request_processing", request_id = 123); let _guard = parent_span.enter(); process_data();}
#[instrument]fn process_data() { info!("Data processing started"); std::thread::sleep(std::time::Duration::from_millis(50)); info!("Data processing finished");}The #[instrument] attribute automatically creates a span for the function and records execution time. The output will contain nested timestamps, which is invaluable for profiling.
3. Structured logs (JSON) and rotation
In a production environment, logs are often collected centrally (ELK, Loki, Grafana). For this purpose, it is convenient to output logs in JSON format.
JSON output with tracing-subscriber
use tracing_subscriber::fmt::format::Json;use tracing_subscriber::prelude::*;
fn main() { let format = Json::new() .with_timer(tracing_subscriber::fmt::time::UtcTime::rfc3339()) .with_target(true) .with_level(true);
tracing_subscriber::fmt() .event_format(format) .init();
tracing::info!("Server started on port 8080");}Now the logs will look like this: