Version v2.0.0 Size 3.5 KB gzip Dependencies Zero dependencies
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
const path = '/users';
console.log(`[api] GET ${path}`);
fetch('/api/logs', { body: JSON.stringify({ level: 'error', path }), method: 'POST' });
// After — Rune
import { consoleTransport, createLogger, remoteTransport } from '@vielzeug/rune';
const api = createLogger({
namespace: 'api',
transports: [
consoleTransport({ level: 'debug' }),
remoteTransport({
handler: (_type, data) => console.debug('remote log', data),
level: 'error',
}),
],
});
api.info({ method: 'GET', path }, 'request');| Feature | Rune | Winston | Pino | console |
|---|---|---|---|---|
| Bundle size | 3.5 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 { batchTransport, consoleTransport, createLogger, lazy, remoteTransport } from '@vielzeug/rune';
const log = createLogger({
logLevel: 'debug',
namespace: 'server',
transports: [
consoleTransport({ timestamp: true }),
remoteTransport({
handler: (_type, data) => console.debug('remote log', data),
level: 'error',
}),
],
});
const requestLog = log.withBindings({
diagnostics: lazy(() => ({ queueDepth: 0 })),
requestId: 'abc-123',
});
requestLog.info({ method: 'GET', path: '/users' }, 'request');
const users = await requestLog.time('load users', () => Promise.resolve(['user-1']));
console.log(users);
const batch = batchTransport({ onFlush: (entries) => console.debug('batch', entries) });
const bufferedLog = createLogger({ transports: [batch.transport] });
bufferedLog.info('queued for delivery');
await batch.dispose();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.5 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