Expand description
§Overview
A small structured context propagation layer for the standard Rust log
ecosystem.
context-logger keeps your existing log::info!, log::warn!, etc. calls
unchanged and adds scoped structured context on top — inherited request-level
fields, local operation fields, computed defaults, and async-safe propagation.
§How it works
When a log record flows through ContextLogger, fields are resolved in this
order:
- Static default fields (e.g.
service,version) - Computed default fields (e.g.
timestamp,level) - Inherited fields from all parent scopes
- Local fields of the active scope
These fields are merged into the log::Record’s key-value store — “last
write wins” for duplicate keys.
§Concepts
The log crate models structured logging through a log::kv::Source — a
push-based iterator over key-value pairs attached to a log::Record.
Context logger adds an additional layer that injects scoped fields into that
source.
log::Record— the log entry produced bylog::info!,log::warn!, etc. Each record carries alog::Record::key_valuessource that consumers visit to extract structured attributes.- Field — a key-value pair (
&str,log::kv::Value) attached to a log record’sSource. Fields carry scoped context likerequest_idoruser_id. LogFields— a collection of fields that implements the extension logic for the log record’s source. When the record flows throughContextLogger, its fields are merged into the record’s ownSource.LogContext— the blueprint for fields. It splits fields intolocalandinheritedcategories with different propagation semantics.LogScopeguard — activates aLogContext, pushing its fields onto the thread-local scope stack. Fields are resolved when a log record flows through the logger.
The scope stack is thread-local: each thread maintains its own independent stack ensuring thread-safety without expensive synchronization.
§Compatibility
ContextLogger wraps any type implementing log::Log. For structured
key-value output, pair it with a logger that supports the kv feature (e.g.
env_logger with features = ["kv"] or log4rs).
§Basic example
use context_logger::{ContextLogger, LogContext, LogContextExt as _};
use log::info;
fn main() {
// Create an underlying logger instance.
let env_logger = env_logger::builder()
.filter_level(log::LevelFilter::Info)
.build();
let filter = env_logger.filter();
// Wrap it with ContextLogger to enable context propagation.
let logger = ContextLogger::new(env_logger)
// Add static default field (static field).
.with_default_field("service", "api")
// Add dynamic default field (per-event field).
.with_default_field_fn("timestamp", |log_record| {
chrono::Utc::now().to_rfc3339()
})
.with_default_field_fn("level", |log_record| log_record.level().to_string());
// Initialize the resulting logger.
logger.init(filter);
// Create a context with properties.
let context = LogContext::new()
// Field that will be inherited by child contexts.
.with_inherited_field("request_id", "req-123")
.with_inherited_field("user_id", 42)
.with_local_field("http_method", "GET");
// Use the context.
context.in_scope(|| {
// Log with context automatically attached:
// service=api timestamp=... level=INFO request_id=req-123 user_id=42
info!("Processing request");
})
}§Async Context Propagation
Context logger supports async functions and can propagate log context across
.await points.
use context_logger::{ContextLogger, LogContext, FutureExt, LogScope};
use log::info;
async fn process_user_data(user_id: &str) {
let context = LogContext::new()
.with_inherited_field("user_id", user_id);
async {
info!("Processing user data"); // Includes user_id
// Context automatically propagates through .await points.
fetch_user_preferences().await;
info!("User data processed"); // Still includes user_id
}
.in_log_context(context)
.await;
}
async fn fetch_user_preferences() {
// Add additional field for this operation.
LogScope::add_local_field("operation", "fetch_preferences");
info!("Fetching preferences"); // Includes both user_id and operation
}
async fn spawn_background_job(user_id: &str) {
let context = LogContext::new()
.with_inherited_field("user_id", user_id);
async {
// The scope stack is thread-local: capture the active context
// before crossing the task boundary with tokio::spawn.
let context = LogScope::current_context();
tokio::spawn(
async move {
info!("Running background job"); // Includes user_id
}
.in_log_context(context),
)
.await
.unwrap();
}
.in_log_context(context)
.await;
}Re-exports§
pub use self::future::FutureExt;
Modules§
- future
- Future types.
Structs§
- Context
Logger - A logger wrapper that enhances
log::Recordwith scoped fields. - LogContext
- A set of fields that can be attached to a logging scope.
- LogFields
- A set of key-value fields that can be attached to a logging scope.
- LogScope
- A guard that represents an active logging context on the current thread’s scope stack.
- LogValue
- Represents a value that can be stored in a log field.
Traits§
- LogContext
Ext - Extension trait for
LogContextto run code within a temporary logging scope.