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_SLOTSfields (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§
- Context
Guard - RAII guard that removes a context entry when dropped.
- Context
Snapshot - An owned, immutable copy of a set of context fields.
- With
Context - A future with a
ContextSnapshotinstalled around every poll. Created byContextSnapshot::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_SLOTScap). - with_
field - Push a key/value pair onto the current thread’s context.
- with_
request_ id - Push a
request_idfield. Seewith_field. - with_
snapshot - Run
fwith the current thread’s context as a borrowed slice ofFields, oldest first, with shadowed keys removed. - with_
trace_ id - Push a
trace_idfield. Seewith_field.