Skip to main content

Crate context_logger

Crate context_logger 

Source
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:

  1. Static default fields (e.g. service, version)
  2. Computed default fields (e.g. timestamp, level)
  3. Inherited fields from all parent scopes
  4. 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 by log::info!, log::warn!, etc. Each record carries a log::Record::key_values source that consumers visit to extract structured attributes.
  • Field — a key-value pair (&str, log::kv::Value) attached to a log record’s Source. Fields carry scoped context like request_id or user_id.
  • LogFields — a collection of fields that implements the extension logic for the log record’s source. When the record flows through ContextLogger, its fields are merged into the record’s own Source.
  • LogContext — the blueprint for fields. It splits fields into local and inherited categories with different propagation semantics.
  • LogScope guard — activates a LogContext, 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§

ContextLogger
A logger wrapper that enhances log::Record with 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§

LogContextExt
Extension trait for LogContext to run code within a temporary logging scope.