@vielzeug/codex 2.0.1 → 2.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/data/catalog.json +137 -129
- package/data/llms-full.txt +13088 -17651
- package/data/llms.txt +12 -11
- package/data/manifest.json +1 -1
- package/data/packages/arsenal.json +1 -1
- package/data/packages/assay.json +1 -1
- package/data/packages/clockwork.json +2 -2
- package/data/packages/codex.json +1 -1
- package/data/packages/coins.json +1 -1
- package/data/packages/conduit.json +1 -1
- package/data/packages/courier.json +1 -1
- package/data/packages/dnd.json +14 -12
- package/data/packages/familiar.json +26 -16
- package/data/packages/flux.json +1 -1
- package/data/packages/forge.json +1 -1
- package/data/packages/herald.json +19 -33
- package/data/packages/keymap.json +13 -19
- package/data/packages/ledger.json +28 -25
- package/data/packages/lingua.json +2 -2
- package/data/packages/necromancer.json +50 -0
- package/data/packages/orbit.json +34 -39
- package/data/packages/ore.json +1 -1
- package/data/packages/prism.json +37 -40
- package/data/packages/pulse.json +26 -24
- package/data/packages/refine.json +1 -1
- package/data/packages/ripple.json +1 -1
- package/data/packages/rune.json +6 -7
- package/data/packages/sandbox.json +7 -6
- package/data/packages/scout.json +10 -10
- package/data/packages/scroll.json +18 -17
- package/data/packages/sourcerer.json +1 -1
- package/data/packages/spell.json +1 -1
- package/data/packages/tempo.json +49 -81
- package/data/packages/vault.json +37 -40
- package/data/packages/ward.json +5 -17
- package/data/packages/wayfinder.json +9 -9
- package/data/refine.json +4902 -4902
- package/data/search.json +205 -206
- package/dist/cli.js +1 -1
- package/dist/cli.js.map +1 -1
- package/dist/http.js +46 -6
- package/dist/http.js.map +1 -1
- package/dist/server.js +1 -1
- package/dist/server.js.map +1 -1
- package/dist/tools/index.js +13 -5
- package/dist/tools/index.js.map +1 -1
- package/package.json +4 -4
package/data/packages/rune.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
|
-
"apiSource": "export type {\n BatchHandle,\n BatchTransportOptions,\n Bindings,\n JsonTransportOptions,\n LogEntry,\n LogLevel,\n LogMethod,\n LogMiddleware,\n Logger,\n LogType,\n PipeOptions,\n RedactTransportOptions,\n RemoteLogData,\n RemoteTransportOptions,\n RuneOptions,\n SampleTransportOptions,\n Transport,\n} from './types';\n\nexport { RuneError
|
|
2
|
+
"apiSource": "export type {\n BatchHandle,\n BatchTransportOptions,\n Bindings,\n JsonTransportOptions,\n LogEntry,\n LogLevel,\n LogMethod,\n LogMiddleware,\n Logger,\n LogType,\n PipeOptions,\n RedactTransportOptions,\n RemoteLogData,\n RemoteTransportOptions,\n RuneOptions,\n SampleTransportOptions,\n Transport,\n} from './types';\n\nexport { RuneError } from './errors';\nexport { isLevelEnabled, PRIORITY } from './types';\nexport type { LazyBinding } from './lazy';\nexport { lazy } from './lazy';\nexport { defaultLogger, createLogger } from './logger';\nexport type { ConsoleTheme, ConsoleThemeEntry, ConsoleTransportOptions, ResolvedTheme } from './console';\nexport { DEFAULT_THEME, consoleTransport, resolveTheme } from './console';\nexport { batchTransport, jsonTransport, pipe, redactTransport, remoteTransport, sampleTransport } from './transports';\n",
|
|
3
3
|
"docs": {
|
|
4
|
-
"index": "---\ntitle: Rune — Structured logging for TypeScript\ndescription: Browser/Node logger with levels, namespaces, pluggable transports, lazy bindings, and timing helpers.\npackage: rune\ncategory: logging\nkeywords: [logging, console, structured, scoped, transports, remote-logging, levels, namespaces, lazy-bindings]\nrelated: [courier, herald, familiar]\nexports:\n [\n createLogger,\n defaultLogger,\n consoleTransport,\n remoteTransport,\n jsonTransport,\n batchTransport,\n sampleTransport,\n redactTransport,\n pipe,\n lazy,\n isLevelEnabled,\n resolveTheme,\n DEFAULT_THEME,\n PRIORITY,\n RuneError,\n
|
|
5
|
-
"api": "---\ntitle: Rune — API Reference\ndescription: API reference for @vielzeug/rune exports, logger methods, configuration types, and transport factories.\n---\n\n[[toc]]\n\n## API Overview\n\n| Symbol | Purpose | Execution mode | Common gotcha |\n| -------------------- | ------------------------------------------------ | -------------- | ------------------------------------------------------------ |\n| `createLogger()` | Create an isolated `Logger` instance | Sync | Omitting `transports` defaults to `consoleTransport()` |\n| `defaultLogger` | Pre-created default logger singleton | — | Shared instance — use `child()` or `withBindings()` to scope |\n| `lazy(fn)` | Defer a binding value past the level check | Sync | Factory runs on every emit, not once |\n| `pipe()` | Fan-out dispatcher to multiple transports | Sync | Errors in one transport don't propagate to others |\n| `isLevelEnabled()` | Utility: test whether a level passes a threshold | Sync | `'off'` always returns `false` |\n| `PRIORITY` | Numeric priority table backing `isLevelEnabled()`| — | Lower number = more verbose |\n| `resolveTheme()` | Merge a partial theme onto the default | Sync | Returns a fully-populated `ResolvedTheme` |\n| `RuneError` | Base class for all `rune`-originated errors | — | Use `RuneError.is(err)` as the type guard |\n| `RuneTransportError` | Internal transport-failure error (never thrown) | — | Inspect via dev-only warnings, not `try`/`catch` |\n| `consoleTransport()` | Styled console output | Sync | Theme is resolved once at factory call, not per entry |\n| `remoteTransport()` | Async HTTP/webhook delivery | Async | Handler errors are swallowed to `console.warn` |\n| `jsonTransport()` | NDJSON to stdout or a custom sink | Sync | `process.stdout` is unavailable in browsers |\n| `batchTransport()` | Buffered batch delivery with flush interval | Sync/Interval | Must call `.dispose()` on shutdown to flush remaining |\n| `sampleTransport()` | Probabilistic entry forwarding | Sync | `rate: 1` forwards all entries; `rate: 0` forwards none |\n| `redactTransport()` | Sensitive field stripping before forwarding | Sync | Place this closest to the remote transport, not console |\n\n## Package Entry Point\n\n| Import | Purpose |\n| ---------------- | -------------------------------------------------------- |\n| `@vielzeug/rune` | All exports — logger, transport factories, `lazy`, types |\n\n## createLogger(initial?, options?)\n\nCreates an isolated logger instance.\n\n```ts\ncreateLogger(namespace: string, options?: Omit<RuneOptions, 'namespace'>): Logger\ncreateLogger(options?: RuneOptions): Logger\n```\n\n- `string` shorthand sets namespace: `createLogger('api')` or `createLogger('api', { logLevel: 'warn' })`.\n- Each call produces a fully independent instance — no shared mutable state.\n- Default transport is `consoleTransport()` when `transports` is omitted.\n\n> **Note — disposed loggers:** after `dispose()` is called, all log methods (`debug`, `info`, `warn`, `error`, `fatal`), `time()`, and `group()` / `groupCollapsed()` silently no-op. The `fn` callback in `group()` still runs — only the group header is suppressed.\n\n> **Note — transport/middleware fault isolation:** if a transport or middleware function throws, the logger catches it, reports it via a dev-only warning (the transport case wraps the error in `RuneTransportError`), and continues — a single misbehaving transport can never crash the caller of `log.info()`/etc., and sibling transports still receive the entry. A throwing middleware drops just that one entry.\n\n**Returns:** `Logger`\n\n**Example:**\n\n```ts\nimport { createLogger } from '@vielzeug/rune';\nimport { consoleTransport, remoteTransport } from '@vielzeug/rune';\n\nconst log = createLogger({ logLevel: 'warn', namespace: 'app' });\n\nconst serverLog = createLogger({\n namespace: 'server',\n transports: [\n consoleTransport(),\n remoteTransport({\n handler: async (type, data) => {\n await fetch('/api/logs', { body: JSON.stringify(data), method: 'POST' });\n },\n level: 'error',\n }),\n ],\n});\n```\n\n## defaultLogger\n\n`defaultLogger` is the pre-created default logger (`createLogger()` called once at module load).\n\nUse it as a quick-start singleton or create a child for module-level use:\n\n```ts\nimport { defaultLogger } from '@vielzeug/rune';\n\nconst log = defaultLogger.child({ namespace: 'app.worker' });\n```\n\n## lazy(fn)\n\nDefers evaluation of an expensive binding value until after the level check passes.\nThe factory function is never called when the log level suppresses the entry.\n\n```ts\nlazy(fn: () => unknown): LazyBinding\n```\n\n```ts\nimport { lazy } from '@vielzeug/rune';\n\nconst reqLog = log.withBindings({\n diagnostics: lazy(() => buildExpensiveDiagnostics()),\n});\n\nreqLog.debug('trace'); // diagnostics() only called when debug is enabled\n```\n\n**Returns:** `LazyBinding`\n\n## Logger Methods\n\n### Logging\n\nAll five methods share the same signature:\n\n```ts\nlog.debug / info / warn / error / fatal(message: string): void\nlog.debug / info / warn / error / fatal(error: Error, context?: Bindings, message?: string): void\nlog.debug / info / warn / error / fatal(context: Bindings, message?: string): void\n```\n\nArgument rules:\n\n- String-only calls accept a single message argument.\n- **Error-first form:** pass an `Error` as the first argument — it is auto-serialized to `{ message, name, stack }` under the `err` key. Optionally follow with a `Bindings` object and/or a message string.\n- Context object comes first when providing structured data without a top-level Error. `Error` values inside the context object are also auto-serialized to `{ message, name, stack }`.\n\n```ts\nlog.error(err, 'request failed'); // err auto-serialized to data.err\nlog.error(err, { requestId }, 'request failed'); // err + context + message\nlog.error({ err: new Error('boom') }, 'failed'); // Error nested in context object\n```\n\n### Composition\n\n| Method | Returns | What it does |\n| ---------------------- | -------- | ----------------------------------------------------------------- |\n| `child(overrides?)` | `Logger` | Clones config, applies overrides, inherits bindings |\n| `withBindings(fields)` | `Logger` | Pins fields to every subsequent call, returns a new child logger |\n| `use(middleware)` | `Logger` | Appends a middleware function to the pipeline, returns new logger |\n\n`child()` transport inheritance:\n\n- Omit `transports` → inherit parent transports (default).\n- Pass `transports: []` → disable all transports on the child.\n- Pass `transports: [...]` → replace entirely with the given list.\n\n`child()` namespace joining:\n\n- `parent.child({ namespace: 'auth' })` on a logger with namespace `'api'` produces `'api.auth'`.\n- Omit `namespace` → inherits parent namespace unchanged.\n\n### Utilities\n\n| Method | Returns | Description |\n| ----------------------------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| `enabled(level)` | `boolean` | True if entries at this level pass the configured threshold |\n| `time(label, fn, level?)` | `T` | Measures sync/async execution; emits at `level` (default `'debug'`), label as message, `{ duration_ms }` in `data`. When `fn` throws or rejects, `{ err }` is also included. |\n| `group(label, fn, level?)` | `T` | Wraps callback in `console.group`; closes even on throw/reject. Pass `level` to gate the group header on the configured threshold (e.g. `'debug'` suppresses when `logLevel` is `'warn'`). |\n| `groupCollapsed(label, fn, level?)` | `T` | Same as `group`, using `console.groupCollapsed`. |\n| `dispose()` | `void` | Silences all subsequent log calls on this logger instance. Does **not** auto-dispose batch transports — hold a reference and call `batchTransport.dispose()` on shutdown. Idempotent. |\n\n### Properties\n\n| Property | Type | Description |\n| ------------------ | -------------------------- | ------------------------------------------------------------------ |\n| `logLevel` | `LogLevel` | Active log level threshold |\n| `namespace` | `string` | Effective namespace string |\n| `middleware` | `readonly LogMiddleware[]` | Middleware pipeline snapshot |\n| `transports` | `readonly Transport[]` | Transport pipeline snapshot |\n| `bindings` | `Readonly<Bindings>` | Snapshot of currently pinned fields |\n| `disposalSignal` | `AbortSignal` | Aborted when `dispose()` is called. Use to tie external lifetimes. |\n| `disposed` | `boolean` | `true` after `dispose()` has been called |\n| `[Symbol.dispose]` | `() => void` | Delegates to `dispose()`. Enables `using` declarations. |\n\n## Transport Factories\n\n### consoleTransport(options?)\n\n```ts\nconsoleTransport(options?: ConsoleTransportOptions): Transport\n```\n\nWrites styled output to the browser console (CSS badges) or Node terminal (plain text). This is the default transport.\n\n| Option | Type | Default | Description |\n| ----------- | ------------------------ | --------- | --------------------------------------------------- |\n| `level` | `LogLevel` | `'debug'` | Minimum level to output |\n| `timestamp` | `boolean` | `true` | Include `HH:MM:SS.mmm` |\n| `ansi` | `boolean` | auto | Force ANSI color codes on/off (Node only) |\n| `format` | `'json' \\| 'raw'` | `'raw'` | Context serialization: `'json'` uses JSON.stringify |\n| `inspectFn` | `(v: unknown) => string` | — | Custom object formatter (e.g. `util.inspect`) |\n| `theme` | `ConsoleTheme` | — | Override default badge colours for this transport |\n\n**Returns:** `Transport`\n\n**Example:**\n\n```ts\nimport { consoleTransport, createLogger } from '@vielzeug/rune';\nimport { inspect } from 'node:util';\n\nconst log = createLogger({\n transports: [consoleTransport({ level: 'info', timestamp: true, inspectFn: inspect })],\n});\n```\n\n### remoteTransport(options)\n\n```ts\nremoteTransport(options: RemoteTransportOptions): Transport\n```\n\nForwards entries asynchronously to a remote handler. Fire-and-forget — handler errors are swallowed to `console.warn` and never propagate to the caller.\n\n| Option | Type | Default | Description |\n| --------- | ------------------------------- | ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `handler` | `(type: LogType, data: RemoteLogData) => void` | — | Required. Receives each forwarded entry |\n| `level` | `LogLevel` | `'debug'` | Minimum level to forward |\n| `env` | `'production' \\| 'development'` | auto-detected | Override the runtime environment marker |\n| `onError` | `(error: unknown, data: RemoteLogData) => void` | — | Called when the handler throws or rejects. Default: a dev-only `console.warn`. Silent in production — provide an explicit handler for production observability. |\n\n**Returns:** `Transport`\n\n**Example:**\n\n```ts\nimport { createLogger, remoteTransport } from '@vielzeug/rune';\n\nconst log = createLogger({\n transports: [\n remoteTransport({\n handler: async (type, data) => {\n await fetch('/api/logs', { body: JSON.stringify(data), method: 'POST' });\n },\n level: 'error',\n }),\n ],\n});\n```\n\n### jsonTransport(options?)\n\n```ts\njsonTransport(options?: JsonTransportOptions): Transport\n```\n\nOutputs newline-delimited JSON (NDJSON) to `stdout` or a custom function. Useful for server-side log aggregation pipelines (ELK, Datadog, etc.).\n\nEach line is a flat JSON object with `level`, `time` (ISO), and optional `ns`, `msg`, plus all merged context fields.\n\n| Option | Type | Default | Description |\n| -------- | ------------------------------ | ---------------- | -------------------------------------------------------------------------------------- |\n| `level` | `LogLevel` | `'debug'` | Minimum level |\n| `output` | `(line: string) => void` | `process.stdout` | Custom output sink |\n| `safe` | `boolean` | `false` | Replace circular references with `'[Circular]'` instead of throwing |\n| `fields` | `{ level?, msg?, ns?, time? }` | — | Custom output field names for aggregator compatibility (e.g. `'severity'` for Datadog) |\n\n**Returns:** `Transport`\n\n**Example:**\n\n```ts\nimport { createLogger, jsonTransport } from '@vielzeug/rune';\n\nconst log = createLogger({\n namespace: 'api',\n transports: [jsonTransport({ level: 'info' })],\n});\n\nlog.info({ path: '/users', status: 200 }, 'request');\n// {\"path\":\"/users\",\"status\":200,\"level\":\"info\",\"time\":\"2026-05-30T...\",\"ns\":\"api\",\"msg\":\"request\"}\n```\n\n### batchTransport(options)\n\n```ts\nbatchTransport(options: BatchTransportOptions): BatchHandle\n```\n\nBuffers entries and delivers them in batches. Flushes when the buffer reaches `maxSize` or after `interval` elapses.\n\n| Option | Type | Default | Description |\n| -------------- | ------------------------------------------------ | --------- | --------------------------------------------------------------------------------------- |\n| `onFlush` | `(entries: LogEntry[]) => void \\| Promise<void>` | — | Required. Receives each batch (may be async) |\n| `onFlushError` | `(entries: LogEntry[], error: unknown) => void` | — | Called when `onFlush` throws or rejects |\n| `level` | `LogLevel` | `'debug'` | Minimum level to buffer |\n| `interval` | `number` | `5000` | Flush interval in milliseconds |\n| `maxSize` | `number` | `50` | Max buffer size before an early flush |\n| `maxBuffer` | `number` | unbounded | Hard cap — oldest entries are dropped silently when exceeded. Does not trigger a flush. |\n\nReturns a `BatchHandle` with:\n\n- `.transport` — the `Transport` function to pass to `createLogger({ transports: [handle.transport] })`.\n- `.flush()` — immediately send buffered entries without stopping the timer.\n- `.dispose()` — stop the interval and flush remaining entries. **Call on shutdown.** Idempotent.\n- `.disposed` — `true` after `dispose()` has been called.\n- `[Symbol.dispose]()` — delegates to `.dispose()`. Enables `using` declarations.\n\nAfter `dispose()`, the transport becomes inert: new entries are silently dropped.\n\n**Returns:** `BatchHandle`\n\n**Example:**\n\n```ts\nimport { batchTransport, createLogger } from '@vielzeug/rune';\n\nconst batch = batchTransport({\n onFlush: (entries) => sendToCollector(entries),\n interval: 10_000,\n maxSize: 100,\n});\n\n// Pass batch.transport to the logger — batch holds flush/dispose\nconst log = createLogger({ transports: [batch.transport] });\n\nprocess.on('exit', () => batch.dispose());\n```\n\n### sampleTransport(options)\n\n```ts\nsampleTransport(options: SampleTransportOptions): Transport\n```\n\nProbabilistically forwards entries to a downstream transport.\n\n| Option | Type | Default | Description |\n| ----------- | ----------- | --------- | ---------------------------------------------- |\n| `rate` | `number` | — | Required. Fraction of entries to forward (0–1) |\n| `transport` | `Transport` | — | Required. Downstream transport |\n| `level` | `LogLevel` | `'debug'` | Minimum level to sample |\n\n**Returns:** `Transport`\n\n**Example:**\n\n```ts\nimport { createLogger, remoteTransport, sampleTransport } from '@vielzeug/rune';\n\nconst log = createLogger({\n transports: [\n sampleTransport({\n rate: 0.1,\n transport: remoteTransport({ handler }),\n }),\n ],\n});\n```\n\n### redactTransport(options)\n\n```ts\nredactTransport(options: RedactTransportOptions): Transport\n```\n\nStrips sensitive fields from `bindings` and `context` before forwarding. Redaction is applied recursively at any depth (up to 20 levels).\n\n::: warning Key matching\n`keys` matches **exact field names** at any nesting depth. Dot-path notation (e.g. `'user.password'`) is **not** supported — use `'password'` to redact every field named `password` regardless of nesting.\n:::\n\n| Option | Type | Default | Description |\n| ------------- | ----------- | -------------- | ------------------------------- |\n| `keys` | `string[]` | — | Required. Field names to redact |\n| `replacement` | `string` | `'[REDACTED]'` | Replacement value |\n| `transport` | `Transport` | — | Required. Downstream transport |\n\n**Returns:** `Transport`\n\n**Example:**\n\n```ts\nimport { createLogger, redactTransport, remoteTransport } from '@vielzeug/rune';\n\nconst log = createLogger({\n transports: [\n redactTransport({\n keys: ['password', 'token', 'ssn'],\n transport: remoteTransport({ handler }),\n }),\n ],\n});\n```\n\n### pipe(...transports) / pipe(options, ...transports)\n\n```ts\npipe(...transports: Transport[]): Transport\npipe(options: PipeOptions, ...transports: Transport[]): Transport\n```\n\nDispatches each `LogEntry` to every transport in the list independently. An error thrown by one transport does not stop the others. Use in place of separate array entries when you want fault isolation or a shared error observer.\n\n`pipe()` with no arguments creates a valid no-op transport — useful for conditional pipeline construction: `pipe(condition ? remoteTransport(opts) : undefined!)` pattern, or simply as a placeholder during development.\n\n| Option | Type | Description |\n| --------- | ------------------------------------------- | --------------------------------------------------------- |\n| `onError` | `(error: unknown, entry: LogEntry) => void` | Called with the error and entry when any transport throws |\n\n**Returns:** `Transport`\n\n**Example:**\n\n```ts\nimport { consoleTransport, createLogger, pipe, remoteTransport } from '@vielzeug/rune';\n\nconst log = createLogger({\n transports: [\n pipe(\n { onError: (err) => metrics.increment('log.transport.error') },\n consoleTransport(),\n remoteTransport({ handler, level: 'error' }),\n ),\n ],\n});\n```\n\n````\n\n## Utilities\n\n### isLevelEnabled(threshold, level)\n\n```ts\nisLevelEnabled(threshold: LogLevel, level: LogLevel): boolean\n````\n\nReturns `true` when `level` is at or above `threshold`. Always returns `false` when `level` is `'off'`. Useful for building custom transports that respect level filtering.\n\n```ts\nimport { isLevelEnabled } from '@vielzeug/rune';\n\nisLevelEnabled('warn', 'error'); // true\nisLevelEnabled('warn', 'info'); // false\nisLevelEnabled('debug', 'off'); // false\n```\n\n### resolveTheme(override?)\n\n```ts\nresolveTheme(override: ConsoleTheme | undefined): ResolvedTheme\n```\n\nDeep-merges a partial `ConsoleTheme` override onto `DEFAULT_THEME`. Returns a fully-populated `ResolvedTheme` where every level and every field is present. Used internally by `consoleTransport()` — call directly when building a custom transport that needs to honour theme overrides.\n\n```ts\nimport { resolveTheme } from '@vielzeug/rune';\n\nconst theme = resolveTheme({ warn: { badge: '⚡' } });\n// theme.warn.badge === '⚡', theme.warn.bg === DEFAULT_THEME.warn.bg (unchanged)\n```\n\n### DEFAULT_THEME\n\nThe built-in badge and namespace colour definitions used by `consoleTransport()`. Override per-transport via `ConsoleTransportOptions.theme`.\n\n### PRIORITY\n\n```ts\nPRIORITY: Record<LogLevel, number>\n```\n\nNumeric priority for each level (`debug: 0`, `info: 1`, `warn: 2`, `error: 3`, `fatal: 4`, `off: 5`) — lower is more verbose. Exported for transport/middleware authors building custom level-comparison logic; `isLevelEnabled()` is built directly on top of it.\n\n## Errors\n\n### RuneError\n\nBase class for all `rune`-originated errors. Use `instanceof RuneError` or `RuneError.is(err)` to catch any error the package throws.\n\n```ts\nimport { RuneError } from '@vielzeug/rune';\n\ntry {\n // ...\n} catch (err) {\n if (RuneError.is(err)) {\n // handle a rune-originated error\n }\n}\n```\n\n**Static methods:**\n\n| Method | Returns | Description |\n| ------------ | ----------------------- | --------------------------------------------- |\n| `is(err)` | `err is RuneError` | Type guard — `true` for `RuneError` and subclasses |\n\n### RuneTransportError\n\n```ts\nclass RuneTransportError extends RuneError {}\n```\n\nConstructed internally when a transport function throws during log entry emission. It is **never thrown or propagated** to application code — the logger catches the underlying error, wraps it here (available as `.cause`), and reports it via a dev-only warning. See the transport/middleware fault-isolation note under `createLogger()` above.\n\n## Types\n\n### LogType\n\n`'debug' | 'error' | 'fatal' | 'info' | 'warn'`\n\n### LogLevel\n\n`LogType | 'off'` — threshold order: `debug < info < warn < error < fatal < off`\n\n### Bindings\n\n`Record<string, unknown>` — Key-value context pinned via `withBindings()` or passed per-call.\n\n### LogEntry\n\nThe structured record produced by every log call and dispatched to all transports.\n\n| Field | Type | Description |\n| ----------- | -------------------- | ------------------------------------------------------------------------ |\n| `data` | `Readonly<Bindings>` | Merged result of pinned bindings and per-call context — already resolved |\n| `level` | `LogType` | Log level |\n| `message` | `string?` | Log message |\n| `namespace` | `string` | Effective namespace at time of call |\n| `timestamp` | `Date` | Exact moment of the call, shared across transports |\n\n### Transport\n\n```ts\ntype Transport = (entry: LogEntry) => void;\n```\n\nReceives every `LogEntry` that passes the logger's level threshold. Responsible for its own formatting, delivery, and per-transport level filtering.\n\n### RemoteLogData\n\nPayload shape delivered to `RemoteTransportOptions.handler`:\n\n| Field | Type | Description |\n| ----------- | ------------------------------- | ----------------------------------------- |\n| `data` | `Bindings?` | Merged structured data (omitted if empty) |\n| `env` | `'production' \\| 'development'` | Runtime env marker |\n| `level` | `LogType` | Log level |\n| `message` | `string?` | Log message |\n| `namespace` | `string?` | Effective namespace |\n| `timestamp` | `string` | Full ISO timestamp |\n\n### PipeOptions\n\n| Field | Type | Description |\n| --------- | ------------------------------------------- | ----------------------------------------------------- |\n| `onError` | `(error: unknown, entry: LogEntry) => void` | Called when a transport in the pipe throws or rejects |\n\n### ResolvedTheme\n\n`Record<LogType | 'group' | 'ns', ConsoleThemeEntry>` — fully resolved theme with all fields populated.\n\n### RuneOptions\n\n| Field | Type | Default | Description |\n| ------------ | ------------------ | ---------------------- | ---------------------------- |\n| `logLevel` | `LogLevel?` | `'debug'` | Logger level threshold |\n| `namespace` | `string?` | `''` | Namespace prefix |\n| `transports` | `Transport[]?` | `[consoleTransport()]` | Transport pipeline |\n| `bindings` | `Bindings?` | `{}` | Initial pinned bindings |\n| `middleware` | `LogMiddleware[]?` | `[]` | Entry transform/filter chain |\n\n### LogMethod\n\n```ts\ntype LogMethod = {\n (message: string): void;\n (error: Error, context?: Bindings, message?: string): void;\n (context: Bindings, message?: string): void;\n};\n```\n\nEvery log-level method uses this signature. Three call forms are supported:\n\n- **String-only:** `log.info('message')`\n- **Error-first:** `log.error(err, { requestId }, 'failed')` — `Error` is auto-serialized to `{ message, name, stack }` under `data.err`. Optionally follow with a `Bindings` object and/or a message string.\n- **Context-first:** `log.info({ key: 'value' }, 'message')` — structured context object, optional message. `Error` values nested inside the context are also auto-serialized.\n\n### LogMiddleware\n\n```ts\ntype LogMiddleware = (entry: LogEntry) => LogEntry | null;\n```\n\nMiddleware functions intercept entries before they reach transports. Return the (optionally mutated) entry to continue, or return `null` to drop the entry. Added via `use(fn)` or `RuneOptions.middleware`.\n\n### LazyBinding\n\nOpaque type returned by `lazy()`. Pass as a value inside `withBindings()`. The factory is only called when the entry is actually emitted (after the level check passes).\n\n### BatchHandle\n\n```ts\ntype BatchHandle = {\n [Symbol.dispose]: () => void;\n dispose: () => void;\n readonly disposed: boolean;\n flush: () => void;\n transport: Transport;\n};\n```\n\nReturned by `batchTransport()`. Pass `handle.transport` to `createLogger({ transports })`; call `handle.dispose()` on shutdown. `disposed` is `true` after `dispose()` has been called.\n\n### Logger\n\nThe full interface returned by `createLogger()` and `defaultLogger`:\n\n```ts\ntype Logger = {\n [Symbol.dispose]: () => void;\n readonly bindings: Readonly<Bindings>;\n child: (overrides?: RuneOptions) => Logger;\n debug: LogMethod;\n readonly disposalSignal: AbortSignal;\n dispose: () => void;\n readonly disposed: boolean;\n enabled: (type: LogLevel) => boolean;\n error: LogMethod;\n fatal: LogMethod;\n group: <T>(label: string, fn: () => T, level?: LogType) => T;\n groupCollapsed: <T>(label: string, fn: () => T, level?: LogType) => T;\n info: LogMethod;\n readonly logLevel: LogLevel;\n readonly middleware: readonly LogMiddleware[];\n readonly namespace: string;\n time: <T>(label: string, fn: () => T, level?: LogType) => T;\n readonly transports: readonly Transport[];\n use: (middleware: LogMiddleware) => Logger;\n warn: LogMethod;\n /** Returns a new child logger with additional pinned bindings. The returned logger is fully independent — disposing it does not affect the parent, and vice versa. */\n withBindings: (bindings: Bindings) => Logger;\n};\n```\n\n### ConsoleTransportOptions\n\n| Field | Type | Default | Description |\n| ----------- | ------------------------ | --------- | --------------------------------------------------- |\n| `level` | `LogLevel` | `'debug'` | Minimum level to output |\n| `timestamp` | `boolean` | `true` | Include `HH:MM:SS.mmm` |\n| `ansi` | `boolean` | auto | Force ANSI color codes on/off (Node only) |\n| `format` | `'json' \\| 'raw'` | `'raw'` | Context serialization: `'json'` uses JSON.stringify |\n| `inspectFn` | `(v: unknown) => string` | — | Custom object formatter (e.g. `util.inspect`) |\n| `theme` | `ConsoleTheme` | — | Override default badge colours for this transport |\n\n### RemoteTransportOptions\n\n| Field | Type | Default | Description |\n| --------- | ------------------------------- | ------------- | --------------------------------------- |\n| `handler` | `(type: LogType, data: RemoteLogData) => void` | — | Required. Receives each forwarded entry |\n| `level` | `LogLevel` | `'debug'` | Minimum level to forward |\n| `env` | `'production' \\| 'development'` | auto-detected | Override the runtime environment marker |\n| `onError` | `(error: unknown, data: RemoteLogData) => void` | — | Called when the handler throws |\n\n### JsonTransportOptions\n\n| Field | Type | Default | Description |\n| -------- | ------------------------------ | ---------------- | ------------------------------------------------------------------- |\n| `level` | `LogLevel` | `'debug'` | Minimum level |\n| `output` | `(line: string) => void` | `process.stdout` | Custom output sink |\n| `safe` | `boolean` | `false` | Replace circular references with `'[Circular]'` instead of throwing |\n| `fields` | `{ level?, msg?, ns?, time? }` | — | Custom output field names (e.g. `level: 'severity'` for Datadog) |\n\n### BatchTransportOptions\n\n| Field | Type | Default | Description |\n| -------------- | ------------------------------------------------ | --------- | --------------------------------------------------------- |\n| `onFlush` | `(entries: LogEntry[]) => void \\| Promise<void>` | — | Required. Receives each batch (may be async) |\n| `onFlushError` | `(entries: LogEntry[], error: unknown) => void` | — | Called when `onFlush` throws or rejects |\n| `level` | `LogLevel` | `'debug'` | Minimum level to buffer |\n| `interval` | `number` | `5000` | Flush interval in milliseconds |\n| `maxSize` | `number` | `50` | Max buffer size before an early flush |\n| `maxBuffer` | `number` | unbounded | Hard cap — drops oldest when exceeded, no flush triggered |\n\n### SampleTransportOptions\n\n| Field | Type | Default | Description |\n| ----------- | ----------- | --------- | ---------------------------------------------- |\n| `rate` | `number` | — | Required. Fraction of entries to forward (0–1) |\n| `transport` | `Transport` | — | Required. Downstream transport |\n| `level` | `LogLevel` | `'debug'` | Minimum level to sample |\n\n### RedactTransportOptions\n\n| Field | Type | Default | Description |\n| ------------- | ----------- | -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `keys` | `string[]` | — | Required. Field names to redact at any depth |\n| `maxDepth` | `number` | `20` | Maximum object nesting depth to traverse. Fields deeper than this are not redacted — a dev-only warning is emitted when hit. **Security:** the warning is suppressed in production; ensure sensitive fields are not nested beyond this limit. |\n| `replacement` | `string` | `'[REDACTED]'` | Replacement value |\n| `transport` | `Transport` | — | Required. Downstream transport |\n",
|
|
6
|
-
"usage": "---\ntitle: Rune — Usage Guide\ndescription: Configuration, transports, scoped loggers, lazy bindings, timers, groups, and best practices for Rune.\n---\n\n[[toc]]\n\n::: tip New to Rune?\nStart with the [Overview](./index.md), then use this page for detailed usage patterns.\n:::\n\n## Basic Usage\n\n`defaultLogger` is the default singleton logger instance. Use `createLogger()` for isolated config.\n\n```ts\nimport { createLogger, defaultLogger } from '@vielzeug/rune';\n\nconst appLog = defaultLogger;\nconst apiLog = createLogger({ namespace: 'api' });\nconst authLog = createLogger('auth'); // shorthand namespace\n```\n\nEach `createLogger()` call is fully independent with its own transport pipeline.\n\nThe two-arg shorthand combines namespace and options cleanly:\n\n```ts\nconst log = createLogger('api', { logLevel: 'warn', transports: [transport] });\n```\n\n## Transports\n\nTransports are the delivery layer. Every `LogEntry` that passes the logger's level threshold is dispatched to each transport in order. Transports handle their own formatting, level filtering, and delivery.\n\n```ts\nimport { createLogger } from '@vielzeug/rune';\nimport { consoleTransport, pipe, remoteTransport, jsonTransport } from '@vielzeug/rune';\n\nconst log = createLogger({\n logLevel: 'debug',\n transports: [\n // Console output with CSS badges (browser) or plain text (Node)\n consoleTransport({ timestamp: true }),\n // Remote delivery — only errors and above\n remoteTransport({\n handler: async (type, data) => {\n await fetch('/api/logs', { body: JSON.stringify(data), method: 'POST' });\n },\n level: 'error',\n }),\n ],\n});\n```\n\nWhen `transports` is omitted, `consoleTransport()` is used automatically.\n\n### Built-in Transport Factories\n\n| Factory | Use case |\n| -------------------- | ------------------------------------------- |\n| `consoleTransport()` | Styled console output (default) |\n| `remoteTransport()` | HTTP/webhook delivery |\n| `jsonTransport()` | NDJSON for server-side log aggregation |\n| `batchTransport()` | Buffered delivery to reduce I/O overhead |\n| `sampleTransport()` | Probabilistic volume reduction |\n| `redactTransport()` | Sensitive field stripping before forwarding |\n| `pipe()` | Fan-out dispatcher to multiple transports |\n\n### Composing Transports\n\nTransport factories are composable wrappers. Chain them to build a pipeline.\n\n`pipe()` dispatches a single entry to multiple transports independently — an error in one transport does not prevent the others from running:\n\n```ts\nimport { batchTransport, pipe, redactTransport, remoteTransport, sampleTransport } from '@vielzeug/rune';\n\nconst log = createLogger({\n transports: [\n consoleTransport({ level: 'debug' }),\n // redact sensitive fields, sample at 10 %, batch + flush every 30 s\n redactTransport({\n keys: ['password', 'token'],\n transport: sampleTransport({\n rate: 0.1,\n transport: batchTransport({\n onFlush: (entries) => sendToDatadog(entries),\n interval: 30_000,\n }),\n }),\n }),\n ],\n});\n```\n\nUse `pipe()` when you want all transports to receive every entry regardless of per-transport failures:\n\n```ts\nimport { pipe } from '@vielzeug/rune';\n\nconst fanout = pipe(\n { onError: (err) => console.warn('transport error', err) },\n consoleTransport(),\n remoteTransport({ handler, level: 'error' }),\n);\n\nconst log = createLogger({ transports: [fanout] });\n```\n\n### Batch Transport Lifecycle\n\n`batchTransport` starts an interval timer on first use. Call `.dispose()` on application shutdown to flush remaining entries and stop the timer:\n\n```ts\nconst batch = batchTransport({\n onFlush: (entries) => sendToCollector(entries),\n interval: 10_000,\n maxSize: 100,\n});\n\n// Pass batch.transport to the logger — batch itself holds flush/dispose\nconst log = createLogger({ transports: [batch.transport] });\n\n// on shutdown — dispose the batch directly\nprocess.on('exit', () => batch.dispose());\n```\n\n`batchTransport.dispose()` is idempotent — calling it twice is safe and will not double-flush. `[Symbol.dispose]` is also available for `using` declarations.\n\n::: warning\n`log.dispose()` silences the logger but does **not** flush or stop batch transports. Always hold a reference to the `batchTransport` and call `.dispose()` on it explicitly at shutdown.\n:::\n\n::: warning\nAfter `log.dispose()`, the logger is silenced — all log calls (`debug`, `info`, `warn`, `error`, `fatal`, `time`, `group`) become no-ops. The `fn` callback in `group()` still executes, but no group header is rendered. This is intentional to prevent logging after application teardown.\n:::\n\n### Node.js: Structured JSON Logging\n\nFor server-side log pipelines (ELK, Datadog, CloudWatch), `jsonTransport` emits NDJSON to stdout:\n\n```ts\nimport { jsonTransport } from '@vielzeug/rune';\n\nconst log = createLogger({\n namespace: 'api',\n transports: [jsonTransport({ level: 'info' })],\n});\n\nlog.info({ path: '/users', status: 200 }, 'request');\n// Outputs: {\"level\":\"info\",\"time\":\"2026-05-30T...\",\"ns\":\"api\",\"path\":\"/users\",\"status\":200,\"msg\":\"request\"}\n```\n\n## Configuration\n\nUse `child()` to derive immutable logger variants.\n\n```ts\nconst AppLog = defaultLogger.child({\n logLevel: 'warn',\n namespace: 'App',\n // transports inherited from defaultLogger by default\n // pass transports: [] to disable all, or transports: [...] to replace\n});\n\n// Individual getters — no config snapshot\nconsole.log(AppLog.logLevel); // 'warn'\nconsole.log(AppLog.namespace); // 'App'\nconsole.log(AppLog.transports); // [...]\n```\n\nLevel threshold order: `debug` < `info` < `warn` < `error` < `fatal` < `off`\n\n## Call Signature\n\nAll log methods share a consistent three-form signature:\n\n```ts\nlog.info('message'); // string only\nlog.error(err, 'request failed'); // Error first — auto-serialized to data.err\nlog.error(err, { requestId }, 'request failed'); // Error + context + message\nlog.info({ key: 'value' }, 'message'); // context object first, message second\nlog.error({ err: new Error('boom') }, 'request failed'); // Error nested in context — also auto-serialized\n```\n\n- **Error-first form:** pass an `Error` as the first argument. It is automatically serialized to `{ message, name, stack }` under the `err` key in `data`. Optionally follow with a `Bindings` object and/or a message string. This is the idiomatic form when the Error is the primary subject of the call.\n- **Context-first form:** pass a plain object as the first argument. `Error` values nested inside are also auto-serialized. Optionally follow with a message string.\n- **String-only form:** a single string message, no structured context.\n\nThe per-call context is shallow-merged with `withBindings()` bindings into `entry.data`.\n\n## Logging Methods\n\n```ts\ndefaultLogger.debug('debug details');\ndefaultLogger.info({ port: 3000 }, 'server started');\ndefaultLogger.warn('cache stale');\ndefaultLogger.error({ err: new Error('timeout') }, 'request failed'); // Error auto-serialized in context\ndefaultLogger.fatal({ service: 'db' }, 'terminating'); // above error, use for unrecoverable state\n```\n\nUse `enabled()` to avoid expensive payload construction before the level check:\n\n```ts\nif (defaultLogger.enabled('debug')) {\n defaultLogger.debug({ diagnostics: buildLargePayload() }, 'diagnostics');\n}\n```\n\nOr use `lazy()` to let Rune gate it automatically:\n\n```ts\nconst reqLog = defaultLogger.withBindings({ diagnostics: lazy(() => buildLargePayload()) });\nreqLog.debug('diagnostics'); // buildLargePayload() only called when debug is enabled\n```\n\n## Pinned Bindings\n\n`withBindings(fields)` returns a child logger where the given fields are merged into every log call. This is the idiomatic way to attach per-request or per-user context.\n\n```ts\nconst api = defaultLogger.child({ namespace: 'api' });\n\nconst reqLog = api.withBindings({ requestId: 'abc-123', userId: 42 });\nreqLog.info('GET /users'); // always includes requestId and userId\nreqLog.warn({ slow: true }, 'query took 2s'); // call-site fields merged in\n```\n\nThe parent logger is not affected. Bindings stack additively through chained `withBindings()` calls:\n\n```ts\nconst base = defaultLogger.withBindings({ service: 'api' });\nconst req = base.withBindings({ requestId: 'xyz' });\n// req emits both service and requestId on every call\n```\n\nThe `bindings` getter returns a defensive snapshot:\n\n```ts\nconsole.log(reqLog.bindings); // { requestId: 'abc-123', userId: 42 }\n```\n\n## Lazy Bindings\n\n`lazy(fn)` defers evaluation of a binding value until after the level check passes. The factory is never called when the entry would be suppressed.\n\n```ts\nimport { lazy } from '@vielzeug/rune';\n\nconst log = defaultLogger.withBindings({\n // Only called when debug entries are emitted\n snapshot: lazy(() => JSON.stringify(getFullAppState())),\n // Regular values are always included as-is\n service: 'api',\n});\n\nlog.debug('state trace'); // snapshot() only called here\nlog.warn('cache miss'); // snapshot() NOT called — warn doesn't need it\n```\n\nLazy bindings are resolved on every emitted call, not cached:\n\n```ts\nconst counter = { n: 0 };\nconst log = defaultLogger.withBindings({ tick: lazy(() => ++counter.n) });\n\nlog.info('a'); // tick: 1\nlog.info('b'); // tick: 2\n```\n\n## Child Loggers\n\n`child(overrides?)` creates a new logger scoped to a namespace, level, or transport set. Use it to create module-level or service-level loggers.\n\n```ts\nconst api = defaultLogger.child({ namespace: 'api' });\nconst auth = api.child({ namespace: 'auth' }); // → 'api.auth' (dot-joined automatically)\n\napi.info('GET /users');\nauth.warn('token expiring');\n```\n\n`child(overrides?)` clones current config and applies overrides. Transports are inherited by default.\n\n```ts\nconst base = createLogger({ logLevel: 'info', namespace: 'app' });\nconst verbose = base.child({ logLevel: 'debug' }); // inherits transports\n\n// Replace transports entirely on the child\nconst silent = base.child({ transports: [] }); // no output\n\n// Override with a different transport set\nconst jsonChild = base.child({ transports: [jsonTransport()] });\n```\n\nChild and parent configs remain independent after creation.\n\n## Timing\n\n`time(label, fn, level?)` measures execution time of sync or async functions. Emits a structured entry with `{ duration_ms }` in `data` and `label` as the message. When `fn` throws or rejects, the entry also includes `{ err }` with the serialized error.\n\n```ts\n// Sync\nconst result = log.time('parse', () => parseDocument(input));\n// Emits: { level: 'debug', message: 'parse', data: { duration_ms: 2.4 } }\n\n// Async\nconst users = await log.time('db.users', () => db.query('SELECT * FROM users'));\n// Emits even on rejection, with { err } included in data\n\n// Custom level\nlog.time('health-check', () => ping(), 'info');\n\n// Skipped when logLevel is 'off', but fn still executes\n```\n\nTo forward timing data to a remote endpoint, include `remoteTransport` in the pipeline — `debug`-level entries will be forwarded at its threshold.\n\n## Groups\n\n`group(label, fn, level?)` and `groupCollapsed(label, fn, level?)` wrap a callback in a console group, ensuring `groupEnd` is called even when the callback throws or rejects.\n\n```ts\nawait log.groupCollapsed('Job', async () => {\n await log.time('process', () => runJob());\n log.info('Done');\n});\n\n// Gate the group header on a log level — suppresses when logLevel is above 'debug'\nlog.group(\n 'verbose trace',\n () => {\n log.debug('internal state', state);\n },\n 'debug',\n);\n```\n\nWhen `logLevel` is `'off'`, the group wrapper is bypassed but the callback still executes. When a `level` is provided and it is below the configured threshold, the group header is skipped but the callback still runs.\n\n## Testing\n\nUse a test transport to assert log entries without mocking `console`. This approach is more robust and does not require spy cleanup:\n\n```ts\nimport { expect, it } from 'vitest';\nimport { createLogger } from '@vielzeug/rune';\nimport type { LogEntry, Transport } from '@vielzeug/rune';\n\nfunction createTestTransport() {\n const entries: LogEntry[] = [];\n const transport: Transport = (entry) => entries.push(entry);\n return { entries, transport };\n}\n\nit('logs errors when enabled', () => {\n const { entries, transport } = createTestTransport();\n const log = createLogger({ logLevel: 'error', transports: [transport] });\n\n log.error('boom');\n\n expect(entries).toHaveLength(1);\n expect(entries[0].level).toBe('error');\n expect(entries[0].message).toBe('boom');\n});\n\nit('suppresses debug when logLevel is warn', () => {\n const { entries, transport } = createTestTransport();\n const log = createLogger({ logLevel: 'warn', transports: [transport] });\n\n log.debug('silent');\n log.warn('loud');\n\n expect(entries).toHaveLength(1);\n});\n```\n\nYou can still spy on `console` methods when testing `consoleTransport` output directly:\n\n```ts\nimport { afterEach, expect, it, vi } from 'vitest';\nimport { consoleTransport, createLogger } from '@vielzeug/rune';\n\nafterEach(() => vi.restoreAllMocks());\n\nit('writes error to console.error', () => {\n const spy = vi.spyOn(console, 'error').mockImplementation(() => {});\n const log = createLogger({ logLevel: 'error', transports: [consoleTransport({ timestamp: false })] });\n\n log.error('boom');\n\n expect(spy).toHaveBeenCalled();\n});\n```\n\n## Framework Integration\n\nRune is framework-agnostic and works as a module-level singleton or a context-injected instance.\n\n::: code-group\n\n```tsx [React]\nimport { createContext, useState, useContext } from 'react';\nimport { createLogger } from '@vielzeug/rune';\n\nconst LogContext = createContext(createLogger({ namespace: 'app' }));\n\nfunction useLogger() {\n return useContext(LogContext);\n}\n\nfunction App() {\n const [requestLogger] = useState(() => createLogger({ namespace: 'app' }).withBindings({ userId: '42' }));\n return (\n <LogContext.Provider value={requestLogger}>\n <Dashboard />\n </LogContext.Provider>\n );\n}\n\nfunction Dashboard() {\n const log = useLogger();\n log.info('Dashboard mounted');\n return <div>Dashboard</div>;\n}\n```\n\n```ts [Vue 3]\nimport { inject, provide } from 'vue';\nimport { createLogger, type Logger } from '@vielzeug/rune';\n\nconst LoggerKey = Symbol('logger');\n\nfunction provideLogger(namespace: string) {\n const logger = createLogger({ namespace });\n provide(LoggerKey, logger);\n return logger;\n}\n\nfunction useLogger(): Logger {\n const logger = inject<Logger>(LoggerKey);\n if (!logger) throw new Error('Logger not provided');\n return logger;\n}\n```\n\n```svelte [Svelte]\n<script lang=\"ts\">\n import { setContext, getContext } from 'svelte';\n import { createLogger } from '@vielzeug/rune';\n\n const logger = createLogger({ namespace: 'app' });\n setContext('logger', logger);\n</script>\n\n<!-- Child component -->\n<script lang=\"ts\">\n import { getContext } from 'svelte';\n import type { Logger } from '@vielzeug/rune';\n\n const logger = getContext<Logger>('logger');\n logger.info('component mounted');\n</script>\n```\n\n:::\n\n### Pitfalls\n\n- **React:** Creating the logger without a stable initializer recreates it on every re-render. Use `useState(() => createLogger(...))`.\n- **Vue 3:** `inject()` must be called at the top level of `setup()`, not inside callbacks.\n- **Svelte:** `getContext()` must be called synchronously during component initialization.\n\n## Working with Other Vielzeug Libraries\n\n### With Courier\n\n```ts\nimport { createCourier, withLogging } from '@vielzeug/courier';\nimport { createLogger } from '@vielzeug/rune';\n\nconst log = createLogger({ namespace: 'courier' });\nconst courier = createCourier({ baseUrl: 'https://api.example.com' });\ncourier.use(withLogging({ logger: (message, meta) => log.debug(meta, message) }));\n```\n\n### With Herald\n\n```ts\nimport { createBus } from '@vielzeug/herald';\nimport { createLogger } from '@vielzeug/rune';\n\nconst log = createLogger({ namespace: 'bus' });\nconst bus = createBus<AppEvents>({\n onDispatch: (event, payload) => log.debug({ event, payload }, 'dispatched'),\n onError: (err, event) => log.error(err, `handler error in \"${event}\"`),\n});\n```\n\n## Best Practices\n\n- Create one child logger per module boundary using `defaultLogger.child({ namespace: 'module.name' })` or `createLogger('module.name')`.\n- Use `withBindings()` to pin request/session context instead of repeating fields on each call.\n- Use `lazy()` for expensive diagnostics bindings only needed at `debug` level.\n- Set `logLevel` from environment (`'debug'` in dev, `'warn'` or `'error'` in prod).\n- Use `enabled()` before expensive payload construction that `lazy()` cannot defer.\n- Configure transports at the application root; pass scoped loggers via DI or context.\n- Keep remote handlers resilient — network failures should not block app flow.\n- Call `batchTransport.dispose()` on shutdown to flush remaining buffered entries.\n- Use `redactTransport` closest to any remote/persistent transport — never strip before console.\n- To style console output, pass `consoleTransport({ theme })` explicitly in `transports`.\n- Use `fatal()` only for genuinely unrecoverable states.\n",
|
|
4
|
+
"index": "---\ntitle: Rune — Structured logging for TypeScript\ndescription: Browser/Node logger with levels, namespaces, pluggable transports, lazy bindings, and timing helpers.\npackage: rune\ncategory: logging\nkeywords: [logging, console, structured, scoped, transports, remote-logging, levels, namespaces, lazy-bindings]\nrelated: [courier, herald, familiar]\nexports:\n [\n createLogger,\n defaultLogger,\n consoleTransport,\n remoteTransport,\n jsonTransport,\n batchTransport,\n sampleTransport,\n redactTransport,\n pipe,\n lazy,\n isLevelEnabled,\n resolveTheme,\n DEFAULT_THEME,\n PRIORITY,\n RuneError,\n ]\nenvironments: [browser, node, ssr, deno]\n---\n\n<!-- markdownlint-disable MD025 MD033 MD060 -->\n\n<PackageHero package=\"rune\" />\n\n## Why Rune?\n\nPlain `console.log` lacks structure: no log levels, no namespacing, no remote delivery, no way to silence logs in production.\n\n```ts\n// Before — manual approach\nconst path = '/users';\nconsole.log(`[api] GET ${path}`);\nfetch('/api/logs', { body: JSON.stringify({ level: 'error', path }), method: 'POST' });\n\n// After — Rune\nimport { consoleTransport, createLogger, remoteTransport } from '@vielzeug/rune';\n\nconst api = createLogger({\n namespace: 'api',\n transports: [\n consoleTransport({ level: 'debug' }),\n remoteTransport({\n handler: (_type, data) => console.debug('remote log', data),\n level: 'error',\n }),\n ],\n});\n\napi.info({ method: 'GET', path }, 'request');\n```\n\n| Feature | Rune | Winston | Pino | console |\n| -------------------- | ------------------------------------------------------------- | ----------------------------------------------------- | -------------------------------------------------- | ------------------------------------------ |\n| Bundle size | <PackageInfo package=\"rune\" type=\"size\" /> | ~44 kB | ~4 kB | 0 kB |\n| Browser support | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> | <ore-icon name=\"check\" size=\"16\"></ore-icon> |\n| Scoped loggers | <ore-icon name=\"check\" size=\"16\"></ore-icon> | Manual | Child | <ore-icon name=\"x\" size=\"16\"></ore-icon> |\n| Pluggable transports | <ore-icon name=\"check\" size=\"16\"></ore-icon> Built-in factories | <ore-icon name=\"check\" size=\"16\"></ore-icon> Transports | <ore-icon name=\"check\" size=\"16\"></ore-icon> Streams | <ore-icon name=\"x\" size=\"16\"></ore-icon> |\n| Structured log entry | <ore-icon name=\"check\" size=\"16\"></ore-icon> `LogEntry` type | Partial | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> |\n| Lazy bindings | <ore-icon name=\"check\" size=\"16\"></ore-icon> `lazy(fn)` | <ore-icon name=\"x\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> |\n| Styled output | <ore-icon name=\"check\" size=\"16\"></ore-icon> CSS badges | Text only | Text only | Manual |\n| Zero dependencies | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> (15+) | <ore-icon name=\"x\" size=\"16\"></ore-icon> (5+) | N/A |\n\n<div class=\"decision-callout\">\n\n**Use Rune when** you need isomorphic logging (browser + Node.js), namespaced module loggers, or remote error delivery without a heavy dependency chain.\n\n**Consider alternatives when** you need high-throughput file-based logging (Pino), file rotation (Winston), or your team already uses a logging framework.\n\n</div>\n\n## Installation\n\n::: code-group\n\n```sh [pnpm]\npnpm add @vielzeug/rune\n```\n\n```sh [npm]\nnpm install @vielzeug/rune\n```\n\n```sh [yarn]\nyarn add @vielzeug/rune\n```\n\n:::\n\n## Quick Start\n\n```ts\nimport { batchTransport, consoleTransport, createLogger, lazy, remoteTransport } from '@vielzeug/rune';\n\nconst log = createLogger({\n logLevel: 'debug',\n namespace: 'server',\n transports: [\n consoleTransport({ timestamp: true }),\n remoteTransport({\n handler: (_type, data) => console.debug('remote log', data),\n level: 'error',\n }),\n ],\n});\n\nconst requestLog = log.withBindings({\n diagnostics: lazy(() => ({ queueDepth: 0 })),\n requestId: 'abc-123',\n});\n\nrequestLog.info({ method: 'GET', path: '/users' }, 'request');\nconst users = await requestLog.time('load users', () => Promise.resolve(['user-1']));\nconsole.log(users);\n\nconst batch = batchTransport({ onFlush: (entries) => console.debug('batch', entries) });\nconst bufferedLog = createLogger({ transports: [batch.transport] });\n\nbufferedLog.info('queued for delivery');\nawait batch.dispose();\n```\n\n## Features\n\n<div class=\"features-grid\">\n\n- Level filtering (`debug` to `off`) with `enabled()` checks, including `fatal` above `error`\n- Immutable config after construction — use `child()` or `withBindings()` to scope\n- Three call forms: `log.info('msg')`, `log.error(err, { id }, 'msg')` (Error-first), or `log.info({ key: 'val' }, 'msg')` — Error-first form auto-serializes to `data.err`\n- `Error` values in context fields are also auto-serialized to `{ message, name, stack }` — survives JSON.stringify\n- Pinned context bindings via `withBindings({ requestId })` — fields on every line\n- Lazy bindings via `lazy(fn)` — expensive computations gated behind the level check\n- Namespaced child loggers via `createLogger('name')` or `logger.child({ namespace })`\n- Middleware pipeline via `use(fn)` — transform or filter entries before transport dispatch\n- Pluggable transport pipeline: `consoleTransport`, `remoteTransport`, `jsonTransport`, `batchTransport`, `sampleTransport`, `redactTransport`\n- Fan-out via `pipe()` — dispatch to multiple transports independently, fault-tolerant\n- Structured `time()` wrapper: emits the label as message with `{ duration_ms }` in context\n- `group()` and `groupCollapsed()` wrappers that auto-close on throw/reject\n- `LogEntry.data` — single merged flat object for transports; no manual merging needed\n- Zero dependencies — <PackageInfo package=\"rune\" type=\"size\" /> gzipped\n\n</div>\n\n## Documentation\n\n<div class=\"doc-links\">\n\n- [Usage Guide](./usage.md)\n- [API Reference](./api.md)\n- [Examples](./examples.md)\n- [Migration Guide](./migration.md)\n\n</div>\n\n## See Also\n\n<div class=\"see-also\">\n\n- [Courier](/courier/) — HTTP client with built-in request/response interception; pipe Rune as a transport to log every API call with structured context\n- [Herald](/herald/) — typed event bus; emit log-level change or flush events across modules without coupling loggers directly\n- [Familiar](/familiar/) — Web Worker pool; use Rune inside task functions to surface structured worker-side logs back to the main thread\n\n</div>\n\n<!-- markdownlint-enable MD025 MD033 MD060 -->\n",
|
|
5
|
+
"api": "---\ntitle: Rune — API Reference\ndescription: API reference for @vielzeug/rune exports, logger methods, configuration types, and transport factories.\n---\n\n[[toc]]\n\n## API Overview\n\n| Symbol | Purpose | Execution mode | Common gotcha |\n| -------------------- | ------------------------------------------------ | -------------- | ------------------------------------------------------------ |\n| `createLogger()` | Create an isolated `Logger` instance | Sync | Omitting `transports` defaults to `consoleTransport()` |\n| `defaultLogger` | Pre-created default logger singleton | — | Shared instance — use `child()` or `withBindings()` to scope |\n| `lazy(fn)` | Defer a binding value past the level check | Sync | Factory runs on every emit, not once |\n| `pipe()` | Fan-out dispatcher to multiple transports | Sync | Errors in one transport don't propagate to others |\n| `isLevelEnabled()` | Utility: test whether a level passes a threshold | Sync | `'off'` always returns `false` |\n| `PRIORITY` | Numeric priority table backing `isLevelEnabled()`| — | Lower number = more verbose |\n| `resolveTheme()` | Merge a partial theme onto the default | Sync | Returns a fully-populated `ResolvedTheme` |\n| `RuneError` | Base class for all `rune`-originated errors | — | Use `RuneError.is(err)` as the type guard |\n| `consoleTransport()` | Styled console output | Sync | Theme is resolved once at factory call, not per entry |\n| `remoteTransport()` | Async HTTP/webhook delivery | Async | Handler errors are swallowed to `console.warn` |\n| `jsonTransport()` | NDJSON to stdout or a custom sink | Sync | `process.stdout` is unavailable in browsers |\n| `batchTransport()` | Buffered batch delivery with flush interval | Async | Await `.dispose()` and handle rejected delivery |\n| `sampleTransport()` | Probabilistic entry forwarding | Sync | `rate: 1` forwards all entries; `rate: 0` forwards none |\n| `redactTransport()` | Sensitive field stripping before forwarding | Sync | Place this closest to the remote transport, not console |\n\n## Package Entry Point\n\n| Import | Purpose |\n| ---------------- | -------------------------------------------------------- |\n| `@vielzeug/rune` | All exports — logger, transport factories, `lazy`, types |\n\n## createLogger(initial?, options?)\n\nCreates an isolated logger instance.\n\n```ts\ncreateLogger(namespace: string, options?: Omit<RuneOptions, 'namespace'>): Logger\ncreateLogger(options?: RuneOptions): Logger\n```\n\n- `string` shorthand sets namespace: `createLogger('api')` or `createLogger('api', { logLevel: 'warn' })`.\n- Each call produces a fully independent instance — no shared mutable state.\n- Default transport is `consoleTransport()` when `transports` is omitted.\n\n> **Note — disposed loggers:** after `dispose()` is called, all log methods (`debug`, `info`, `warn`, `error`, `fatal`), `time()`, and `group()` / `groupCollapsed()` silently no-op. The `fn` callback in `group()` still runs — only the group header is suppressed.\n\n> **Note — transport/middleware fault isolation:** if a transport or middleware function throws, the logger catches it, reports it via a dev-only warning, and continues — a single misbehaving transport can never crash the caller of `log.info()`/etc., and sibling transports still receive the entry. A throwing middleware drops just that one entry.\n\n**Returns:** `Logger`\n\n**Example:**\n\n```ts\nimport { createLogger } from '@vielzeug/rune';\nimport { consoleTransport, remoteTransport } from '@vielzeug/rune';\n\nconst log = createLogger({ logLevel: 'warn', namespace: 'app' });\n\nconst serverLog = createLogger({\n namespace: 'server',\n transports: [\n consoleTransport(),\n remoteTransport({\n handler: async (_type, data) => {\n await fetch('/api/logs', { body: JSON.stringify(data), method: 'POST' });\n },\n level: 'error',\n }),\n ],\n});\n```\n\n## defaultLogger\n\n`defaultLogger` is the pre-created default logger (`createLogger()` called once at module load).\n\nUse it as a quick-start singleton or create a child for module-level use:\n\n```ts\nimport { defaultLogger } from '@vielzeug/rune';\n\nconst log = defaultLogger.child({ namespace: 'app.worker' });\n```\n\n## lazy(fn)\n\nDefers evaluation of an expensive binding value until after the level check passes.\nThe factory function is never called when the log level suppresses the entry.\n\n```ts\nlazy(fn: () => unknown): LazyBinding\n```\n\n```ts\nimport { lazy } from '@vielzeug/rune';\n\nconst reqLog = log.withBindings({\n diagnostics: lazy(() => buildExpensiveDiagnostics()),\n});\n\nreqLog.debug('trace'); // diagnostics() only called when debug is enabled\n```\n\n**Returns:** `LazyBinding`\n\n## Logger Methods\n\n### Logging\n\nAll five methods share the same signature:\n\n```ts\nlog.debug / info / warn / error / fatal(message: string): void\nlog.debug / info / warn / error / fatal(error: Error, context?: Bindings, message?: string): void\nlog.debug / info / warn / error / fatal(context: Bindings, message?: string): void\n```\n\nArgument rules:\n\n- String-only calls accept a single message argument.\n- **Error-first form:** pass an `Error` as the first argument — it is auto-serialized to `{ message, name, stack }` under the `err` key. Optionally follow with a `Bindings` object and/or a message string.\n- Context object comes first when providing structured data without a top-level Error. `Error` values inside the context object are also auto-serialized to `{ message, name, stack }`.\n\n```ts\nlog.error(err, 'request failed'); // err auto-serialized to data.err\nlog.error(err, { requestId }, 'request failed'); // err + context + message\nlog.error({ err: new Error('boom') }, 'failed'); // Error nested in context object\n```\n\n### Composition\n\n| Method | Returns | What it does |\n| ---------------------- | -------- | ----------------------------------------------------------------- |\n| `child(overrides?)` | `Logger` | Clones config, applies overrides, inherits bindings |\n| `withBindings(fields)` | `Logger` | Pins fields to every subsequent call, returns a new child logger |\n| `use(middleware)` | `Logger` | Appends a middleware function to the pipeline, returns new logger |\n\n`child()` transport inheritance:\n\n- Omit `transports` → inherit parent transports (default).\n- Pass `transports: []` → disable all transports on the child.\n- Pass `transports: [...]` → replace entirely with the given list.\n\n`child()` namespace joining:\n\n- `parent.child({ namespace: 'auth' })` on a logger with namespace `'api'` produces `'api.auth'`.\n- Omit `namespace` → inherits parent namespace unchanged.\n\n### Utilities\n\n| Method | Returns | Description |\n| ----------------------------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| `enabled(level)` | `boolean` | True if entries at this level pass the configured threshold |\n| `time(label, fn, level?)` | `T` | Measures sync/async execution; emits at `level` (default `'debug'`), label as message, `{ duration_ms }` in `data`. When `fn` throws or rejects, `{ err }` is also included. |\n| `group(label, fn, level?)` | `T` | Wraps callback in `console.group`; closes even on throw/reject. Pass `level` to gate the group header on the configured threshold (e.g. `'debug'` suppresses when `logLevel` is `'warn'`). |\n| `groupCollapsed(label, fn, level?)` | `T` | Same as `group`, using `console.groupCollapsed`. |\n| `dispose()` | `void` | Silences all subsequent log calls on this logger instance. Does **not** auto-dispose batch transports — hold a reference and call `batchTransport.dispose()` on shutdown. Idempotent. |\n\n### Properties\n\n| Property | Type | Description |\n| ------------------ | -------------------------- | ------------------------------------------------------------------ |\n| `logLevel` | `LogLevel` | Active log level threshold |\n| `namespace` | `string` | Effective namespace string |\n| `middleware` | `readonly LogMiddleware[]` | Middleware pipeline snapshot |\n| `transports` | `readonly Transport[]` | Transport pipeline snapshot |\n| `bindings` | `Readonly<Bindings>` | Snapshot of currently pinned fields |\n| `disposalSignal` | `AbortSignal` | Aborted when `dispose()` is called. Use to tie external lifetimes. |\n| `disposed` | `boolean` | `true` after `dispose()` has been called |\n| `[Symbol.dispose]` | `() => void` | Delegates to `dispose()`. Enables `using` declarations. |\n\n## Transport Factories\n\n### consoleTransport(options?)\n\n```ts\nconsoleTransport(options?: ConsoleTransportOptions): Transport\n```\n\nWrites styled output to the browser console (CSS badges) or Node terminal (plain text). This is the default transport.\n\n| Option | Type | Default | Description |\n| ----------- | ------------------------ | --------- | --------------------------------------------------- |\n| `level` | `LogLevel` | `'debug'` | Minimum level to output |\n| `timestamp` | `boolean` | `true` | Include `HH:MM:SS.mmm` |\n| `ansi` | `boolean` | auto | Force ANSI color codes on/off (Node only) |\n| `format` | `'json' \\| 'raw'` | `'raw'` | Context serialization: `'json'` uses JSON.stringify |\n| `inspectFn` | `(v: unknown) => string` | — | Custom object formatter (e.g. `util.inspect`) |\n| `theme` | `ConsoleTheme` | — | Override default badge colours for this transport |\n\n**Returns:** `Transport`\n\n**Example:**\n\n```ts\nimport { consoleTransport, createLogger } from '@vielzeug/rune';\nimport { inspect } from 'node:util';\n\nconst log = createLogger({\n transports: [consoleTransport({ level: 'info', timestamp: true, inspectFn: inspect })],\n});\n```\n\n### remoteTransport(options)\n\n```ts\nremoteTransport(options: RemoteTransportOptions): Transport\n```\n\nForwards entries asynchronously to a remote handler. Fire-and-forget — handler errors are swallowed to `console.warn` and never propagate to the caller.\n\n| Option | Type | Default | Description |\n| --------- | ------------------------------- | ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `handler` | `(type: LogType, data: RemoteLogData) => void` | — | Required. Receives each forwarded entry |\n| `level` | `LogLevel` | `'debug'` | Minimum level to forward |\n| `env` | `'production' \\| 'development'` | auto-detected | Override the runtime environment marker |\n| `onError` | `(error: unknown, data: RemoteLogData) => void` | — | Called when the handler throws or rejects. Default: a dev-only `console.warn`. Silent in production — provide an explicit handler for production observability. |\n\n**Returns:** `Transport`\n\n**Example:**\n\n```ts\nimport { createLogger, remoteTransport } from '@vielzeug/rune';\n\nconst log = createLogger({\n transports: [\n remoteTransport({\n handler: async (_type, data) => {\n await fetch('/api/logs', { body: JSON.stringify(data), method: 'POST' });\n },\n level: 'error',\n }),\n ],\n});\n```\n\n### jsonTransport(options?)\n\n```ts\njsonTransport(options?: JsonTransportOptions): Transport\n```\n\nOutputs newline-delimited JSON (NDJSON) to `stdout` or a custom function. Useful for server-side log aggregation pipelines (ELK, Datadog, etc.).\n\nEach line is a flat JSON object with `level`, `time` (ISO), and optional `ns`, `msg`, plus all merged context fields.\n\n| Option | Type | Default | Description |\n| -------- | ------------------------------ | ---------------- | -------------------------------------------------------------------------------------- |\n| `level` | `LogLevel` | `'debug'` | Minimum level |\n| `output` | `(line: string) => void` | `process.stdout` | Custom output sink |\n| `safe` | `boolean` | `false` | Replace circular references with `'[Circular]'` instead of throwing |\n| `fields` | `{ level?, msg?, ns?, time? }` | — | Custom output field names for aggregator compatibility (e.g. `'severity'` for Datadog) |\n\n**Returns:** `Transport`\n\n**Example:**\n\n```ts\nimport { createLogger, jsonTransport } from '@vielzeug/rune';\n\nconst log = createLogger({\n namespace: 'api',\n transports: [jsonTransport({ level: 'info' })],\n});\n\nlog.info({ path: '/users', status: 200 }, 'request');\n// {\"path\":\"/users\",\"status\":200,\"level\":\"info\",\"time\":\"2026-05-30T...\",\"ns\":\"api\",\"msg\":\"request\"}\n```\n\n### batchTransport(options)\n\n```ts\nbatchTransport(options: BatchTransportOptions): BatchHandle\n```\n\nBuffers entries and delivers them in order. Flushes when the buffer reaches `maxSize` or after `interval` elapses; `flush()` and `dispose()` wait for accepted batch delivery.\n\n| Option | Type | Default | Description |\n| -------------- | ------------------------------------------------ | --------- | -------------------------------------------------------------------------------------------- |\n| `onFlush` | `(entries: LogEntry[]) => void \\| Promise<void>` | — | Required. Receives each batch; implement retry here when successful retry must fulfill drain |\n| `onFlushError` | `(entries: LogEntry[], error: unknown) => void` | — | Observes delivery failure; matching `flush()` or later `dispose()` rejects |\n| `level` | `LogLevel` | `'debug'` | Minimum level to buffer |\n| `interval` | `number` | `5000` | Finite interval in milliseconds greater than zero |\n| `maxSize` | `number` | `50` | Finite positive integer batch size before early flush |\n| `maxBuffer` | `number` | unbounded | Finite non-negative integer hard cap; oldest entries drop when exceeded |\n\nReturns a `BatchHandle` with:\n\n- `.transport` — the `Transport` function to pass to `createLogger({ transports: [handle.transport] })`.\n- `.flush()` — immediately send buffered entries and resolve after delivery; rejects when delivery fails.\n- `.dispose()` — stop the interval, reject new entries, and settle after every accepted batch completes. Rejects if any automatic or final delivery fails. Idempotent.\n- `.disposed` — `true` when disposal starts.\n- `[Symbol.asyncDispose]()` — delegates to `.dispose()`. Enables `await using` declarations.\n\nAfter `dispose()`, the transport becomes inert: new entries are silently dropped.\n\n**Returns:** `BatchHandle`\n\n**Example:**\n\n```ts\nimport { batchTransport, createLogger } from '@vielzeug/rune';\n\nconst batch = batchTransport({\n interval: 10_000,\n maxSize: 100,\n onFlush: (entries) => console.debug('batch', entries),\n});\n\nconst log = createLogger({ transports: [batch.transport] });\n\nasync function shutdown() {\n await batch.dispose();\n}\n```\n\n### sampleTransport(options)\n\n```ts\nsampleTransport(options: SampleTransportOptions): Transport\n```\n\nProbabilistically forwards entries to a downstream transport.\n\n| Option | Type | Default | Description |\n| ----------- | ----------- | --------- | ---------------------------------------------- |\n| `rate` | `number` | — | Required finite fraction of entries to forward (0–1) |\n| `transport` | `Transport` | — | Required. Downstream transport |\n| `level` | `LogLevel` | `'debug'` | Minimum level to sample |\n\n**Returns:** `Transport`\n\n**Example:**\n\n```ts\nimport { createLogger, remoteTransport, sampleTransport } from '@vielzeug/rune';\n\nconst log = createLogger({\n transports: [\n sampleTransport({\n rate: 0.1,\n transport: remoteTransport({ handler: (_type, data) => console.debug('sampled log', data) }),\n }),\n ],\n});\n```\n\n### redactTransport(options)\n\n```ts\nredactTransport(options: RedactTransportOptions): Transport\n```\n\nStrips sensitive fields from `bindings` and `context` before forwarding. Redaction is applied recursively at any depth (up to 20 levels).\n\n::: warning Key matching\n`keys` matches **exact field names** at any nesting depth. Dot-path notation (e.g. `'user.password'`) is **not** supported — use `'password'` to redact every field named `password` regardless of nesting.\n:::\n\n| Option | Type | Default | Description |\n| ------------- | ----------- | -------------- | ------------------------------- |\n| `keys` | `string[]` | — | Required. Field names to redact |\n| `replacement` | `string` | `'[REDACTED]'` | Replacement value |\n| `transport` | `Transport` | — | Required. Downstream transport |\n\n**Returns:** `Transport`\n\n**Example:**\n\n```ts\nimport { createLogger, redactTransport, remoteTransport } from '@vielzeug/rune';\n\nconst log = createLogger({\n transports: [\n redactTransport({\n keys: ['password', 'token', 'ssn'],\n transport: remoteTransport({ handler: (_type, data) => console.debug('redacted log', data) }),\n }),\n ],\n});\n```\n\n### pipe(...transports) / pipe(options, ...transports)\n\n```ts\npipe(...transports: Transport[]): Transport\npipe(options: PipeOptions, ...transports: Transport[]): Transport\n```\n\nDispatches each `LogEntry` to every transport in the list independently. An error thrown by one transport does not stop the others. Use in place of separate array entries when you want fault isolation or a shared error observer.\n\n`pipe()` with no arguments creates a valid no-op transport — useful for conditional pipeline construction: `pipe(condition ? remoteTransport(opts) : undefined!)` pattern, or simply as a placeholder during development.\n\n| Option | Type | Description |\n| --------- | ------------------------------------------- | --------------------------------------------------------- |\n| `onError` | `(error: unknown, entry: LogEntry) => void` | Called with the error and entry when any transport throws |\n\n**Returns:** `Transport`\n\n**Example:**\n\n```ts\nimport { consoleTransport, createLogger, pipe, remoteTransport } from '@vielzeug/rune';\n\nconst log = createLogger({\n transports: [\n pipe(\n { onError: (error) => console.warn('transport error', error) },\n consoleTransport(),\n remoteTransport({\n handler: (_type, data) => console.debug('remote log', data),\n level: 'error',\n }),\n ),\n ],\n});\n```\n\n````\n\n## Utilities\n\n### isLevelEnabled(threshold, level)\n\n```ts\nisLevelEnabled(threshold: LogLevel, level: LogLevel): boolean\n````\n\nReturns `true` when `level` is at or above `threshold`. Always returns `false` when `level` is `'off'`. Useful for building custom transports that respect level filtering.\n\n```ts\nimport { isLevelEnabled } from '@vielzeug/rune';\n\nisLevelEnabled('warn', 'error'); // true\nisLevelEnabled('warn', 'info'); // false\nisLevelEnabled('debug', 'off'); // false\n```\n\n### resolveTheme(override?)\n\n```ts\nresolveTheme(override: ConsoleTheme | undefined): ResolvedTheme\n```\n\nDeep-merges a partial `ConsoleTheme` override onto `DEFAULT_THEME`. Returns a fully-populated `ResolvedTheme` where every level and every field is present. Used internally by `consoleTransport()` — call directly when building a custom transport that needs to honour theme overrides.\n\n```ts\nimport { resolveTheme } from '@vielzeug/rune';\n\nconst theme = resolveTheme({ warn: { badge: '⚡' } });\n// theme.warn.badge === '⚡', theme.warn.bg === DEFAULT_THEME.warn.bg (unchanged)\n```\n\n### DEFAULT_THEME\n\nThe built-in badge and namespace colour definitions used by `consoleTransport()`. Override per-transport via `ConsoleTransportOptions.theme`.\n\n### PRIORITY\n\n```ts\nPRIORITY: Record<LogLevel, number>\n```\n\nNumeric priority for each level (`debug: 0`, `info: 1`, `warn: 2`, `error: 3`, `fatal: 4`, `off: 5`) — lower is more verbose. Exported for transport/middleware authors building custom level-comparison logic; `isLevelEnabled()` is built directly on top of it.\n\n## Errors\n\n### RuneError\n\nBase class for all `rune`-originated errors. Use `instanceof RuneError` or `RuneError.is(err)` to catch any error the package throws.\n\n```ts\nimport { RuneError } from '@vielzeug/rune';\n\ntry {\n // ...\n} catch (err) {\n if (RuneError.is(err)) {\n // handle a rune-originated error\n }\n}\n```\n\n**Static methods:**\n\n| Method | Returns | Description |\n| ------------ | ----------------------- | --------------------------------------------- |\n| `is(err)` | `err is RuneError` | Type guard — `true` for `RuneError` and subclasses |\n\n## Types\n\n### LogType\n\n`'debug' | 'error' | 'fatal' | 'info' | 'warn'`\n\n### LogLevel\n\n`LogType | 'off'` — threshold order: `debug < info < warn < error < fatal < off`\n\n### Bindings\n\n`Record<string, unknown>` — Key-value context pinned via `withBindings()` or passed per-call.\n\n### LogEntry\n\nThe structured record produced by every log call and dispatched to all transports.\n\n| Field | Type | Description |\n| ----------- | -------------------- | ------------------------------------------------------------------------ |\n| `data` | `Readonly<Bindings>` | Merged result of pinned bindings and per-call context — already resolved |\n| `level` | `LogType` | Log level |\n| `message` | `string?` | Log message |\n| `namespace` | `string` | Effective namespace at time of call |\n| `timestamp` | `Date` | Exact moment of the call, shared across transports |\n\n### Transport\n\n```ts\ntype Transport = (entry: LogEntry) => void;\n```\n\nReceives every `LogEntry` that passes the logger's level threshold. Responsible for its own formatting, delivery, and per-transport level filtering.\n\n### RemoteLogData\n\nPayload shape delivered to `RemoteTransportOptions.handler`:\n\n| Field | Type | Description |\n| ----------- | ------------------------------- | ----------------------------------------- |\n| `data` | `Bindings?` | Merged structured data (omitted if empty) |\n| `env` | `'production' \\| 'development'` | Runtime env marker |\n| `level` | `LogType` | Log level |\n| `message` | `string?` | Log message |\n| `namespace` | `string?` | Effective namespace |\n| `timestamp` | `string` | Full ISO timestamp |\n\n### PipeOptions\n\n| Field | Type | Description |\n| --------- | ------------------------------------------- | ----------------------------------------------------- |\n| `onError` | `(error: unknown, entry: LogEntry) => void` | Called when a transport in the pipe throws or rejects |\n\n### ResolvedTheme\n\n`Record<LogType | 'group' | 'ns', ConsoleThemeEntry>` — fully resolved theme with all fields populated.\n\n### RuneOptions\n\n| Field | Type | Default | Description |\n| ------------ | ------------------ | ---------------------- | ---------------------------- |\n| `logLevel` | `LogLevel?` | `'debug'` | Logger level threshold |\n| `namespace` | `string?` | `''` | Namespace prefix |\n| `transports` | `Transport[]?` | `[consoleTransport()]` | Transport pipeline |\n| `bindings` | `Bindings?` | `{}` | Initial pinned bindings |\n| `middleware` | `LogMiddleware[]?` | `[]` | Entry transform/filter chain |\n\n### LogMethod\n\n```ts\ntype LogMethod = {\n (message: string): void;\n (error: Error, context?: Bindings, message?: string): void;\n (context: Bindings, message?: string): void;\n};\n```\n\nEvery log-level method uses this signature. Three call forms are supported:\n\n- **String-only:** `log.info('message')`\n- **Error-first:** `log.error(err, { requestId }, 'failed')` — `Error` is auto-serialized to `{ message, name, stack }` under `data.err`. Optionally follow with a `Bindings` object and/or a message string.\n- **Context-first:** `log.info({ key: 'value' }, 'message')` — structured context object, optional message. `Error` values nested inside the context are also auto-serialized.\n\n### LogMiddleware\n\n```ts\ntype LogMiddleware = (entry: LogEntry) => LogEntry | null;\n```\n\nMiddleware functions intercept entries before they reach transports. Return the (optionally mutated) entry to continue, or return `null` to drop the entry. Added via `use(fn)` or `RuneOptions.middleware`.\n\n### LazyBinding\n\nOpaque type returned by `lazy()`. Pass as a value inside `withBindings()`. The factory is only called when the entry is actually emitted (after the level check passes).\n\n### BatchHandle\n\n```ts\ntype BatchHandle = {\n [Symbol.asyncDispose]: () => Promise<void>;\n dispose: () => Promise<void>;\n readonly disposed: boolean;\n flush: () => Promise<void>;\n transport: Transport;\n};\n```\n\nReturned by `batchTransport()`. Pass `handle.transport` to `createLogger({ transports })`; await `handle.dispose()` during graceful shutdown. `disposed` is `true` when disposal starts.\n\n### Logger\n\nThe full interface returned by `createLogger()` and `defaultLogger`:\n\n```ts\ntype Logger = {\n [Symbol.dispose]: () => void;\n readonly bindings: Readonly<Bindings>;\n child: (overrides?: RuneOptions) => Logger;\n debug: LogMethod;\n readonly disposalSignal: AbortSignal;\n dispose: () => void;\n readonly disposed: boolean;\n enabled: (type: LogLevel) => boolean;\n error: LogMethod;\n fatal: LogMethod;\n group: <T>(label: string, fn: () => T, level?: LogType) => T;\n groupCollapsed: <T>(label: string, fn: () => T, level?: LogType) => T;\n info: LogMethod;\n readonly logLevel: LogLevel;\n readonly middleware: readonly LogMiddleware[];\n readonly namespace: string;\n time: <T>(label: string, fn: () => T, level?: LogType) => T;\n readonly transports: readonly Transport[];\n use: (middleware: LogMiddleware) => Logger;\n warn: LogMethod;\n /** Returns a new child logger with additional pinned bindings. The returned logger is fully independent — disposing it does not affect the parent, and vice versa. */\n withBindings: (bindings: Bindings) => Logger;\n};\n```\n\n### ConsoleTransportOptions\n\n| Field | Type | Default | Description |\n| ----------- | ------------------------ | --------- | --------------------------------------------------- |\n| `level` | `LogLevel` | `'debug'` | Minimum level to output |\n| `timestamp` | `boolean` | `true` | Include `HH:MM:SS.mmm` |\n| `ansi` | `boolean` | auto | Force ANSI color codes on/off (Node only) |\n| `format` | `'json' \\| 'raw'` | `'raw'` | Context serialization: `'json'` uses JSON.stringify |\n| `inspectFn` | `(v: unknown) => string` | — | Custom object formatter (e.g. `util.inspect`) |\n| `theme` | `ConsoleTheme` | — | Override default badge colours for this transport |\n\n### RemoteTransportOptions\n\n| Field | Type | Default | Description |\n| --------- | ------------------------------- | ------------- | --------------------------------------- |\n| `handler` | `(type: LogType, data: RemoteLogData) => void` | — | Required. Receives each forwarded entry |\n| `level` | `LogLevel` | `'debug'` | Minimum level to forward |\n| `env` | `'production' \\| 'development'` | auto-detected | Override the runtime environment marker |\n| `onError` | `(error: unknown, data: RemoteLogData) => void` | — | Called when the handler throws |\n\n### JsonTransportOptions\n\n| Field | Type | Default | Description |\n| -------- | ------------------------------ | ---------------- | ------------------------------------------------------------------- |\n| `level` | `LogLevel` | `'debug'` | Minimum level |\n| `output` | `(line: string) => void` | `process.stdout` | Custom output sink |\n| `safe` | `boolean` | `false` | Replace circular references with `'[Circular]'` instead of throwing |\n| `fields` | `{ level?, msg?, ns?, time? }` | — | Custom output field names (e.g. `level: 'severity'` for Datadog) |\n\n### BatchTransportOptions\n\n| Field | Type | Default | Description |\n| -------------- | ------------------------------------------------ | --------- | --------------------------------------------------------- |\n| `onFlush` | `(entries: LogEntry[]) => void \\| Promise<void>` | — | Required. Receives each batch (may be async) |\n| `onFlushError` | `(entries: LogEntry[], error: unknown) => void` | — | Observes delivery failure; matching `flush()` or later `dispose()` rejects |\n| `level` | `LogLevel` | `'debug'` | Minimum level to buffer |\n| `interval` | `number` | `5000` | Finite interval in milliseconds greater than zero |\n| `maxSize` | `number` | `50` | Finite positive integer batch size before early flush |\n| `maxBuffer` | `number` | unbounded | Finite non-negative integer cap; drops oldest entries when exceeded |\n\n### SampleTransportOptions\n\n| Field | Type | Default | Description |\n| ----------- | ----------- | --------- | ---------------------------------------------- |\n| `rate` | `number` | — | Required finite fraction of entries to forward (0–1) |\n| `transport` | `Transport` | — | Required. Downstream transport |\n| `level` | `LogLevel` | `'debug'` | Minimum level to sample |\n\n### RedactTransportOptions\n\n| Field | Type | Default | Description |\n| ------------- | ----------- | -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `keys` | `string[]` | — | Required. Field names to redact at any depth |\n| `maxDepth` | `number` | `20` | Finite non-negative integer nesting depth. Fields deeper than this are not redacted — a dev-only warning is emitted when hit. **Security:** the warning is suppressed in production; ensure sensitive fields are not nested beyond this limit. |\n| `replacement` | `string` | `'[REDACTED]'` | Replacement value |\n| `transport` | `Transport` | — | Required. Downstream transport |\n",
|
|
6
|
+
"usage": "---\ntitle: Rune — Usage Guide\ndescription: Configuration, transports, scoped loggers, lazy bindings, timers, groups, and best practices for Rune.\n---\n\n[[toc]]\n\n::: tip New to Rune?\nStart with the [Overview](./index.md), then use this page for detailed usage patterns.\n:::\n\n## Basic Usage\n\n`defaultLogger` is the default singleton logger instance. Use `createLogger()` for isolated config.\n\n```ts\nimport { createLogger, defaultLogger } from '@vielzeug/rune';\n\nconst appLog = defaultLogger;\nconst apiLog = createLogger({ namespace: 'api' });\nconst authLog = createLogger('auth'); // shorthand namespace\n```\n\nEach `createLogger()` call is fully independent with its own transport pipeline.\n\nThe two-arg shorthand combines namespace and options cleanly:\n\n```ts\nconst log = createLogger('api', { logLevel: 'warn', transports: [transport] });\n```\n\n## Transports\n\nTransports are the delivery layer. Every `LogEntry` that passes the logger's level threshold is dispatched to each transport in order. Transports handle their own formatting, level filtering, and delivery.\n\n```ts\nimport { consoleTransport, createLogger, remoteTransport } from '@vielzeug/rune';\n\nconst log = createLogger({\n logLevel: 'debug',\n transports: [\n consoleTransport({ timestamp: true }),\n remoteTransport({\n handler: (_type, data) => console.debug('remote log', data),\n level: 'error',\n }),\n ],\n});\n```\n\nWhen `transports` is omitted, `consoleTransport()` is used automatically.\n\n### Built-in Transport Factories\n\n| Factory | Use case |\n| -------------------- | ------------------------------------------- |\n| `consoleTransport()` | Styled console output (default) |\n| `remoteTransport()` | HTTP/webhook delivery |\n| `jsonTransport()` | NDJSON for server-side log aggregation |\n| `batchTransport()` | Buffered delivery to reduce I/O overhead |\n| `sampleTransport()` | Probabilistic volume reduction |\n| `redactTransport()` | Sensitive field stripping before forwarding |\n| `pipe()` | Fan-out dispatcher to multiple transports |\n\n### Composing Transports\n\nTransport factories are composable wrappers. Chain them to build a pipeline.\n\nWrap a downstream transport to redact fields, sample volume, and batch delivery:\n\n```ts\nimport { batchTransport, consoleTransport, createLogger, redactTransport, sampleTransport } from '@vielzeug/rune';\n\nconst batch = batchTransport({\n interval: 30_000,\n onFlush: (entries) => console.debug('batch', entries),\n});\n\nconst log = createLogger({\n transports: [\n consoleTransport({ level: 'debug' }),\n redactTransport({\n keys: ['password', 'token'],\n transport: sampleTransport({\n rate: 0.1,\n transport: batch.transport,\n }),\n }),\n ],\n});\n\nawait batch.dispose();\n```\n\nUse `pipe()` when every downstream transport must receive an entry despite sibling transport failures:\n\n```ts\nimport { consoleTransport, createLogger, pipe, remoteTransport } from '@vielzeug/rune';\n\nconst fanout = pipe(\n { onError: (error) => console.warn('transport error', error) },\n consoleTransport(),\n remoteTransport({\n handler: (_type, data) => console.debug('remote log', data),\n level: 'error',\n }),\n);\n\nconst log = createLogger({ transports: [fanout] });\n```\n\n### Batch Transport Lifecycle\n\n`batchTransport` starts an interval timer on first use. Await `.dispose()` during graceful application shutdown to stop the timer and finish delivery for every accepted batch:\n\n```ts\nimport { batchTransport, createLogger } from '@vielzeug/rune';\n\nconst batch = batchTransport({\n interval: 10_000,\n maxSize: 100,\n onFlush: (entries) => console.debug('batch', entries),\n});\n\nconst log = createLogger({ transports: [batch.transport] });\n\nasync function shutdown() {\n try {\n await batch.dispose();\n } catch (error) {\n console.error('log delivery failed during shutdown', error);\n throw error;\n }\n}\n```\n\n`batchTransport.dispose()` is idempotent — repeated calls return the same drain promise and never double-flush. It rejects when an accepted batch cannot deliver. `[Symbol.asyncDispose]` is available for `await using` declarations. Do not use a Node `exit` handler: Node cannot await asynchronous cleanup there.\n\n::: warning\n`log.dispose()` silences the logger but does not flush or stop batch transports. Keep a direct batch reference and `await batch.dispose()` during shutdown.\n:::\n\n::: warning\nAfter `log.dispose()`, the logger is silenced — all log calls (`debug`, `info`, `warn`, `error`, `fatal`, `time`, `group`) become no-ops. The `fn` callback in `group()` still executes, but no group header is rendered. This is intentional to prevent logging after application teardown.\n:::\n\n### Node.js: Structured JSON Logging\n\nFor server-side log pipelines (ELK, Datadog, CloudWatch), `jsonTransport` emits NDJSON to stdout:\n\n```ts\nimport { jsonTransport } from '@vielzeug/rune';\n\nconst log = createLogger({\n namespace: 'api',\n transports: [jsonTransport({ level: 'info' })],\n});\n\nlog.info({ path: '/users', status: 200 }, 'request');\n// Outputs: {\"level\":\"info\",\"time\":\"2026-05-30T...\",\"ns\":\"api\",\"path\":\"/users\",\"status\":200,\"msg\":\"request\"}\n```\n\n## Configuration\n\nUse `child()` to derive immutable logger variants.\n\n```ts\nconst AppLog = defaultLogger.child({\n logLevel: 'warn',\n namespace: 'App',\n // transports inherited from defaultLogger by default\n // pass transports: [] to disable all, or transports: [...] to replace\n});\n\n// Individual getters — no config snapshot\nconsole.log(AppLog.logLevel); // 'warn'\nconsole.log(AppLog.namespace); // 'App'\nconsole.log(AppLog.transports); // [...]\n```\n\nLevel threshold order: `debug` < `info` < `warn` < `error` < `fatal` < `off`\n\n## Call Signature\n\nAll log methods share a consistent three-form signature:\n\n```ts\nlog.info('message'); // string only\nlog.error(err, 'request failed'); // Error first — auto-serialized to data.err\nlog.error(err, { requestId }, 'request failed'); // Error + context + message\nlog.info({ key: 'value' }, 'message'); // context object first, message second\nlog.error({ err: new Error('boom') }, 'request failed'); // Error nested in context — also auto-serialized\n```\n\n- **Error-first form:** pass an `Error` as the first argument. It is automatically serialized to `{ message, name, stack }` under the `err` key in `data`. Optionally follow with a `Bindings` object and/or a message string. This is the idiomatic form when the Error is the primary subject of the call.\n- **Context-first form:** pass a plain object as the first argument. `Error` values nested inside are also auto-serialized. Optionally follow with a message string.\n- **String-only form:** a single string message, no structured context.\n\nThe per-call context is shallow-merged with `withBindings()` bindings into `entry.data`.\n\n## Logging Methods\n\n```ts\ndefaultLogger.debug('debug details');\ndefaultLogger.info({ port: 3000 }, 'server started');\ndefaultLogger.warn('cache stale');\ndefaultLogger.error({ err: new Error('timeout') }, 'request failed'); // Error auto-serialized in context\ndefaultLogger.fatal({ service: 'db' }, 'terminating'); // above error, use for unrecoverable state\n```\n\nUse `enabled()` to avoid expensive payload construction before the level check:\n\n```ts\nif (defaultLogger.enabled('debug')) {\n defaultLogger.debug({ diagnostics: buildLargePayload() }, 'diagnostics');\n}\n```\n\nOr use `lazy()` to let Rune gate it automatically:\n\n```ts\nconst reqLog = defaultLogger.withBindings({ diagnostics: lazy(() => buildLargePayload()) });\nreqLog.debug('diagnostics'); // buildLargePayload() only called when debug is enabled\n```\n\n## Pinned Bindings\n\n`withBindings(fields)` returns a child logger where the given fields are merged into every log call. This is the idiomatic way to attach per-request or per-user context.\n\n```ts\nconst api = defaultLogger.child({ namespace: 'api' });\n\nconst reqLog = api.withBindings({ requestId: 'abc-123', userId: 42 });\nreqLog.info('GET /users'); // always includes requestId and userId\nreqLog.warn({ slow: true }, 'query took 2s'); // call-site fields merged in\n```\n\nThe parent logger is not affected. Bindings stack additively through chained `withBindings()` calls:\n\n```ts\nconst base = defaultLogger.withBindings({ service: 'api' });\nconst req = base.withBindings({ requestId: 'xyz' });\n// req emits both service and requestId on every call\n```\n\nThe `bindings` getter returns a defensive snapshot:\n\n```ts\nconsole.log(reqLog.bindings); // { requestId: 'abc-123', userId: 42 }\n```\n\n## Lazy Bindings\n\n`lazy(fn)` defers evaluation of a binding value until after the level check passes. The factory is never called when the entry would be suppressed.\n\n```ts\nimport { lazy } from '@vielzeug/rune';\n\nconst log = defaultLogger.withBindings({\n // Only called when debug entries are emitted\n snapshot: lazy(() => JSON.stringify(getFullAppState())),\n // Regular values are always included as-is\n service: 'api',\n});\n\nlog.debug('state trace'); // snapshot() only called here\nlog.warn('cache miss'); // snapshot() NOT called — warn doesn't need it\n```\n\nLazy bindings are resolved on every emitted call, not cached:\n\n```ts\nconst counter = { n: 0 };\nconst log = defaultLogger.withBindings({ tick: lazy(() => ++counter.n) });\n\nlog.info('a'); // tick: 1\nlog.info('b'); // tick: 2\n```\n\n## Child Loggers\n\n`child(overrides?)` creates a new logger scoped to a namespace, level, or transport set. Use it to create module-level or service-level loggers.\n\n```ts\nconst api = defaultLogger.child({ namespace: 'api' });\nconst auth = api.child({ namespace: 'auth' }); // → 'api.auth' (dot-joined automatically)\n\napi.info('GET /users');\nauth.warn('token expiring');\n```\n\n`child(overrides?)` clones current config and applies overrides. Transports are inherited by default.\n\n```ts\nconst base = createLogger({ logLevel: 'info', namespace: 'app' });\nconst verbose = base.child({ logLevel: 'debug' }); // inherits transports\n\n// Replace transports entirely on the child\nconst silent = base.child({ transports: [] }); // no output\n\n// Override with a different transport set\nconst jsonChild = base.child({ transports: [jsonTransport()] });\n```\n\nChild and parent configs remain independent after creation.\n\n## Timing\n\n`time(label, fn, level?)` measures execution time of sync or async functions. Emits a structured entry with `{ duration_ms }` in `data` and `label` as the message. When `fn` throws or rejects, the entry also includes `{ err }` with the serialized error.\n\n```ts\n// Sync\nconst result = log.time('parse', () => parseDocument(input));\n// Emits: { level: 'debug', message: 'parse', data: { duration_ms: 2.4 } }\n\n// Async\nconst users = await log.time('db.users', () => db.query('SELECT * FROM users'));\n// Emits even on rejection, with { err } included in data\n\n// Custom level\nlog.time('health-check', () => ping(), 'info');\n\n// Skipped when logLevel is 'off', but fn still executes\n```\n\nTo forward timing data to a remote endpoint, include `remoteTransport` in the pipeline — `debug`-level entries will be forwarded at its threshold.\n\n## Groups\n\n`group(label, fn, level?)` and `groupCollapsed(label, fn, level?)` wrap a callback in a console group, ensuring `groupEnd` is called even when the callback throws or rejects.\n\n```ts\nawait log.groupCollapsed('Job', async () => {\n await log.time('process', () => runJob());\n log.info('Done');\n});\n\n// Gate the group header on a log level — suppresses when logLevel is above 'debug'\nlog.group(\n 'verbose trace',\n () => {\n log.debug('internal state', state);\n },\n 'debug',\n);\n```\n\nWhen `logLevel` is `'off'`, the group wrapper is bypassed but the callback still executes. When a `level` is provided and it is below the configured threshold, the group header is skipped but the callback still runs.\n\n## Testing\n\nUse a test transport to assert log entries without mocking `console`. This approach is more robust and does not require spy cleanup:\n\n```ts\nimport { expect, it } from 'vitest';\nimport { createLogger } from '@vielzeug/rune';\nimport type { LogEntry, Transport } from '@vielzeug/rune';\n\nfunction createTestTransport() {\n const entries: LogEntry[] = [];\n const transport: Transport = (entry) => entries.push(entry);\n return { entries, transport };\n}\n\nit('logs errors when enabled', () => {\n const { entries, transport } = createTestTransport();\n const log = createLogger({ logLevel: 'error', transports: [transport] });\n\n log.error('boom');\n\n expect(entries).toHaveLength(1);\n expect(entries[0].level).toBe('error');\n expect(entries[0].message).toBe('boom');\n});\n\nit('suppresses debug when logLevel is warn', () => {\n const { entries, transport } = createTestTransport();\n const log = createLogger({ logLevel: 'warn', transports: [transport] });\n\n log.debug('silent');\n log.warn('loud');\n\n expect(entries).toHaveLength(1);\n});\n```\n\nYou can still spy on `console` methods when testing `consoleTransport` output directly:\n\n```ts\nimport { afterEach, expect, it, vi } from 'vitest';\nimport { consoleTransport, createLogger } from '@vielzeug/rune';\n\nafterEach(() => vi.restoreAllMocks());\n\nit('writes error to console.error', () => {\n const spy = vi.spyOn(console, 'error').mockImplementation(() => {});\n const log = createLogger({ logLevel: 'error', transports: [consoleTransport({ timestamp: false })] });\n\n log.error('boom');\n\n expect(spy).toHaveBeenCalled();\n});\n```\n\n## Framework Integration\n\nRune is framework-agnostic and works as a module-level singleton or a context-injected instance.\n\n::: code-group\n\n```tsx [React]\nimport { createContext, useState, useContext } from 'react';\nimport { createLogger } from '@vielzeug/rune';\n\nconst LogContext = createContext(createLogger({ namespace: 'app' }));\n\nfunction useLogger() {\n return useContext(LogContext);\n}\n\nfunction App() {\n const [requestLogger] = useState(() => createLogger({ namespace: 'app' }).withBindings({ userId: '42' }));\n return (\n <LogContext.Provider value={requestLogger}>\n <Dashboard />\n </LogContext.Provider>\n );\n}\n\nfunction Dashboard() {\n const log = useLogger();\n log.info('Dashboard mounted');\n return <div>Dashboard</div>;\n}\n```\n\n```ts [Vue 3]\nimport { inject, provide } from 'vue';\nimport { createLogger, type Logger } from '@vielzeug/rune';\n\nconst LoggerKey = Symbol('logger');\n\nfunction provideLogger(namespace: string) {\n const logger = createLogger({ namespace });\n provide(LoggerKey, logger);\n return logger;\n}\n\nfunction useLogger(): Logger {\n const logger = inject<Logger>(LoggerKey);\n if (!logger) throw new Error('Logger not provided');\n return logger;\n}\n```\n\n```svelte [Svelte]\n<script lang=\"ts\">\n import { setContext, getContext } from 'svelte';\n import { createLogger } from '@vielzeug/rune';\n\n const logger = createLogger({ namespace: 'app' });\n setContext('logger', logger);\n</script>\n\n<!-- Child component -->\n<script lang=\"ts\">\n import { getContext } from 'svelte';\n import type { Logger } from '@vielzeug/rune';\n\n const logger = getContext<Logger>('logger');\n logger.info('component mounted');\n</script>\n```\n\n:::\n\n### Pitfalls\n\n- **React:** Creating the logger without a stable initializer recreates it on every re-render. Use `useState(() => createLogger(...))`.\n- **Vue 3:** `inject()` must be called at the top level of `setup()`, not inside callbacks.\n- **Svelte:** `getContext()` must be called synchronously during component initialization.\n\n## Working with Other Vielzeug Libraries\n\n### With Courier\n\n```ts\nimport { createCourier, withLogging } from '@vielzeug/courier';\nimport { createLogger } from '@vielzeug/rune';\n\nconst log = createLogger({ namespace: 'courier' });\nconst courier = createCourier({ baseUrl: 'https://api.example.com' });\ncourier.use(withLogging({ logger: (message, meta) => log.debug(meta, message) }));\n```\n\n### With Herald\n\n```ts\nimport { createBus } from '@vielzeug/herald';\nimport { createLogger } from '@vielzeug/rune';\n\nconst log = createLogger({ namespace: 'bus' });\nconst bus = createBus<AppEvents>({\n onDispatch: (event, payload) => log.debug({ event, payload }, 'dispatched'),\n onError: (err, event) => log.error(err, `handler error in \"${event}\"`),\n});\n```\n\n## Best Practices\n\n- Create one child logger per module boundary using `defaultLogger.child({ namespace: 'module.name' })` or `createLogger('module.name')`.\n- Use `withBindings()` to pin request/session context instead of repeating fields on each call.\n- Use `lazy()` for expensive diagnostics bindings only needed at `debug` level.\n- Set `logLevel` from environment (`'debug'` in dev, `'warn'` or `'error'` in prod).\n- Use `enabled()` before expensive payload construction that `lazy()` cannot defer.\n- Configure transports at the application root; pass scoped loggers via DI or context.\n- Keep remote handlers resilient — network failures should not block app flow.\n- Await `batchTransport.dispose()` during graceful shutdown to drain remaining accepted entries.\n- Use `redactTransport` closest to any remote/persistent transport — never strip before console.\n- To style console output, pass `consoleTransport({ theme })` explicitly in `transports`.\n- Use `fatal()` only for genuinely unrecoverable states.\n",
|
|
7
7
|
"examples": "---\ntitle: Rune — Examples\ndescription: Practical examples and recipes for rune.\n---\n\n## Examples\n\n- [Module Logger Pattern](./examples/module-logger-pattern.md)\n- [Child Logger Overrides](./examples/child-logger-overrides.md)\n- [Production Setup](./examples/production-setup.md)\n- [Timing And Grouping](./examples/timing-and-grouping.md)\n- [React Integration](./examples/react-integration.md)\n- [Request Middleware](./examples/request-middleware.md)\n- [Testing](./examples/testing.md)\n"
|
|
8
8
|
},
|
|
9
9
|
"examples": [
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
},
|
|
25
25
|
{
|
|
26
26
|
"id": "lifecycle",
|
|
27
|
-
"code": "import { batchTransport, createLogger } from '@vielzeug/rune';\n\n// Two-arg shorthand: namespace + options\nconst log = createLogger('api', { logLevel: 'debug' });\n\nlog.info('logger created');\nlog.debug({ url: '/health' }, 'request start');\n\n// disposed logger silences all subsequent calls\nlog.dispose();\nlog.info('this is silenced — no output');\n\nconsole.log('log.disposed:', log.disposed);\n\n// batchTransport idempotency — double-dispose does not double-flush\nconst flushed: string[] = [];\nconst batch = batchTransport({\n interval: 60_000,\n onFlush: (entries) => {\n flushed.push(...entries.map((e) => e.message ?? ''));\n },\n});\n\nconst batchLog = createLogger('batch', { transports: [batch.transport] });\nbatchLog.info('entry-1');\nbatchLog.warn('entry-2');\n\
|
|
27
|
+
"code": "import { batchTransport, createLogger } from '@vielzeug/rune';\n\n// Two-arg shorthand: namespace + options\nconst log = createLogger('api', { logLevel: 'debug' });\n\nlog.info('logger created');\nlog.debug({ url: '/health' }, 'request start');\n\n// disposed logger silences all subsequent calls\nlog.dispose();\nlog.info('this is silenced — no output');\n\nconsole.log('log.disposed:', log.disposed);\n\n// batchTransport idempotency — double-dispose does not double-flush\nconst flushed: string[] = [];\nconst batch = batchTransport({\n interval: 60_000,\n onFlush: (entries) => {\n flushed.push(...entries.map((e) => e.message ?? ''));\n },\n});\n\nconst batchLog = createLogger('batch', { transports: [batch.transport] });\nbatchLog.info('entry-1');\nbatchLog.warn('entry-2');\n\nawait batch.dispose(); // flushes once and waits for delivery\nawait batch.dispose(); // same settled promise — no double flush\n\nconsole.log('flushed messages:', flushed);\n",
|
|
28
28
|
"name": "Logger Lifecycle & Disposal"
|
|
29
29
|
},
|
|
30
30
|
{
|
|
@@ -56,8 +56,7 @@
|
|
|
56
56
|
"RuneOptions": "export type {\n BatchHandle,\n BatchTransportOptions,\n Bindings,\n JsonTransportOptions,\n LogEntry,\n LogLevel,\n LogMethod,\n LogMiddleware,\n Logger,\n LogType,\n PipeOptions,\n RedactTransportOptions,\n RemoteLogData,\n RemoteTransportOptions,\n RuneOptions,\n SampleTransportOptions,\n Transport,\n} from './types';",
|
|
57
57
|
"SampleTransportOptions": "export type {\n BatchHandle,\n BatchTransportOptions,\n Bindings,\n JsonTransportOptions,\n LogEntry,\n LogLevel,\n LogMethod,\n LogMiddleware,\n Logger,\n LogType,\n PipeOptions,\n RedactTransportOptions,\n RemoteLogData,\n RemoteTransportOptions,\n RuneOptions,\n SampleTransportOptions,\n Transport,\n} from './types';",
|
|
58
58
|
"Transport": "export type {\n BatchHandle,\n BatchTransportOptions,\n Bindings,\n JsonTransportOptions,\n LogEntry,\n LogLevel,\n LogMethod,\n LogMiddleware,\n Logger,\n LogType,\n PipeOptions,\n RedactTransportOptions,\n RemoteLogData,\n RemoteTransportOptions,\n RuneOptions,\n SampleTransportOptions,\n Transport,\n} from './types';",
|
|
59
|
-
"RuneError": "export { RuneError
|
|
60
|
-
"RuneTransportError": "export { RuneError, RuneTransportError } from './errors';",
|
|
59
|
+
"RuneError": "export { RuneError } from './errors';",
|
|
61
60
|
"isLevelEnabled": "export { isLevelEnabled, PRIORITY } from './types';",
|
|
62
61
|
"PRIORITY": "export { isLevelEnabled, PRIORITY } from './types';",
|
|
63
62
|
"LazyBinding": "export type { LazyBinding } from './lazy';",
|