Loading pageā¦
Rust walkthroughs
Loading pageā¦
tracing_subscriber, how do fmt::Layer and EnvFilter compose for structured logging?fmt::Layer handles formatting and outputting log events while EnvFilter controls which spans and events pass through based on environment variables like RUST_LOG. These layers compose through the Layer trait, forming a pipeline where each layer can inspect, modify, or filter events. EnvFilter acts as a gate that decides which events reach fmt::Layer, while fmt::Layer formats and writes the events that pass through. Together they provide configurable structured logging: EnvFilter reduces noise by filtering events, and fmt::Layer controls the output format including JSON support for structured logging in production.
use tracing_subscriber;
fn main() {
// Simple subscriber with fmt layer
tracing_subscriber::fmt()
.init();
// Equivalent to:
tracing_subscriber::registry()
.with(tracing_subscriber::fmt::layer())
.init();
tracing::info!("Hello from tracing");
}fmt::Layer provides human-readable output to stdout by default.
use tracing_subscriber;
fn main() {
// Combine fmt layer with EnvFilter
tracing_subscriber::fmt()
.with_env_filter("debug") // Show debug and above
.init();
tracing::debug!("Debug message");
tracing::info!("Info message");
tracing::warn!("Warn message");
}
// Run with RUST_LOG environment variable:
// RUST_LOG=my_app=debug cargo run
// RUST_LOG=info cargo run
// RUST_LOG=debug,my_module=trace cargo runEnvFilter reads RUST_LOG environment variable to configure filtering.
use tracing_subscriber::{registry, fmt, EnvFilter};
fn main() {
// Explicit layer composition
let fmt_layer = fmt::layer()
.with_target(false)
.with_thread_ids(true);
let env_filter = EnvFilter::from_default_env()
.add_directive("my_app=debug".parse().unwrap());
registry()
.with(env_filter)
.with(fmt_layer)
.init();
tracing::info!("Layers composed");
}Layers are added with .with() and execute in order: filter then format.
use tracing_subscriber::EnvFilter;
fn main() {
// From environment variable (RUST_LOG)
let filter1 = EnvFilter::from_default_env();
// From specific environment variable
let filter2 = EnvFilter::try_from_env("MY_LOG_VAR")
.unwrap_or_else(|_| EnvFilter::new("info"));
// Hard-coded default with env override
let filter3 = EnvFilter::try_from_default_env()
.or_else(|_| EnvFilter::new("warn").ok())
.unwrap();
// Multiple directives
let filter4 = EnvFilter::new("info")
.add_directive("my_app=debug".parse().unwrap())
.add_directive("hyper=warn".parse().unwrap());
}EnvFilter supports defaults, environment variables, and programmatic configuration.
use tracing_subscriber::EnvFilter;
fn main() {
// Directive syntax: target[span{field}=value]
// Level only
let filter = EnvFilter::new("debug");
// Module target
let filter = EnvFilter::new("my_app::module=debug");
// Wildcard modules
let filter = EnvFilter::new("my_app::http::*=info");
// Span filtering
let filter = EnvFilter::new("my_app[request{path=\"/api\"}]=trace");
// Multiple directives
let filter = EnvFilter::new("debug,hyper=warn,tokio=off");
// Level meanings:
// trace > debug > info > warn > error
// "my_app=debug" shows debug, info, warn, error
// "my_app=off" disables all for that target
}Directives control which events pass through at each level.
use tracing_subscriber::fmt;
fn main() {
tracing_subscriber::fmt()
// Output destination
.with_writer(std::io::stderr)
// Include/exclude fields
.with_target(true)
.with_thread_ids(true)
.with_thread_names(false)
.with_line_number(true)
.with_file(true)
// Format options
.with_level(true)
.with_ansi(true) // Colors
// Compact format
.compact()
.init();
tracing::info!("Formatted output");
}fmt::Layer has many formatting options for human-readable output.
use tracing_subscriber::fmt;
fn main() {
tracing_subscriber::fmt()
.json() // Output as JSON
.with_current_span(true)
.with_span_list(true)
.with_target(true)
.with_level(true)
.init();
tracing::info!(
user_id = 42,
action = "login",
"User logged in"
);
}.json() outputs structured logs suitable for log aggregation systems.
use tracing_subscriber::{fmt, EnvFilter};
fn main() {
let filter = EnvFilter::try_from_default_env()
.unwrap_or_else(|_| EnvFilter::new("info"));
let fmt_layer = fmt::layer()
.with_target(true)
.with_thread_ids(false)
.compact();
tracing_subscriber::registry()
.with(filter)
.with(fmt_layer)
.init();
// Only info and above passes to fmt layer
tracing::debug!("Hidden by filter"); // Not shown
tracing::info!("Visible"); // Shown
}Filter executes before formatter, reducing output.
use tracing_subscriber::{fmt, EnvFilter, Layer};
fn main() {
// Each layer can have its own filter
let stdout_layer = fmt::layer()
.with_filter(EnvFilter::new("info"));
let stderr_layer = fmt::layer()
.with_writer(std::io::stderr)
.with_filter(EnvFilter::new("error"));
tracing_subscriber::registry()
.with(stdout_layer)
.with(stderr_layer)
.init();
// Info goes to stdout, errors to both stdout and stderr
tracing::info!("Info message"); // stdout only
tracing::error!("Error message"); // stdout and stderr
}Each layer can filter independently.
use tracing_subscriber;
fn main() {
tracing_subscriber::fmt()
.json()
.with_env_filter("debug")
.init();
// Structured fields become JSON keys
tracing::info!(
user_id = %12345,
email = "user@example.com",
logged_in = true,
"User authenticated"
);
// Spans add context
let span = tracing::info_span!("http_request", method = "GET", path = "/api/users");
let _guard = span.enter();
tracing::info!("Processing request");
}Structured fields integrate with JSON output for log aggregation.
use tracing_subscriber::EnvFilter;
fn main() {
// Filter based on span fields
let filter = EnvFilter::new("debug")
.add_directive("my_app[request{path=\"/api/*\"}]=trace".parse().unwrap());
tracing_subscriber::registry()
.with(filter)
.with(tracing_subscriber::fmt::layer())
.init();
let span = tracing::info_span!("request", path = "/api/users");
let _guard = span.enter();
tracing::trace!("This shows because path matches filter");
}EnvFilter can filter based on span field values.
use tracing_subscriber::{registry, fmt, EnvFilter, Layer};
fn main() {
// Order: filter -> formatter
// Events flow through layers in reverse order of .with() calls
// This works correctly:
registry()
.with(EnvFilter::new("info")) // Applied second (after fmt)
.with(fmt::layer()) // Applied first
.init();
// Layer execution order:
// 1. fmt::layer receives event
// 2. EnvFilter decides if it should be written
// For per-layer filtering, use .with_filter() on individual layers
}Layers are composed in reverse order of .with() calls.
use tracing_subscriber::{fmt, EnvFilter};
fn init_logging() {
let filter = EnvFilter::try_from_default_env()
.unwrap_or_else(|_| {
// Production default: warn, but info for our app
EnvFilter::new("warn")
.add_directive("my_app=info".parse().unwrap())
});
let fmt_layer = fmt::layer()
.json()
.with_target(true)
.with_thread_names(true)
.with_line_number(true);
tracing_subscriber::registry()
.with(filter)
.with(fmt_layer)
.init();
}
fn main() {
init_logging();
tracing::info!("Application started");
}Use environment variable overrides with sensible defaults for production.
use tracing_subscriber::{fmt, EnvFilter};
#[cfg(debug_assertions)]
fn init_logging() {
// Development: human-readable, verbose
tracing_subscriber::fmt()
.with_env_filter("debug")
.with_target(true)
.with_file(true)
.with_line_number(true)
.pretty() // Multi-line formatted output
.init();
}
#[cfg(not(debug_assertions))]
fn init_logging() {
// Production: JSON, filtered
tracing_subscriber::fmt()
.with_env_filter(EnvFilter::new("info")
.add_directive("my_app=info".parse().unwrap()))
.json()
.with_target(true)
.with_current_span(true)
.init();
}
fn main() {
init_logging();
tracing::info!("Configured appropriately");
}Different configurations for development and production environments.
use tracing_subscriber::{fmt, EnvFilter, layer::SubscriberExt};
use tracing::collect::with_default;
#[test]
fn test_with_logging() {
// Use a test-specific subscriber
let subscriber = tracing_subscriber::registry()
.with(EnvFilter::new("debug"))
.with(fmt::layer().with_test_writer()); // Captures for test output
with_default(subscriber, || {
tracing::info!("Test log");
});
}with_test_writer() captures logs for test output.
use std::{fs::File, sync::Arc};
use tracing_subscriber::{fmt, EnvFilter};
fn main() {
let file = File::create("app.log").unwrap();
// Console output for info and above
let console = fmt::layer()
.with_filter(EnvFilter::new("info"));
// File output for debug and above
let file_layer = fmt::layer()
.with_writer(Arc::new(file))
.with_filter(EnvFilter::new("debug"));
tracing_subscriber::registry()
.with(console)
.with(file_layer)
.init();
tracing::debug!("File only");
tracing::info!("Both file and console");
tracing::error!("Both file and console");
}Multiple layers can write to different destinations with different filters.
use tracing_subscriber::{registry, fmt, EnvFilter, Layer};
use tracing::{Event, Metadata};
fn main() {
// Custom layer to demonstrate event flow
struct DebugLayer;
impl<S: tracing::Subscriber> Layer<S> for DebugLayer {
fn on_event(&self, event: &Event, _ctx: tracing_subscriber::layer::Context<'_, S>) {
println!("Event seen: {}", event.metadata().name());
}
}
registry()
.with(DebugLayer) // 2. Custom layer
.with(EnvFilter::new("info")) // 3. Filter
.with(fmt::layer()) // 4. Format
.init();
// Flow: fmt::layer -> EnvFilter -> DebugLayer
tracing::debug!("Filtered out"); // Never reaches DebugLayer
tracing::info!("Passes through"); // Reaches all layers
}Understanding layer order helps debug logging issues.
use tracing_subscriber::EnvFilter;
use std::sync::Arc;
fn main() {
let filter = EnvFilter::new("info");
let filter_handle = filter.clone(); // For dynamic changes
tracing_subscriber::registry()
.with(filter)
.with(tracing_subscriber::fmt::layer())
.init();
// Can't directly modify EnvFilter after init
// For dynamic changes, use ReloadFilter or similar approaches
tracing::info!("Static level");
}For dynamic log level changes, use ReloadFilter or similar.
| Feature | fmt::Layer | EnvFilter |
|---------|-------------|--------------|
| Purpose | Format and output | Filter events |
| Position | Output layer | Filter layer |
| Configuration | Format options, writer | Level directives, env vars |
| JSON support | Yes (.json()) | N/A (filtering only) |
| Common use | Format logs for display | Control verbosity |
fmt::Layer and EnvFilter compose to provide structured, configurable logging:
fmt::Layer handles how log events are formatted and where they're written. It supports human-readable formats (.compact(), .pretty()) and machine-readable JSON (.json()). Configuration controls which metadata fields appear: target, thread ID, line numbers, etc.
EnvFilter controls which events pass through to subsequent layers. It reads RUST_LOG (or a custom env var) for directive-based filtering: my_app=debug enables debug level for a module, trace enables everything, off disables all output.
Composition works through the layer system: events flow through layers in reverse order of .with() calls. Typically, EnvFilter gates events and fmt::Layer formats those that pass. This separation allows multiple output layers with independent filtersāe.g., warnings to console, debug to file.
Key insight: The layer architecture separates concerns cleanly. EnvFilter answers "what should be logged?" while fmt::Layer answers "how should it look?" This enables powerful combinations like JSON output for production (parseable by log aggregators) with human-readable output for development, all controlled by environment variables for runtime configuration without code changes.