Rune 3 Changes
Rune 3 removes shared singleton state and the redundant pipe() fan-out helper. Production delivery primitives and middleware remain first-class, while log methods now support a preferred message-first form alongside structured and Error-first calls.
Removed APIs:
defaultLogger— create logger instances explicitly withcreateLogger()pipe()andPipeOptions— pass multiple transports directly tocreateLogger({ transports })RuneTransportError— transport and middleware failures remain isolated rather than escaping to application code
Retained APIs:
remoteTransport(),batchTransport(),sampleTransport(), andredactTransport()LogMiddleware,RuneOptions.middleware,Logger.use(), andLogger.middleware- Context-first and Error-first log calls, plus the preferred message-first form
Replace defaultLogger
Create logger instances explicitly so ownership, configuration, and disposal are visible.
// Rune 2
import { defaultLogger } from '@vielzeug/rune';
const log = defaultLogger.child({ namespace: 'app' });// Rune 3
import { createLogger } from '@vielzeug/rune';
const log = createLogger({ namespace: 'app' });Replace pipe()
Pass transports directly to the logger. Dispatch already isolates synchronous failures so one transport cannot block its siblings.
// Rune 2
const log = createLogger({
transports: [pipe(consoleTransport(), jsonTransport())],
});// Rune 3
const log = createLogger({
transports: [consoleTransport(), jsonTransport()],
});Prefer Message-First Logging
Message-first calls are easiest to scan in application code. Context-first remains available for structured events and callback adapters; Error-first remains available for forwarding failures without manual wrapping.
log.info('request started', { requestId });
log.debug({ type: 'dispatch', payload }, 'bus:dispatch');
log.error(err, { requestId }, 'request failed');Keep Cross-Transport Middleware
Middleware still transforms or filters an entry once before every transport. use() returns a new logger and does not mutate its parent.
const log = createLogger({
middleware: [(entry) => ({ ...entry, data: { ...entry.data, env: 'production' } })],
transports: [consoleTransport(), jsonTransport()],
});
const errorsOnly = log.use((entry) => (entry.level === 'error' || entry.level === 'fatal' ? entry : null));Update Depth-Limited Redaction
Rune 2 stopped inspecting values beyond maxDepth and forwarded those subtrees unchanged. Rune 3 fails closed by replacing the entire deeper subtree.
const safe = redactTransport({
keys: ['password', 'token'],
maxDepth: 10,
transport: remoteTransport({
handler: (_level, data) => fetch('/api/logs', { body: JSON.stringify(data), method: 'POST' }),
onError: (error) => console.error('log delivery failed', error),
}),
});If downstream consumers relied on deeply nested non-sensitive values, raise maxDepth deliberately or flatten the logged context. Do not disable the limit for untrusted object graphs.
Preserve Production Delivery
Remote delivery, batching, and sampling remain composable transport factories. Observe asynchronous failures and dispose batch handles during shutdown.
const remote = remoteTransport({
handler: (_level, data) => fetch('/api/logs', { body: JSON.stringify(data), method: 'POST' }),
onError: (error) => console.error('log delivery failed', error),
});
const batch = batchTransport({
onFlush: (entries) => fetch('/api/log-batches', { body: JSON.stringify(entries), method: 'POST' }),
});
const log = createLogger({ transports: [sampleTransport({ rate: 0.25, transport: remote }), batch.transport] });
await batch.dispose();