Skip to content

Configuration ​

createLogger(options) accepts a LoggerOptions object:

ts
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 onError hook (or the native console.error when no hook is set), passing info.phase ('filter' | 'process' | 'plugin' | 'formatter' | 'transport') and, where applicable, the offending source (plugin/transport name) and the entry;
  • 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:

LevelValue
trace10
debug20
info30
warn40
error50
fatal60

setLevel accepts either a level name or a numeric value. Entries below the configured level are dropped before reaching the pipeline:

ts
log.setLevel('debug'); // also accepts a number, e.g. 20
log.getLevel(); // 20

Custom 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:

ts
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, the LOG_LEVEL env var, per-transport level, 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:

ts
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:

ts
log.setContext('requestId', 'abc-123');
log.mergeContext({ tenant: 'acme', env: 'prod' });
log.info('handling request'); // includes requestId/tenant/env

Scoped 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):

bash
LOG_LEVEL=debug node server.js

Point 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:

bash
# enable http & db scopes (wildcards allowed), disable noisy http:noise
LOGRAIL_DEBUG='http*,db*,-http:noise' node server.js
ts
const 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:

ts
import { createNodeRuntime } from 'lograil/runtime';

const log = createLogger({
  runtime: createNodeRuntime({ appName: 'my-app', disableFile: false }),
});
RuntimeOptionEffect
Node / Electron mainappNameRequired by FileTransport; always part of the log file name
Node / Electron mainfileTransportOptionsForwarded to FileTransport (mode rotate-time); overrides the disk-safety defaults (10 MB/file, 14 daily files, 200 MB total)
Node / Electron maindisableFileConsole-only (no file)
Electron mainreceiveFromRendererReceive renderer logs over IPC (default true)

See Runtime for the full adapter contract.

Released under the MIT License.