Use Cases

Libraries and SDKs

Ship evlog inside a reusable package: attribute your events, throw catalog errors, and leave drains and sampling to the host application.

Your package runs inside someone else's process. When it logs, the events it emits land in the host application's stream, pass through the host's redaction, and reach whatever drains the host configured. That division is the whole contract: the library emits, the application configures. This page covers both sides, for a package that ships evlog and for an application that runs one.

Add evlog logging to my library or SDK

initLogger() writes process-wide state shared by every evlog copy of the same major version, and the last call wins. A library that calls it at import time replaces the host's drain, sampling, and redaction for the whole process. Configuration belongs to the application.

Emit without owning the configuration

Use the global log API the same way you would use console.log. It works whether or not the host configured evlog: without configuration it prints (pretty in development, JSON otherwise). The form you call decides how the event travels:

  • The object form, log.info({ source: 'mylib', action: 'sync' }), always becomes a wide event through the host's pipeline: sampled, redacted, and delivered to the drain the host configured.
  • The tagged form, log.info('mylib.client', 'GET /users'), prints through the host's pretty printer when pretty is on (the development default) and reaches neither a drain nor redaction there. In JSON mode it becomes a small wide event carrying tag and message.

If the host may be draining, prefer the object form and keep your package name in a field so the event stays filterable:

src/sync.ts
import { log } from 'evlog'

export function fetchRecords(path: string): void {
  log.info({ source: 'mylib.client', message: `GET ${path}` })
}

Attribute wide events to your package

When an operation deserves one wide event instead of individual messages, build it with createLogger and put the package name in a field the host can query. source is a convention, not an API: pick one field name and document it in your package's README.

src/sync.ts
import { createLogger } from 'evlog'

export function syncRecords(records: number): void {
  const log = createLogger({ source: 'mylib', operation: 'sync-records' })
  log.set({ records: { total: records } })
  // ...
  log.emit()
}

If your fields benefit from compile-time checking, type the context: createLogger<SyncContext>(). Typed Fields covers the patterns.

Contribute to the host's request event

Inside a host application, the framework integration already builds one wide event per request and emits it at the end of the lifecycle. A package that creates its own logger during a request forks that into two unrelated events. Instead, accept the request logger as a parameter and add your context to the event the host already owns:

src/checkout.ts
import type { AuditableLogger } from 'evlog'

export function chargePayment(log: AuditableLogger, amount: number): void {
  log.set({ payment: { amount, provider: 'stripe' } })
  // ...
}

The host resolves the request logger with useLogger() from the framework subpath it uses (evlog/hono, evlog/next, ...) or with the framework-native accessor, then passes it to your package. Every log.set() you call merges into the request's wide event, so the host sees the whole operation in one place. Wide Events explains the accumulation model.

Request loggers from some framework integrations also carry log.fork() for background work that needs its own event, correlated with the parent request. Integrations attach it when they run a logger storage (Hono, oRPC, Express, Fastify, NestJS, SvelteKit, React Router, Next.js, Elysia); Nitro and Nuxt do not attach it yet. A standalone createLogger() instance never has it. Framework Integrations documents what each one attaches.

Throw errors the host can act on

Your package's failures are more useful as structured errors than as bare Error instances. Define a catalog with a prefix your package owns:

src/errors.ts
import { defineErrorCatalog } from 'evlog'

export const errors = defineErrorCatalog('mylib', {
  RATE_LIMITED: {
    status: 429,
    message: 'Too many requests',
    why: 'The upstream provider returned HTTP 429',
    fix: 'Retry with exponential backoff',
  },
})

The wire format is ${prefix}.${KEY}, so a host running several evlog-instrumented packages never sees two error codes collide as long as each package owns one prefix. The Catalogs page covers the packaging recipe for shipping a catalog as its own npm package, including the declare module 'evlog' augmentation block that gives your consumers autocomplete on the codes.

What the host application controls

Everything about where events go, which ones survive, and what gets scrubbed is decided by the application's initLogger() call. Library events pass through the same pipeline as the application's own:

SettingDecidesApplies to library events
enabledWhether anything is emitted at all. When false, every emit and tagged log is a no-op.Yes, all of them
minLevelMinimum severity for the global log API. Does not apply to createLogger().emit().Tagged logs yes, wide events no
samplingHead rates per level, plus keep conditions that force retention.Yes
redactWhich fields and patterns are scrubbed before console output and drains.Yes
drainWhere events are sent.Yes
silentWhether built-in console output is suppressed.Yes

evlog has no category tree that routes one package's events to a different drain. Filter on what the package emits instead: the tag on tagged logs, the source field on wide events, either in your drain or in whatever consumes the events downstream.

Test the events your library emits

Your events are part of your package's public behavior, so test them. Two ways, both relying on the fact that the test process is the host:

Collect events with a drain function in your test setup:

test/events.test.ts
import { initLogger } from 'evlog'
import type { DrainContext, WideEvent } from 'evlog'

const events: WideEvent[] = []

initLogger({ drain: (ctx: DrainContext) => { events.push(ctx.event) } })

// run the operation under test, then assert
// events[0].source === 'mylib'

Or use the memory drain, which keeps events in a ring buffer you can read and clear. Draining is asynchronous: emit() hands the event to the drain without waiting for it, so give the promise a tick before reading or the buffer is still empty:

test/events.test.ts
import { createMemoryDrain, readMemoryLogs } from 'evlog/memory'
import { initLogger } from 'evlog'

initLogger({ drain: createMemoryDrain({ store: 'test' }) })

// run the operation under test, then give the drain a tick
await new Promise(resolve => setTimeout(resolve, 0))

const events = readMemoryLogs({ store: 'test', filter: e => e.source === 'mylib' })

For catalog errors, compare on the factory's code property rather than a string literal, so a rename becomes a TypeScript error: expect(err.code).toBe(errors.RATE_LIMITED.code). Sampling covers what the host's sampling configuration can do to events, including dropping them entirely.

Where to go next

If your package wraps a framework instead of running inside one, Extend Overview maps the extension points: custom drains, enrichers, and plugins follow the same emit-only contract. To debug a host application that uses your package, the host's stream and diagnostics channel let you subscribe to every event in the process, including yours.