Zum Inhalt springen
Deutsch

LogContext

Dieser Inhalt ist noch nicht in deiner Sprache verfügbar.

const LogContext: object

Defined in: src/LogContext.ts:89

The LogContext namespace exposes the MDC operations. The class- style LogContext.run(...) shape follows the MDC pattern — a scoped, thread-local-like map of diagnostic context that propagates through every log line inside the callback — and keeps the public API tight without exporting the underlying AsyncLocalStorage instance.

get(): LogContextData

Read the current context. Returns the frozen empty object when called outside any run — never undefined, never null, so callers can .entries() over it without guarding.

LogContextData

isEmpty(context): boolean

Does this context carry nothing?

For the envelope builders, which ask once per tell and are the reason this exists (#411). Object.keys(context).length === 0 answers the same question and allocates an array to do it — on the overwhelmingly common path, where no run is active at all, purely to discover that a frozen empty object is empty.

The identity check is therefore a fast path in front of the general one, not a replacement for it. LogContext.run({}, cb) installs a store that is empty but is not EMPTY, and callers must keep omitting context for it: attaching {} instead would route every such message through LogContext.run(env.context, …) on delivery, adding an AsyncLocalStorage frame per message where the point was to remove work.

LogContextData

boolean

run<T>(context, callback): T

Run callback with context as the current context. The previous context (if any) is shadowed for the duration of the call and restored automatically. Sync and async callback both work — AsyncLocalStorage preserves the binding across awaits.

T

LogContextData

() => T

T

runEach<TItem>(entries, callback): Promise<void>

Process entries sequentially, each under the context captured when that entry was enqueued.

This is the batching counterpart to LogContext.runFresh. runFresh is right when the deferred work belongs to nobody; runEach is right when it belongs to someone specific per item — a mailbox drained in one turn, a flush of buffered writes, a batch of requests coalesced across tenants. Handling such a batch under one ambient context attributes every item to whichever request happened to trigger the flush.

The capture must happen at enqueue time, since that is the only moment the item’s own context is still current:

queue.push({ context: LogContext.get(), item: job }); // …later, in some other turn: await LogContext.runEach(queue.splice(0), (job) => this.handle(job));

Returns Promise<void> by design. Collecting results would make this read like Promise.all and invite callers to treat it as a concurrency helper, which it is not — the ordering and the one-scope-per-item guarantee are the product, and the payload of each call is a side effect (a log line, a downstream tell). Anything worth returning is worth writing where the batch was built.

Errors propagate immediately and abandon the remaining entries. Swallowing them would be a supervision policy, and that decision does not belong to a diagnostics primitive; a caller who wants per-item isolation puts the try/catch inside callback, where it still runs under the right context.

TItem

Iterable<LogContextEntry<TItem>>

(item) => unknown

Promise<void>

runFresh<T>(callback): T

Run callback with the context explicitly emptied, shadowing whatever was ambient. The inverse of LogContext.with: where with inherits, this deliberately does not.

Reach for it at the seam where work stops belonging to the caller that happened to start it — a background drain loop, a retry timer, a queue consumer, anything kicked off with a bare un-awaited promise. Without it, AsyncLocalStorage hands that work the context of whichever turn created it, and every tell it makes stamps that context onto the envelope; if the work then serves another tenant, the first tenant’s identifiers travel with it. Starting from empty is cheaper to reason about than remembering to strip individual keys, and it fails safe: a field nobody set cannot leak.

The framework applies it to its own seams of that shape (#718): a dispatcher turn (ActorCell.runReported), a fired schedule (Scheduler), and an inbound cluster frame that carries no context (Cluster.onEnvelope). So the deferred work still needing this by hand is the work you defer — an un-awaited promise, a buffer flushed later, a raw setTimeout — not anything the framework hands you.

No store means no wrapper. With nothing ambient, get() already returns EMPTY, so running callback bare is observably identical to running it inside storage.run(EMPTY, …) — and it keeps the runtime’s no-store fast path, which is what makes the framework’s own three seams affordable. The cost of an active store is not the run call: it is that every async resource created under one propagates it, so wrapping a turn unconditionally taxed each await inside it. Measured on benchmarks/single-node/tell-throughput.ts, an unconditional wrapper cost 3-8 % of tell throughput in a process with no MDC at all; this guard takes that back, and a process that does open scopes pays only where there is something to shadow.

T

() => T

T

snapshot(): Record<string, string | number | boolean>

Capture the current context as a plain (mutable-by-the-caller) object — useful when you need to pass the context through a boundary that strips Readonly (e.g. a JSON serialiser). Returns a fresh copy every call.

Record<string, string | number | boolean>

with<T>(extra, callback): T

Run callback with extra fields merged into the current context. Equivalent to run({ ...get(), ...extra }, callback) but a touch shorter at call sites that just want to add a field.

T

LogContextData

() => T

T