Plugins
Plugins extend the logger through optional lifecycle hooks. Every hook is optional; a plugin that wants to transform entries implements onEntry.
interface Plugin {
/** Unique plugin name. */
readonly name: string;
onInit?(ctx: PluginContext): void | Promise<void>;
onEntry?(entry: LogEntry): LogEntry | null | Promise<LogEntry | null>;
onTransport?(transport: Transport): void;
onDestroy?(): void | Promise<void>;
}Return null from onEntry to drop an entry; return a (possibly new) entry to pass it downstream.
Registering a plugin
import { createLogger } from 'lograil';
const log = createLogger();
await log.use({
name: 'add-host',
onEntry(entry) {
entry.metadata = { ...entry.metadata, host: os.hostname() };
return entry;
},
});PluginContext
onInit receives a PluginContext — the bridge that lets a plugin reconfigure the logger at runtime:
interface PluginContext {
addTransport(transport: Transport): void;
removeTransport(name: string): void;
pipeline: Pipeline; // add/remove filters & processors, change formatter
use(plugin: Plugin): Promise<void>;
unregisterPlugin(name: string): void;
logger: Logger;
}Example — a plugin that adds a redacting processor and a sampling filter:
import { createRedactProcessor, createLevelFilter, type Filter } from 'lograil';
// Keep roughly 10% of entries (random sampling).
const sampleFilter: Filter = () => Math.random() < 0.1;
log.use({
name: 'secure',
onInit(ctx) {
ctx.pipeline.addProcessor(createRedactProcessor(['password', 'token']));
ctx.pipeline.addFilter(sampleFilter);
ctx.pipeline.addFilter(createLevelFilter(20)); // keep debug and above
},
});Lifecycle
Plugins participate in the log production and teardown process through four optional lifecycle hooks.
Overview
onEntry Interception Chain
Each log entry passes through every plugin's onEntry hook in registration order. If a plugin returns null, subsequent plugins do not see the entry.
Hook Triggers
| Hook | When | Properties |
|---|---|---|
onInit | When the plugin is registered (via use) | Async, runs once |
onEntry | For every entry, before the formatter | Async, chained in registration order |
onTransport | When a transport is added (including by other plugins) | Sync, iterates all plugins |
onDestroy | On unregisterPlugin or logger.destroy() | Async; fire-and-forget for single unregister |
log.unregisterPlugin('secure'); // triggers onDestroy
log.hasPlugin('secure'); // falseAsync behavior and error handling
onEntryinterception is asynchronous and runs serially in registration order.- If a plugin's
onEntrythrows, the error is reported (viaonError) but the entry passes through unchanged to the next plugin — a faulty plugin never drops or corrupts logs. - Call
await log.flush()/await log.destroy()before process exit to ensure all plugin work completes. - Child loggers share the same
PluginManager— plugins only need to be registered once.
Built-in plugin: OTel trace correlation
createOtelTracePlugin() automatically injects the active OpenTelemetry trace and span identifiers into each entry's metadata (traceId / spanId), so an OtlpTransport (or any backend that reads those fields) can correlate logs with their span — without you threading the context manually.
@opentelemetry/api is an optional peer dependency. If it isn't installed, or no span is active, the plugin is a no-op and entries are unaffected.
import { createOtelTracePlugin } from 'lograil';
await log.use(createOtelTracePlugin());
// inside a traced operation, the active span is picked up automatically:
logger.info('handling request'); // => metadata: { traceId, spanId }