log
log is not a logging library in itself but an interface or facade for logging. It defines logging macros such as error!, warn!, info!, debug!, and trace!. These macros are used by libraries and applications to produce logs, but log does not handle how these logs are output or managed. Instead, it delegates this responsibility to a backend implementation, such as env_logger or slog.
The separation of the interface (log) from the backend allows libraries to remain agnostic to the specific logging implementation used by the application.
env_logger
env_logger is a simple backend implementation for the log interface. It writes logs to stderr and provides runtime configuration via the RUST_LOG environment variable. This makes it easy to dynamically adjust log levels without modifying or recompiling your code. It’s suitable for smaller or simpler projects.
Example of combining log with env_logger:
use log::{info, warn};
use env_logger;
fn main() {
env_logger::init(); // Initialize the logger backend
info!("Application started");
warn!("This is a warning message");
}slog
slog (Structured Logger) is another backend implementation, but it focuses on structured logging, allowing logs to carry rich metadata in addition to simple text messages. This makes it ideal for applications that require structured data for log analysis, but it may be more complex than env_logger for simple projects.
tracing
tracing is a separate logging framework developed by the Tokio project. It is not compatible with the log interface and uses its own API. In addition to structured logging, tracing introduces the concept of spans, which are essential for asynchronous applications.
Spans
Spans represent a period of time in your program’s execution and allow you to track the flow of execution across asynchronous tasks.
tracingalso provides automatic instrumentation for functions using the#[instrument]attribute.
Span Example:
use tracing::{info, span, Level};
use tracing_subscriber;
#[tokio::main]
async fn main() {
tracing_subscriber::fmt::init(); // Initialize tracing
let parent_span = span!(Level::INFO, "parent_span");
let _enter = parent_span.enter(); // Enter the parent span
async_task().await;
}
async fn async_task() {
let span = span!(Level::DEBUG, "async_task", task_id = 1);
let _enter = span.enter(); // Enter a nested span
info!("Performing async work");
}Here, the log message “Performing async work” is associated with both the async_task span and its parent span, making it easier to trace the program’s flow. With #[instrument], tracing can automatically generate spans for functions and capture their arguments as structured metadata.
use tracing::{info, instrument};
use tracing_subscriber;
#[tokio::main]
async fn main() {
tracing_subscriber::fmt::init();
do_work("example").await;
}
#[instrument]
async fn do_work(task_name: &str) {
info!("Starting work");
}tracing supports structured logging by allowing you to attach key-value pairs to log messages and spans. This makes logs easier to analyze programmatically.
use tracing::{info, span, Level};
fn main() {
let my_span = span!(Level::INFO, "example_span", user_id = 42, action = "login");
let _enter = my_span.enter();
info!("User performed an action");
}Tracing layers and subscribers
The tracing-subscriber crate provides powerful mechanisms to manage and customize the behavior of spans and events in Rust applications. The core concept revolves around layers and subscribers. While subscribers are responsible for managing and collecting spans and events, layers add modular functionality on top of subscribers.
When you use the init() method on a layer (e.g., console_subscriber::ConsoleLayer::init()), the layer is set as the global subscriber. This effectively registers the layer to receive and process all spans and events in your application. Internally this happens:
- Wraps itself in a subscriber (
Registryby default). - Registers the subscriber globally using
tracing::subscriber::set_global_default.
use console_subscriber::ConsoleLayer;
ConsoleLayer::builder()
.retention(std::time::Duration::from_secs(60))
.init();The registry approach allows you to compose multiple layers into a single subscriber. Instead of a single layer owning the global subscriber, the Registry serves as the subscriber, and layers are added to it to extend its functionality.
use tracing_subscriber::{fmt, EnvFilter, Registry};
use tracing_subscriber::prelude::*;
let console_layer = console_subscriber::spawn();
let fmt_layer = fmt::layer();
let filter_layer = EnvFilter::new("debug");
tracing_subscriber::registry()
.with(console_layer)
.with(fmt_layer)
.with(filter_layer)
.init();Common Layer Options in tracing-subscriber
FmtLayer
The FmtLayer provides human-readable output for spans and events. It supports customizable formats, including JSON. This layer is commonly used for basic logging.
use tracing_subscriber::fmt;
tracing_subscriber::fmt()
.with_thread_ids(true) // Include thread IDs
.with_thread_names(true) // Include thread names
.json() // Output logs in JSON format
.init();EnvFilter
The EnvFilter layer enables runtime control over log verbosity based on environment variables. This is especially useful for dynamic filtering without recompiling your application.
use tracing_subscriber::EnvFilter;
let filter_layer = EnvFilter::new("info,my_crate=debug");
tracing_subscriber::fmt()
.with_env_filter(filter_layer)
.init();info,my_crate=debugmeans:- Logs at
infolevel for all crates. - Logs at
debuglevel formy_crate.
- Logs at
RUST_LOG=info,my_crate=debug cargo runConsoleLayer
Part of the console-subscriber crate, this layer provides a live debugging console for asynchronous tasks in Tokio-based applications. It tracks tasks, resources, and spans in real-time.
Example:
use console_subscriber::ConsoleLayer;
let console_layer = ConsoleLayer::builder()
.retention(std::time::Duration::from_secs(60))
.server_addr(([127, 0, 0, 1], 5555))
.spawn();
tracing_subscriber::registry()
.with(console_layer)
.init();OpentelemetryLayer
The OpentelemetryLayer exports tracing data to observability platforms like Jaeger or Zipkin for distributed tracing. It integrates with the opentelemetry crate.
use tracing_opentelemetry::OpenTelemetryLayer;
use tracing_subscriber::{Registry, prelude::*};
use opentelemetry::sdk::export::trace::stdout;
let tracer = stdout::new_pipeline().install_simple();
let telemetry_layer = OpenTelemetryLayer::new(tracer);
tracing_subscriber::registry()
.with(telemetry_layer)
.init();FilterLayer
The FilterLayer allows you to dynamically control which spans and events are recorded. It can filter based on span names, metadata, or other criteria.
use tracing_subscriber::filter::{filter_fn, LevelFilter};
use tracing_subscriber::fmt;
let filter_layer = filter_fn(|metadata| metadata.level() <= &tracing::Level::INFO);
tracing_subscriber::fmt()
.with_filter(filter_layer)
.init();