Skip to content

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 with createLogger()
  • pipe() and PipeOptions — pass multiple transports directly to createLogger({ transports })
  • RuneTransportError — transport and middleware failures remain isolated rather than escaping to application code

Retained APIs:

  • remoteTransport(), batchTransport(), sampleTransport(), and redactTransport()
  • LogMiddleware, RuneOptions.middleware, Logger.use(), and Logger.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.

ts
// Rune 2
import { defaultLogger } from '@vielzeug/rune';
const log = defaultLogger.child({ namespace: 'app' });
ts
// 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.

ts
// Rune 2
const log = createLogger({
  transports: [pipe(consoleTransport(), jsonTransport())],
});
ts
// 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.

ts
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.

ts
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.

ts
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.

ts
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();