Wide Events Mixin Server Deno Bun
The Wide Events Mixin adds functionality for creating comprehensive, self-contained log entries that capture an entire operation's context and data in a single emission. This pattern is sometimes called "canonical log lines" or "wide events."
For a complete Express example, see the Wide Events Logging guide.
Installation
npm install @loglayer/mixin-wide-eventspnpm add @loglayer/mixin-wide-eventsyarn add @loglayer/mixin-wide-eventsbun add @loglayer/mixin-wide-eventsdeno add npm:@loglayer/mixin-wide-eventsWhy Wide Events?
Wide events solve a common observability problem: when you have distributed systems with many log entries, it can be difficult to correlate all the data from a single operation. Wide events capture everything in one place, making it easy to:
- See the complete context of an operation at a glance
- Correlate all data without joining multiple log entries
- Simplify log analysis and debugging
For more context, see Why Logging Sucks.
Quick Start
1. Create the AsyncLocalStorage instance
// async-local-storage.ts
import { AsyncLocalStorage } from "async_hooks";
import type { ILogLayer } from "loglayer";
export const asyncLocalStorage = new AsyncLocalStorage<{
logger: ILogLayer;
}>();2. Create a helper to get the logger
// logger.ts
import { asyncLocalStorage } from "./async-local-storage";
import { LogLayer, StructuredTransport, useLogLayerMixin } from "loglayer";
import { createWideEventMixin } from "@loglayer/mixin-wide-events";
// Register the mixin once
useLogLayerMixin(createWideEventMixin({ asyncContext: asyncLocalStorage }));
// Create LogLayer
export const log = new LogLayer({
transport: new StructuredTransport({ logger: console }),
});
// Helper to get logger from async context
export function getLogger() {
return asyncLocalStorage.getStore()?.logger ?? log;
}3. Use in your code
getLogger().withWideEvents({ userId: "123" });
await doSomething();
getLogger().withWideEvents({ orderId: "456" });
getLogger().emitWideEvent({ message: "Order processed" });API
createWideEventMixin(options)
Creates a wide event mixin that can be registered with LogLayer.
import { createWideEventMixin } from "@loglayer/mixin-wide-events";
const mixin = createWideEventMixin({
asyncContext: new AsyncLocalStorage(),
});Configuration Options
Required Parameters
| Name | Type | Description |
|---|---|---|
asyncContext | AsyncLocalStorage<Record<string, any>> | An async context implementation for propagating wide event data across async boundaries. |
Optional Parameters
| Name | Type | Default | Description |
|---|---|---|---|
includeContext | boolean | true | Include data from withContext() calls in the emitted wide event. |
wideEventField | string | undefined | Field name to nest all wide event data under. When undefined, data is flattened at root level. |
withWideEvents(data)
(data: Record<string, any>) => this
Accumulates data into the wide event. Call multiple times to build up the event. Nested objects are deep merged - later calls merge into existing nested objects rather than replacing them entirely. Returns the logger for chaining.
// Simple accumulation
logger.withWideEvents({ userId: "123" });
await doSomething();
logger.withWideEvents({ orderId: "456" });
// Nested objects are merged:
logger.withWideEvents({ user: { id: "123" } });
logger.withWideEvents({ user: { name: "Alice" } });
// Result: { user: { id: "123", name: "Alice" } }
// Or chain with other methods
log.child()
.withContext({ requestId: "123" })
.withWideEvents({ userId: "456" })
.info("Processing");getWideEvents(key?)
(key?: string) => Record<string, any> | any
Retrieves the currently accumulated wide event data. Returns undefined if called outside async context or if the key doesn't exist.
logger.withWideEvents({ userId: "123" });
logger.withWideEvents({ orderId: "456" });
// Get all accumulated data
const data = logger.getWideEvents();
// { userId: "123", orderId: "456" }
// Get specific key
const userId = logger.getWideEvents("userId");
// "123"clearWideEvents(key?)
(key?: string) => this
Clears the accumulated wide event data. Optionally clear only a specific key. Returns the logger for chaining.
// Clear all data
logger.withWideEvents({ first: "data" });
logger.emitWideEvent({ message: "First" });
logger.clearWideEvents();
logger.withWideEvents({ second: "data" });
logger.emitWideEvent({ message: "Second" });
// Clear specific key
logger.withWideEvents({ user: { id: "123", name: "Alice" } });
logger.clearWideEvents("user");
// Result: user object is removedemitWideEvent(config)
(config: { message: string; level?: LogLevelType; metadata?: Record<string, any> }) => this
Emits the accumulated wide event as a single log entry. Returns the logger for chaining.
| Option | Type | Default | Description |
|---|---|---|---|
message | string | - | The log message for the wide event. |
level | LogLevelType | "info" | The log level for the emission. |
metadata | Record<string, any> | undefined | Additional metadata to include in this emission. |
logger.emitWideEvent({ message: "Order processed" });
// Or chain with other operations
logger
.withWideEvents({ statusCode: 200 })
.emitWideEvent({ message: "Request completed" })
.info("After emitting");Data Priority
When multiple sources provide the same key, the following priority order applies:
withContext()data (lowest priority)withWideEvents()dataemitWideEvent({ metadata: {...} })data (highest priority)
Top-level keys are overwritten by later calls. Nested objects are deep merged.
// Top-level keys: later calls win
logger.withWideEvents({ key: "first" });
logger.withWideEvents({ key: "second" });
// Result: key = "second"
// Nested objects: merged together
logger.withWideEvents({ user: { id: "123" } });
logger.withWideEvents({ user: { name: "Alice" } });
// Result: { user: { id: "123", name: "Alice" } }
// Priority example
logger.withContext({ key: "from-context" });
logger.withWideEvents({ key: "from-wideEvents" });
logger.emitWideEvent({ message: "test" });
// Result: key = "from-wideEvents" (wideEvents overrides context)Interaction with withMetadata()
withMetadata() and withWideEvents() serve different purposes:
withMetadata()- Adds metadata to an immediate log entry (not accumulated into wide events)withWideEvents()- Accumulates data for the final wide event emission
// withMetadata() logs immediately with the data
logger.withMetadata({ userId: "123" }).info("User action");
// Output: { msg: "User action", userId: "123" }
// withWideEvents() accumulates for emitWideEvent()
logger.withWideEvents({ orderId: "456" });
logger.emitWideEvent({ message: "Wide event" });
// Output: { msg: "Wide event", orderId: "456" }
// They work independently and can be used together
logger.withMetadata({ debug: true }).debug("Debug info");
logger.withWideEvents({ businessData: "value" });
logger.emitWideEvent({ message: "Complete" });
// Intermediate log: { msg: "Debug info", debug: true }
// Wide event: { msg: "Complete", businessData: "value" }Complete Example
Here's a complete Express middleware example:
// async-local-storage.ts
import { AsyncLocalStorage } from "async_hooks";
export const asyncLocalStorage = new AsyncLocalStorage<{
logger: import("loglayer").ILogLayer;
}>();// logger.ts
import { asyncLocalStorage } from "./async-local-storage";
import { LogLayer, StructuredTransport, useLogLayerMixin } from "loglayer";
import { createWideEventMixin } from "@loglayer/mixin-wide-events";
useLogLayerMixin(createWideEventMixin({ asyncContext: asyncLocalStorage }));
export const log = new LogLayer({
transport: new StructuredTransport({ logger: console }),
});
export function getLogger() {
return asyncLocalStorage.getStore()?.logger ?? log;
}// app.ts
import express from "express";
import { asyncLocalStorage } from "./async-local-storage";
import { log, getLogger } from "./logger";
const app = express();
// Set up context per request
app.use((req, res, next) => {
const logger = log.child().withContext({ requestId: req.id });
asyncLocalStorage.run({ logger }, next);
});
// Route handler - no wrapping needed!
app.get("/orders/:id", (req, res) => {
getLogger().debug("Fetching order");
const order = getOrder(req.params.id);
getLogger().withWideEvents({ orderId: order.id, items: order.items.length });
res.on("finish", () => {
getLogger().withWideEvents({ statusCode: res.statusCode });
getLogger().emitWideEvent({ message: "Request completed" });
});
res.json(order);
});Browser Compatibility
The AsyncLocalStorage class is from Node.js and not available in browsers. For browser environments, provide your own compatible async context implementation:
class BrowserAsyncContext<T> {
run(data: T, callback: () => void) {
this.store = data;
callback();
}
getStore(): T | undefined {
return this.store;
}
private store: T | undefined;
}
const browserContext = new BrowserAsyncContext();
const mixin = createWideEventMixin({ asyncContext: browserContext });See Also
- Wide Events Logging Guide - A comprehensive guide to implementing wide event logging
- AsyncLocalStorage - Node.js documentation
