Version v3.0.0 Size 3.6 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}`);
// After — Rune
import { consoleTransport, createLogger, jsonTransport } from '@vielzeug/rune';
const api = createLogger({
namespace: 'api',
transports: [
consoleTransport({ level: 'debug' }),
jsonTransport({ level: 'error' }),
],
});
api.info('request', { method: 'GET', path });| Feature | Rune | Winston | Pino | console |
|---|---|---|---|---|
| Bundle size | 3.6 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 { consoleTransport, createLogger, jsonTransport, lazy } from '@vielzeug/rune';
const log = createLogger({
logLevel: 'debug',
namespace: 'server',
transports: [
consoleTransport({ timestamp: true }),
jsonTransport({ level: 'error' }),
],
});
const requestLog = log.withBindings({
diagnostics: lazy(() => ({ queueDepth: 0 })),
requestId: 'abc-123',
});
requestLog.info('request', { method: 'GET', path: '/users' });
const users = await requestLog.time('load users', () => Promise.resolve(['user-1']));
console.log(users);Features
- Level filtering (
debugtooff) withenabled()checks, includingfatalaboveerror - Immutable config after construction — use
child()orwithBindings()to scope - Message-first, context-first, and Error-first log calls for application code and adapters
Errorvalues are 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 }) - Pluggable transport pipeline: console, JSON, remote, batching, sampling, and fail-closed redaction
- Immutable middleware transforms and filters applied before every transport
- 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.6 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