Skip to content

Wide Events Mixin Server Deno Bun

npm version

Source

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

bash
npm install @loglayer/mixin-wide-events
bash
pnpm add @loglayer/mixin-wide-events
bash
yarn add @loglayer/mixin-wide-events
bash
bun add @loglayer/mixin-wide-events
bash
deno add npm:@loglayer/mixin-wide-events

Why 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

typescript
// 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

typescript
// 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

typescript
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.

typescript
import { createWideEventMixin } from "@loglayer/mixin-wide-events";

const mixin = createWideEventMixin({
  asyncContext: new AsyncLocalStorage(),
});

Configuration Options

Required Parameters

NameTypeDescription
asyncContextAsyncLocalStorage<Record<string, any>>An async context implementation for propagating wide event data across async boundaries.

Optional Parameters

NameTypeDefaultDescription
includeContextbooleantrueInclude data from withContext() calls in the emitted wide event.
wideEventFieldstringundefinedField 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.

typescript
// 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.

typescript
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.

typescript
// 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 removed

emitWideEvent(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.

OptionTypeDefaultDescription
messagestring-The log message for the wide event.
levelLogLevelType"info"The log level for the emission.
metadataRecord<string, any>undefinedAdditional metadata to include in this emission.
typescript
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:

  1. withContext() data (lowest priority)
  2. withWideEvents() data
  3. emitWideEvent({ metadata: {...} }) data (highest priority)

Top-level keys are overwritten by later calls. Nested objects are deep merged.

typescript
// 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
typescript
// 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:

typescript
// async-local-storage.ts
import { AsyncLocalStorage } from "async_hooks";

export const asyncLocalStorage = new AsyncLocalStorage<{
  logger: import("loglayer").ILogLayer;
}>();
typescript
// 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;
}
typescript
// 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:

typescript
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