Logging in Rust Applications: Best Practices in Practice

Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

Logging in Rust Applications: Best Practices in Practice



Logging is one of the key elements of any production application. It helps diagnose errors, track system behavior, and collect metrics. In the Rust ecosystem, there are several mature solutions for this. In this article, we will explore best practices for logging using real code examples.



1. Basics: the log crate and facade architecture



In Rust, the de facto standard is the log crate. It provides an abstract facade that is not tied to a specific implementation. This means you write logs using the log macros, and which specific backend will process them (output to console, to file, send to ELK) is decided when another crate is connected.



Setting up basic logging



// Cargo.toml[dependencies]log = "0.4"env_logger = "0.11"


// main.rsuse log::{info, warn, error, debug};

fn main() { // Initialize env_logger (reads the RUST_LOG variable) env_logger::init();

info!("Application started"); warn!("Using default configuration"); error!("Critical error: {}", "file not found");}


Run the application with the environment variable:

RUST_LOG=info cargo run


You will see on the screen:

[2025-01-15T10:30:00Z INFO  my_app] Application started[2025-01-15T10:30:00Z WARN  my_app] Using default configuration[2025-01-15T10:30:00Z ERROR my_app] Critical error: file not found


2. Flexible configuration of log levels



The RUST_LOG variable allows you to flexibly filter messages by modules and levels. This is critical for large projects where noise from debug logs can hide important events.



Filtering examples



# Only errors and warnings from the entire applicationRUST_LOG=warn

# Debug only in the database module, everything else — infoRUST_LOG=info,my_crate::database=debug

# All levels, including traceRUST_LOG=trace


In practice, use this approach:

use std::env;

fn setup_logging() { if env::var("RUST_LOG").is_err() { // Default value for development env::set_var("RUST_LOG", "info,my_crate=debug"); } env_logger::init();}


3. Structured logging with tracing



For complex systems (microservices, asynchronous applications), it is better to use the tracing crate. It supports spans and structured fields, allowing you to link logs into request chains.



Comparison of log and tracing



  • log — simple, lightweight, suitable for small projects.
  • tracing — powerful, supports spans, events, structured fields; ideal for async code and systems with tracing instrumentation.


Example with tracing and tracing-subscriber



// Cargo.toml[dependencies]tracing = "0.1"tracing-subscriber = "0.3"


use tracing::{info, error, span, Level};use tracing_subscriber::FmtSubscriber;

#[tokio::main]async fn main() { // Configure subscriber for stdout output let subscriber = FmtSubscriber::builder() .with_max_level(Level::TRACE) .finish(); tracing::subscriber::set_global_default(subscriber) .expect("Failed to set subscriber");

// Create a span for an HTTP request let request_span = span!(Level::INFO, "http_request", method = "GET", path = "/api/users"); let _guard = request_span.enter();

info!("Request processing started");

// Nested span for database work let db_span = span!(Level::DEBUG, "db_query", query = "SELECT * FROM users"); let _db_gu

Blogs

Book Recommendations