Configuration
createLogger(options) accepts a LoggerOptions object:
interface LoggerOptions {
/** Minimum level to emit. Default `info`. */
level?: LogLevelInput; // 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'fatal' | number
/** Optional scope / namespace for this logger. */
scope?: string;
/** Runtime adapter. Auto-detected when omitted. */
runtime?: RuntimeAdapter;
/**
* Initial context store. When omitted the logger creates an empty one
* automatically. See [Context & Metadata](./context) for when to use
* `context` versus `metadata`.
*/
context?: ContextStore;
/** Transports. Defaults to the runtime's default transports. */
transports?: Transport[];
/** Pipeline configuration or instance. */
pipeline?: Pipeline | PipelineOptions;
/** Shared plugin manager (internal, used by scope()). */
plugins?: PluginManager;
/**
* Global handler for internal errors (a throwing filter/processor/plugin, a
* failing formatter, or a broken transport). When omitted, errors are printed
* to the native `console.error`. The logger never rethrows them.
*/
onError?: (error: unknown, info: { phase: string; entry?: LogEntry; source?: string }) => void;
/**
* Max time (ms) to wait for an async `Transport.write` before treating it as
* failed, so a stalled sink can never hang `flush()`/`destroy()`. Default 5000.
*/
writeTimeoutMs?: number;
/**
* Global per-transport async write-queue depth limit. When any transport's
* pending queue depth reaches this value, the newest entry is dropped and
* reported via `onError` (`phase: 'transport'`). A transport's own
* `queueLimit` takes precedence when both are set — the tighter bound wins.
* `0` disables the global limit (backward-compatible). Default `0`.
*/
maxQueueDepth?: number;
/**
* Environment variable (name) whose value — if set to a valid level name —
* overrides `level`. Defaults to `"LOG_LEVEL"`. Set to `null` to disable.
*/
levelEnvVar?: string | null;
/**
* Scope filter: a comma/space-separated list of glob patterns (with
* `*` wildcards); a leading `-` excludes. Only entries whose `scope` matches are
* emitted. Read automatically from `scopeFilterEnvVar` when omitted.
*/
scopeFilter?: string | string[];
/**
* Environment variable whose value supplies the scope filter when
* `scopeFilter` is not set. Defaults to `"LOGRAIL_DEBUG"`. Set to `null`
* to disable.
*/
scopeFilterEnvVar?: string | null;
}Internal error handling
The logger is designed to never throw from a log.* call. If an internal step fails — a Filter/Processor/plugin.onEntry throws, a Formatter throws, or a Transport.write throws or stalls — the error is:
- reported exactly once via the
onErrorhook (or the nativeconsole.errorwhen no hook is set), passinginfo.phase('filter' | 'process' | 'plugin' | 'formatter' | 'transport') and, where applicable, the offendingsource(plugin/transport name) and theentry; - never propagated to the caller, so logging can never crash your app.
A broken Processor or plugin.onEntry keeps the last good entry (it is not dropped); a throwing Filter drops the entry (as a safe default). An async Transport.write that does not settle within writeTimeoutMs is reported as a timeout failure and the queue moves on, so flush()/destroy() always resolve.
FileTransport write errors are reported on every occurrence (not just the first). The transport calls its onError hook with the failing LogEntry so you can detect disk-full, EPERM, and other transient errors and take action (e.g. switch to a fallback path). If no onError hook is set, the error is logged to console.error.
Log levels
Levels are ordered by severity:
| Level | Value |
|---|---|
| trace | 10 |
| debug | 20 |
| info | 30 |
| warn | 40 |
| error | 50 |
| fatal | 60 |
setLevel accepts either a level name or a numeric value. Entries below the configured level are dropped before reaching the pipeline:
log.setLevel('debug'); // also accepts a number, e.g. 20
log.getLevel(); // 20Custom levels
You can register additional named levels between (or beyond) the built-ins. This is useful for domain-specific severities — e.g. an audit level for security-critical events that sits between info and error:
import { registerLevel } from 'lograil';
// 45 sits between warn (40) and error (50) — tune to where you need it.
registerLevel('audit', 45);
log.setLevel('audit'); // now usable everywhere a level name is
log.error('payment declined'); // still emits (error 50 >= audit 45)
log.warn('low balance'); // filtered out (warn 40 < audit 45)- The name must be new (not a built-in and not already registered); a collision throws.
- The value must be a positive finite number; duplicates of an existing name throw too, so a bad registration fails fast.
- Use
unregisterLevel(name)to remove a custom level (built-ins are protected and cannot be removed).listCustomLevels()returns the currently-registered names. - Custom names work with
setLevel,getLevel, theLOG_LEVELenv var, per-transportlevel, and level commands — they are first-class after registration. The registry is process-wide; register once at startup.
Per-transport level filtering
Each transport can declare its own level, which is applied after the logger-level filter. This lets you fan out different severity levels to different sinks — for example, sending only error+ to a remote collector while writing all levels to a local file:
import { Logger, ConsoleTransport } from 'lograil';
const logger = new Logger({
transports: [
// Local file: all levels
new FileTransport({ mode: 'single', appName: 'app', dir: './logs' }),
// Remote: only errors and above
new OtlpTransport({
endpoint: 'http://localhost:4318/v1/logs',
level: 'error',
}),
],
});The effective filter chain is: logger-level → pipeline filters → per-transport level.
Context
Context holds structured fields attached to every entry. Set it once and it travels with all subsequent logs:
log.setContext('requestId', 'abc-123');
log.mergeContext({ tenant: 'acme', env: 'prod' });
log.info('handling request'); // includes requestId/tenant/envScoped loggers get an isolated child context, so setting context on a child never leaks to the parent.
Environment variables & namespace filtering
Two zero-config controls are available for operations without code changes.
LOG_LEVEL
Set the LOG_LEVEL environment variable to a level name to override the configured level at startup (useful for turning up verbosity in prod without a redeploy):
LOG_LEVEL=debug node server.jsPoint it at a different variable with levelEnvVar, or disable with levelEnvVar: null.
Scope filtering
When you use scoped loggers (logger.scope('http')), you can restrict which scopes actually emit via the LOGRAIL_DEBUG environment variable or the scopeFilter option. Syntax mirrors the popular debug package:
# enable http & db scopes (wildcards allowed), disable noisy http:noise
LOGRAIL_DEBUG='http*,db*,-http:noise' node server.jsconst log = createLogger({ scopeFilter: ['http*', 'db*', '-http:noise'] });A leading - excludes a pattern; * matches any run of characters. scope is the full dotted name (e.g. http:server), so http* matches it.
When omitted, the logger reads LOGRAIL_DEBUG automatically. You can point it at a different variable with scopeFilterEnvVar, or disable the env read entirely with scopeFilterEnvVar: null.
Runtime options
When the runtime is auto-detected you can still pass options via detectRuntime or the explicit factory functions:
import { createNodeRuntime } from 'lograil/runtime';
const log = createLogger({
runtime: createNodeRuntime({ appName: 'my-app', disableFile: false }),
});| Runtime | Option | Effect |
|---|---|---|
| Node / Electron main | appName | Required by FileTransport; always part of the log file name |
| Node / Electron main | fileTransportOptions | Forwarded to FileTransport (mode rotate-time); overrides the disk-safety defaults (10 MB/file, 14 daily files, 200 MB total) |
| Node / Electron main | disableFile | Console-only (no file) |
| Electron main | receiveFromRenderer | Receive renderer logs over IPC (default true) |
See Runtime for the full adapter contract.