Emitting Events
Your package runs inside someone else's process. When it logs, the events land in the host's stream, pass through the host's redaction, and reach whatever drains the host configured. This page covers emitting so the host can tell your events from its own, in four situations: plain progress, wide events, running inside a request, and a dependency graph that contains more than one copy of evlog.
initLogger() from library code, and configure nothing at import time. 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 replaces the host's drain, sampling, and redaction for the whole process, on a path the host's own code never executed.Emit progress with the global log API
Use the global log API the 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.client', message: 'GET /users' }), 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 carryingtagandmessage.
If the host may be draining, prefer the object form and keep your package name in a field so the event stays filterable:
import { log } from 'evlog'
export class ApiClient {
async request(path: string): Promise<Response> {
log.info({ source: 'mylib.client', message: `GET ${path}` })
// ...
}
}
The first segment is your package name and stays stable across releases. Treat it as a public identifier: hosts filter on it.
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.
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. Testing shows how to assert on the events this produces.
Join the host's request when you run inside one
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, and the host loses the connection between its handler and your code. Instead, accept the request logger as a parameter and add your context to the event the host already owns:
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.
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.Design your API so the logger stays optional: chargePayment(log, amount) where the host passes what it has. A package that quietly reaches for an ambient logger cannot run inside a queue worker or a script, and the host has no way to hand you the request logger you skipped.
Expect more than one copy of evlog in the graph
Your package's dependency tree and the host's can resolve different physical copies of evlog, because package managers hash optional peers differently across workspaces. evlog handles the common case for you: every 2.x copy registers the same process-wide slot, so an initLogger() from the host's copy configures your copy too, and your emits flow through the host's drains.
Declare evlog as a peer dependency pinned to a single major to stay inside that guarantee:
{
"peerDependencies": {
"evlog": "^2.0.0"
}
}
Two different majors in one process is the case evlog can't fix: they can't share request scope or configuration, so events emitted through the other copy are undrained and unredacted, and evlog prints a warning when the second major registers. Deduplicate the graph to a single major when that warning appears; nothing in your package code needs to change.
For errors, duplicate copies have a second consequence on how the host should catch yours, covered in Structured Errors.
Overview
Ship evlog inside a reusable package: legible events and errors in every host application that runs it, without owning any logging configuration.
Structured Errors
Replace bare errors with a catalog your package owns: typed factories, a collision-free wire format, and codes the host can act on.