Skip to main content

Module context

Module context 

Source
Available on crate feature std only.
Expand description

Thread-local logging context.

Context carries ambient key-value data attached to every record emitted on the current thread: stash a trace_id or request_id at the start of a request and every log line below it carries the value. with_field (and friends) push a value and return a ContextGuard that removes it when dropped.

§Rules

  • Shadowing. When two active entries share a key, the most recent one wins and the record carries the key once.
  • Capacity. A snapshot carries at most MAX_CONTEXT_SLOTS fields (the most recent ones) and the thread keeps at most that many entries; pushing beyond it evicts the oldest entry, whose guard then becomes a no-op.
  • Reentrancy. Sinks and formats may log, push context, or drop guards while a record is being dispatched. Changes made during dispatch take effect when the outermost dispatch on the thread finishes; a record emitted from inside a sink sees the context that was active when the outer record started.
  • Thread teardown. Guards dropped while thread-local storage is being destroyed do nothing instead of aborting the process.

§Async tasks

Context is per thread, and a task in a work-stealing runtime can resume on a different thread after every .await. That is why ContextGuard is neither Send nor Sync: a guard held across an .await makes the future !Send, so tokio::spawn and similar reject it at compile time instead of silently attaching one request’s fields to another request’s records.

For tasks, capture the context as a ContextSnapshot (an owned, Send + Sync, cheaply clonable value) and re-install it where the work runs:

use log_io::context;

let _req = context::with_request_id("req-7");
let snapshot = context::capture();

std::thread::spawn(move || {
    // Re-install on the worker thread for the duration of a closure.
    snapshot.scope(|| {
        assert_eq!(context::len(), 1);
    });
})
.join()
.unwrap();

For futures, ContextSnapshot::wrap re-installs the snapshot around every poll, which works with any executor:

ⓘ
let snapshot = log_io::context::capture();
tokio::spawn(snapshot.wrap(async move {
    handle_request().await; // every record carries the captured fields
}));

!Send only helps where the executor requires Send. Futures that share one thread without that requirement (join! or select! inside a task, LocalSet / spawn_local, block_on) can each hold a guard across an .await, and an unwrapped future that does so leaves its fields on the thread while it is suspended: every future polled on that thread in the meantime logs them. Wrap such futures with ContextSnapshot::wrap (use ContextSnapshot::default() when there is nothing to capture). A wrapped future keeps the guards it holds across an .await with itself between polls, so its fields are visible exactly while it is being polled. The rule of thumb: never hold a ContextGuard across an .await in a future that is not wrapped.

Structs§

ContextGuard
RAII guard that removes a context entry when dropped.
ContextSnapshot
An owned, immutable copy of a set of context fields.
WithContext
A future with a ContextSnapshot installed around every poll. Created by ContextSnapshot::wrap.

Constants§

MAX_CONTEXT_SLOTS
Maximum number of context fields attached to one record, and the maximum number of live context entries per thread.

Functions§

capture
Capture the current thread’s context as an owned snapshot.
clear
Remove every context entry on the current thread. Existing guards become no-ops.
len
Number of context fields a record emitted now would carry (after shadowing and the MAX_CONTEXT_SLOTS cap).
with_field
Push a key/value pair onto the current thread’s context.
with_request_id
Push a request_id field. See with_field.
with_snapshot
Run f with the current thread’s context as a borrowed slice of Fields, oldest first, with shadowed keys removed.
with_trace_id
Push a trace_id field. See with_field.