# context-logger
[](https://crates.io/crates/context-logger)
[](https://docs.rs/context-logger)
[](LICENSE)
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.
## Why `context-logger`?
The standard `log` crate is a small and widely used logging facade, but it does
not provide scoped context propagation by itself. This becomes noisy when every
log line should carry fields like `request_id`, `user_id`, `tenant_id`, or
`operation`.
`context-logger` fills this gap by adding a small runtime context layer on top
of ordinary `log` calls:
- **Works with existing `log` calls** — keep using `log::info!`, `log::warn!`,
and your existing logger backend.
- **Fully dynamic context** — create, extend, merge, and pass context as
ordinary runtime data. Add fields at any point in your code without declaring
spans.
- **Scoped structured fields** — attach request-level fields, operation-level
fields, and default fields to every log record, with explicit control over
what is inherited by child scopes.
- **Async propagation** — carry context through `.await` with `FutureExt`.
### Is this a replacement for `tracing`?
> No. `context-logger` is focused on logs, not traces.
`tracing` puts logs and spans into one instrumentation model.
`context-logger` keeps logs as regular `log` records and adds scoped structured
context to them. Context remains ordinary runtime data, so existing
`log::info!`, `log::warn!`, etc. calls stay unchanged.
> What about traces?
Use a dedicated tracing library such as [`fastrace`].
This keeps the two concerns separate:
- `context-logger` enriches ordinary `log` records with scoped structured
context;
- `fastrace` records timeline spans and exports traces through OpenTelemetry.
Together they cover structured logs and distributed traces without requiring all
logging to go through the `tracing` instrumentation model.
## Example output
```bash
cargo run --example log_async | jq
{
"instance": "contexted_log_async",
"level": "INFO",
"message": "Initialized context logger",
"target": "log_async",
"timestamp": 1784151962445
}
{
"instance": "contexted_log_async",
"level": "INFO",
"message": "Logging in",
"target": "log_async",
"thread_name": "first_future",
"timestamp": 1784151962445,
"user_id": "12345"
}
{
"action": {
"action": "login",
"name": "user"
},
"instance": "contexted_log_async",
"level": "INFO",
"message": "User logged in successfully",
"target": "log_async",
"thread_name": "first_future",
"timestamp": 1784151962445
}
```
## Usage
### Basic Example
Add `context-logger` to your `Cargo.toml`:
```toml
[dependencies]
chrono = "0.4"
context-logger = "0.2"
log = { version = "0.4", features = ["kv_serde"] }
env_logger = { version = "0.11", features = ["kv"] }
```
Then, you can use it in your code:
```rust
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.
```rust
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;
}
```
## License
This project is licensed under the MIT License. See the [LICENSE] file for
details.
[`log`]: https://crates.io/crates/log
[LICENSE]: ./LICENSE
[`fastrace`]: https://docs.rs/fastrace