Why Rune?
Plain console.log lacks structure: no log levels, no namespacing, no remote delivery, no way to silence logs in production.
ts
// Before — manual approach
if (process.env.NODE_ENV !== 'production') {
console.log('[api] GET /users', data);
}
fetch('/api/logs', { method: 'POST', body: JSON.stringify({ level: 'error', msg }) });
// After — Rune
import { createLogger } from '@vielzeug/rune';
import { consoleTransport, pipe, remoteTransport } from '@vielzeug/rune';
const api = createLogger({
namespace: 'api',
transports: [
pipe(consoleTransport({ level: 'debug' }), remoteTransport({ handler: sendToCollector, level: 'error' })),
],
});
api.info({ data }, 'GET /users');| Feature | Rune | Winston | Pino | console |
|---|---|---|---|---|
| Bundle size | 3.4 KB | ~44 kB | ~4 kB | 0 kB |
| Browser support | ||||
| Scoped loggers | Manual | Child | ||
| Pluggable transports | ||||
| Structured log entry | LogEntry type | Partial | ||
| Lazy bindings | lazy(fn) | |||
| Styled output | Text only | Text only | Manual | |
| Zero dependencies | N/A |
Use Rune when you need isomorphic logging (browser + Node.js), namespaced module loggers, or remote error delivery without a heavy dependency chain.
Consider alternatives when you need high-throughput file-based logging (Pino), file rotation (Winston), or your team already uses a logging framework.
Installation
sh
pnpm add @vielzeug/runesh
npm install @vielzeug/runesh
yarn add @vielzeug/runeQuick Start
ts
import { Rune, createLogger, lazy } from '@vielzeug/rune';
import { consoleTransport, pipe, remoteTransport, jsonTransport } from '@vielzeug/rune';
// Default logger — uses consoleTransport() automatically
Rune.info('Boot complete');
Rune.warn('Cache stale');
Rune.fatal({ err: new Error('unrecoverable') }, 'startup failed');
// Namespaced child loggers
const api = createLogger('api');
api.info({ method: 'GET', path: '/users' }, 'request');
// Pinned bindings — lazy() only evaluates when the level passes
const reqLog = api.withBindings({
requestId: 'abc-123',
diagnostics: lazy(() => buildDiagnostics()),
});
reqLog.debug('processing'); // diagnostics() called only here
// Structured timing — label is the message; emits { duration_ms } in context
await reqLog.time('db.query', () => runQuery());
// Custom transport pipeline
const serverLog = createLogger({
logLevel: 'info',
namespace: 'server',
transports: [
consoleTransport({ timestamp: true }),
remoteTransport({
handler: async (type, data) => {
await fetch('/api/logs', { body: JSON.stringify(data), method: 'POST' });
},
level: 'error',
}),
],
});
// Node.js: structured JSON for log aggregation
const nodeLog = createLogger({
transports: [jsonTransport({ level: 'warn' })],
});Features
- Level filtering (
debugtooff) withenabled()checks, includingfatalaboveerror - Immutable config after construction — use
child()orwithBindings()to scope - Three call forms:
log.info('msg'),log.error(err, { id }, 'msg')(Error-first), orlog.info({ key: 'val' }, 'msg')— Error-first form auto-serializes todata.err Errorvalues in context fields are also auto-serialized to{ message, name, stack }— survives JSON.stringify- Pinned context bindings via
withBindings({ requestId })— fields on every line - Lazy bindings via
lazy(fn)— expensive computations gated behind the level check - Namespaced child loggers via
createLogger('name')orlogger.child({ namespace }) - Middleware pipeline via
use(fn)— transform or filter entries before transport dispatch - Pluggable transport pipeline:
consoleTransport,remoteTransport,jsonTransport,batchTransport,sampleTransport,redactTransport - Fan-out via
pipe()— dispatch to multiple transports independently, fault-tolerant - Structured
time()wrapper: emits the label as message with{ duration_ms }in context group()andgroupCollapsed()wrappers that auto-close on throw/rejectLogEntry.data— single merged flat object for transports; no manual merging needed- Zero dependencies — 3.4 KB gzipped
Documentation
See Also
- Courier — HTTP client with built-in request/response interception; pipe Rune as a transport to log every API call with structured context
- Herald — typed event bus; emit log-level change or flush events across modules without coupling loggers directly
- Familiar — Web Worker pool; use Rune inside task functions to surface structured worker-side logs back to the main thread