konsole-logger
v5.1.4
Published
Structured, namespaced logging for browser and Node.js — numeric log levels, child loggers, beautiful terminal output, flexible transports
Downloads
666
Maintainers
Keywords
Readme
Console
The only structured logger that runs natively in browsers and Node.js — with zero dependencies.
Why Console?
| Feature | Console | Pino | Winston | Bunyan | Consola |
|---------|:-------:|:----:|:-------:|:------:|:-------:|
| Browser support | Native | No | No | No | Yes |
| Worker offloading | Yes | No | No | No | No |
| Bundle (gzip) | ~10 KB | ~32 KB | ~70 KB | ~45 KB | ~12 KB |
| Dependencies | 0 | 11 | 11 | 0 | 0 |
| Child loggers | Yes | Yes | Yes | Yes | Yes (withTag) |
| File rotation + gzip | Built-in | Separate | Separate | No | No |
| Field redaction | Built-in | Plugin | No | No | No |
| Configurable timestamps | 7 presets + custom | Epoch only | Basic | Basic | Basic |
| DevTools styling | CSS badges | No | No | No | No |
| TypeScript-first | Yes | Partial | Partial | No | Yes |
Features
- Browser-first, Node.js ready — worker transport (Web Worker /
worker_threads) keeps the main thread free - Six numeric log levels — trace / debug / info / warn / error / fatal
- Structured output — consistent JSON schema, compatible with Datadog, Loki, CloudWatch
- Beautiful terminal output — ANSI colors on TTY, NDJSON in pipes, styled badges in DevTools
- Configurable timestamps — full date+time by default, ISO 8601, epoch, nanosecond precision, or custom format
- Child loggers — attach request-scoped context that flows into every log line
- Async context propagation —
runWithContext()auto-bindsrequestId/traceIdthrough async scope viaAsyncLocalStorage— no child threading - Field redaction — mask sensitive data (
password,req.headers.authorization) before any output or transport - Serializers — pluggable per-field transforms with built-in
err/req/res; Errors auto-flatten to full stack/cause (no more"err":{}) - Flexible transports — HTTP, file (with rotation + gzip), stream, or console; per-transport filter and transform
- Circular buffer — memory-efficient in-process log history (browser); zero-overhead in Node.js
- Fast — on par with Pino, significantly faster than Winston and Bunyan, at 1/3 the bundle size
- TypeScript first — full type safety, zero runtime dependencies
Installation
npm install konsole-loggerAlso works with
yarn add konsole-loggerorpnpm add konsole-logger
Quick Start
Note: The exported class is named
Konsole(with a K) becauseConsoleis a reserved global in JavaScript.
import { Konsole } from 'konsole-logger';
const logger = new Konsole({ namespace: 'MyApp' });
logger.info('Server started', { port: 3000 });
logger.warn('Config file missing, using defaults');
logger.error(new Error('Database connection failed'));Terminal output (TTY):
2025-03-16 10:23:45.123 INF [MyApp] Server started port=3000
2025-03-16 10:23:45.124 WRN [MyApp] Config file missing, using defaults
2025-03-16 10:23:45.125 ERR [MyApp] Database connection failedPipe / CI output (NDJSON):
{"level":30,"levelName":"info","time":"2025-03-16T10:23:45.000Z","namespace":"MyApp","msg":"Server started","port":3000}Log Levels
| Method | Level | Value |
|--------|-------|-------|
| logger.trace() | trace | 10 |
| logger.debug() | debug | 20 |
| logger.info() / logger.log() | info | 30 |
| logger.warn() | warn | 40 |
| logger.error() | error | 50 |
| logger.fatal() | fatal | 60 |
Set a minimum threshold — entries below it are discarded entirely:
const logger = new Konsole({ namespace: 'App', level: 'info' });
logger.trace('loop tick'); // dropped — below threshold
logger.debug('cache miss'); // dropped — below threshold
logger.info('ready'); // ✅ loggedChange the threshold at runtime:
logger.setLevel('debug');Calling Conventions
All four styles work and produce the same structured LogEntry:
// 1. Simple string
logger.info('Server started');
// 2. String + fields (recommended)
logger.info('Request received', { method: 'GET', path: '/users', ms: 42 });
// 3. Object-first with msg key
logger.info({ msg: 'Request received', method: 'GET', path: '/users' });
// 4. Error — message extracted, error stored in fields.err
logger.error(new Error('Connection refused'));Output Formats
The format option controls how logs are printed. 'auto' (default) picks the right one for the environment:
| Format | Description |
|--------|-------------|
| 'auto' | Browser → browser, Node.js TTY → pretty, Node.js pipe → json |
| 'pretty' | ANSI-colored human-readable output |
| 'json' | Newline-delimited JSON — aggregator-friendly |
| 'text' | Plain text, no ANSI — for CI or log files |
| 'browser' | Styled %c badges in DevTools |
| 'silent' | No output; logs still stored in the buffer and sent to transports |
const logger = new Konsole({ namespace: 'App', format: 'silent' });Timestamps
Every log line includes a full date+time timestamp by default (2025-03-16 10:23:45.123). Configure the format per-logger:
| Preset | Output |
|--------|--------|
| 'datetime' (default) | 2025-03-16 10:23:45.123 |
| 'iso' | 2025-03-16T10:23:45.123Z |
| 'time' | 10:23:45.123 |
| 'date' | 2025-03-16 |
| 'unix' | 1710583425 |
| 'unixMs' | 1710583425123 |
| 'none' | (omitted) |
| (date, hrTime?) => string | Custom function |
// ISO timestamps everywhere
const logger = new Konsole({ namespace: 'App', timestamp: 'iso' });
// High-resolution timestamps (nanosecond precision)
const logger = new Konsole({
namespace: 'App',
timestamp: { format: 'iso', highResolution: true },
});
// Change at runtime (works in browser too)
logger.setTimestamp('unixMs');
logger.setTimestamp((d) => d.toLocaleString('ja-JP'));Browser runtime control
// Via window.__Konsole (after exposeToWindow())
__Konsole.setTimestamp('iso') // all loggers
__Konsole.getLogger('Auth').setTimestamp('iso') // specific loggerChild Loggers
Create a child that automatically injects context into every entry it produces:
const logger = new Konsole({ namespace: 'API' });
// Per-request child
const req = logger.child({ requestId: 'req_abc', userId: 42 });
req.info('Request started', { path: '/users' });
// → INF [API] Request started requestId=req_abc userId=42 path=/users
// Nest further — bindings accumulate
const db = req.child({ component: 'postgres' });
db.debug('Query', { ms: 4 });
// → DBG [API] Query requestId=req_abc userId=42 component=postgres ms=4Child options:
const child = logger.child(
{ requestId: 'req_abc' },
{ namespace: 'API:handler', level: 'warn' }
);Children are ephemeral — not registered in Konsole.instances, share the parent's buffer.
Async Context Propagation (Node.js)
Bind request-scoped fields to an async scope once, and every log inside (through await, setTimeout, Promise.then, middleware chains) auto-includes them — no child-logger plumbing:
import { Konsole } from 'konsole-logger';
const logger = new Konsole({ namespace: 'API' });
// One-time init at app startup
await Konsole.enableContext();
// Express / Fastify / Hono middleware
app.use((req, _res, next) => {
Konsole.runWithContext({ requestId: req.id, userId: req.user?.id }, () => next());
});
// Anywhere downstream — no need to thread a child logger
async function chargeCustomer(amount: number) {
logger.info('charging', { amount });
// → { msg: 'charging', amount, requestId: 'r_abc', userId: 42 }
await db.charge(amount);
}Precedence (low → high): ALS context < child bindings < call-site fields. Call-site always wins; bindings override context on key collision.
Nested scopes merge:
Konsole.runWithContext({ requestId: 'r1' }, () => {
Konsole.runWithContext({ userId: 'u1' }, () => {
logger.info('both apply');
// → fields: { requestId: 'r1', userId: 'u1' }
});
});Zero overhead when unused — AsyncLocalStorage is lazy-loaded. Apps that never call enableContext() pay a single null check per log call. Browser: runWithContext invokes fn() directly; context is a no-op.
API:
| Method | Description |
|--------|-------------|
| await Konsole.enableContext() | One-time init (loads node:async_hooks). Safe to call multiple times. |
| Konsole.runWithContext(store, fn) | Run fn with store merged into every log entry inside the scope. Returns fn's result. |
| Konsole.getContext() | Read the current store, or undefined if no scope is active. |
Redaction
Automatically mask sensitive fields before they reach any output, transport, or buffer:
const logger = new Konsole({
namespace: 'API',
redact: ['password', 'user.creditCard', 'req.headers.authorization'],
});
logger.info('Login attempt', { user: 'alice', password: 'hunter2' });
// → INF [API] Login attempt user=alice password=[REDACTED]
logger.info('Request', {
req: { headers: { authorization: 'Bearer tok', host: 'example.com' } },
});
// → authorization is [REDACTED], host is untouchedRedaction uses dot-notation for nested paths. Values are replaced with '[REDACTED]' before reaching the buffer, formatter, or any transport — nothing leaks.
Child logger inheritance
Children always inherit their parent's redact paths and can add more. A child can never redact fewer fields than its parent:
const parent = new Konsole({ namespace: 'App', redact: ['password'] });
const child = parent.child({ service: 'auth' }, { redact: ['token'] });
child.info('event', { password: 'secret', token: 'abc' });
// → both password and token are [REDACTED]
parent.info('event', { password: 'secret', token: 'abc' });
// → only password is [REDACTED] — parent is unaffected by child pathsDisable redaction at runtime (browser only)
For debugging in DevTools, you can temporarily disable redaction to see the real values. This toggle is only available in the browser via window.__Konsole — it cannot be disabled in Node.js:
// In DevTools console (after Konsole.exposeToWindow()):
__Konsole.disableRedaction(true) // show real values
__Konsole.disableRedaction(false) // restore redactionAdvanced: using redaction utilities directly
The redaction functions are exported for use in custom transports:
import { compileRedactPaths, applyRedaction, REDACTED } from 'konsole-logger';
const paths = compileRedactPaths(['password', 'req.headers.authorization']);
const redactedEntry = applyRedaction(entry, paths);Serializers
Serializers transform structured field values before any output, transport, or
buffer write. They fix the most common logging foot-gun — JSON.stringify(err)
returning "{}" — and let you reshape noisy objects (HTTP req/res, ORM models,
domain entities) into something compact and useful.
import { Konsole, stdSerializers } from 'konsole-logger';
const logger = new Konsole({
namespace: 'App',
serializers: stdSerializers, // err / req / res
});
logger.error('db failure', { err: new Error('timeout') });
// JSON: { ..., "err": { "type": "Error", "message": "timeout", "stack": "..." } }Built-in stdSerializers
| Key | Handles | Output |
|-----|---------|--------|
| err | Error instances (with cause chains, custom props) | { type, message, stack, ...customProps, cause? } |
| req | Node http.IncomingMessage, Express req, Fetch Request | { method, url, headers, remoteAddress, remotePort } |
| res | Node http.ServerResponse, Express res | { statusCode, headers } |
Auto Error flattening — even without configuring serializers, any field
containing an Error is auto-expanded so it never serializes to "{}":
const logger = new Konsole({ namespace: 'App' });
logger.error('failed', { err: new Error('boom') }); // err.stack survivesCustom serializers
new Konsole({
namespace: 'App',
serializers: {
...stdSerializers,
user: (u: any) => ({ id: u.id, role: u.role }), // strip PII
},
});Child inheritance — children inherit parent serializers and can override per
key. The child below ships only user.name; the parent still ships user.id:
const parent = new Konsole({
namespace: 'App',
serializers: { user: (u: any) => ({ id: u.id }) },
});
const child = parent.child({}, {
serializers: { user: (u: any) => ({ name: u.name }) },
});Safety guarantees — serialization is cycle-safe and JSON-safe by construction:
- Path-scoped cycle detection —
err.self = err, mutualcausechains, and shared sub-graphs all serialize without throwing. - Repeated non-cyclic references across sibling branches are preserved as full
copies, not collapsed to
[Circular]. toJSON-aware —URL,Buffer,Date, Decimal, Moment, etc. round-trip through their canonical form instead of becoming{}.- Own
__proto__keys (e.g. fromJSON.parse('{"__proto__":...}')) are preserved as data properties without mutating any prototype. - Fetch
Headers/ Map-like header containers are flattened by interface so redaction paths likereq.headers.authorizationactually see the value.
Transports
Ship logs to external destinations alongside (or instead of) console output:
HTTP
const logger = new Konsole({
namespace: 'App',
transports: [
{
name: 'datadog',
url: 'https://http-intake.logs.datadoghq.com/v1/input',
headers: { 'DD-API-KEY': process.env.DD_API_KEY },
batchSize: 50,
flushInterval: 10000,
filter: (entry) => entry.levelValue >= 40, // warn+ only
},
],
});File (Node.js)
import { Konsole, FileTransport } from 'konsole-logger';
const logger = new Konsole({
namespace: 'App',
transports: [
new FileTransport({ path: '/var/log/app.log' }),
],
});With rotation:
new FileTransport({
path: '/var/log/app.log',
rotation: {
maxSize: 10 * 1024 * 1024, // rotate at 10 MB
interval: 'daily', // also rotate daily
maxFiles: 7, // keep 7 rotated files
compress: true, // gzip old files (.log.1.gz)
},
});Stream
import { StreamTransport } from 'konsole-logger';
const logger = new Konsole({
namespace: 'App',
transports: [new StreamTransport({ stream: process.stdout, format: 'json' })],
});Add at runtime
logger.addTransport(new FileTransport({ path: './debug.log' }));Graceful shutdown (Node.js)
Flush all transports before the process exits — no logs lost in Lambda, K8s, or containers:
// Option 1: automatic — registers SIGTERM, SIGINT, and beforeExit handlers
Konsole.enableShutdownHook();
// Option 2: manual
process.on('SIGTERM', async () => {
await Konsole.shutdown();
process.exit(0);
});Configuration
new Konsole({
namespace?: string; // default: 'Global' — logger identifier
level?: LogLevelName; // default: 'trace' — minimum level threshold
format?: KonsoleFormat; // default: 'auto' — output format (pretty/json/text/browser/silent)
timestamp?: TimestampFormat | TimestampOptions; // default: 'datetime'
redact?: string[]; // dot-notation field paths to mask with '[REDACTED]'
serializers?: Record<string, (value: unknown) => unknown>; // per-field transforms (use `stdSerializers` for err/req/res)
transports?: (Transport | TransportConfig)[]; // external log destinations
maxLogs?: number; // default: 10000 — circular buffer capacity
defaultBatchSize?: number; // default: 100 — entries per viewLogs() call
retentionPeriod?: number; // default: 172800000 — 48h auto-cleanup
cleanupInterval?: number; // default: 3600000 (1 hour)
useWorker?: boolean; // default: false
})API Reference
Instance methods
| Method | Description |
|--------|-------------|
| trace / debug / info / log / warn / error / fatal | Log at the given level |
| child(bindings, options?) | Create a child logger with merged bindings |
| setLevel(level) | Change minimum level at runtime |
| setTimestamp(format) | Change timestamp format at runtime |
| getLogs() | Return all entries from the circular buffer |
| getLogsAsync() | Async variant (for worker mode) |
| clearLogs() | Empty the buffer |
| viewLogs(batchSize?) | Print a batch of stored logs to the console |
| getStats() | { logCount, capacity } |
| addTransport(transport) | Attach a transport at runtime |
| flushTransports() | Flush all pending batches |
| destroy() | Flush, stop timers, deregister |
Static methods
| Method | Description |
|--------|-------------|
| Konsole.getLogger(namespace) | Retrieve a registered logger |
| Konsole.getNamespaces() | List all registered namespaces |
| Konsole.exposeToWindow() | Expose __Konsole on window for browser debugging |
| Konsole.enableGlobalPrint(enabled) | Override output for all loggers |
| Konsole.addGlobalTransport(transport) | Add a transport to all existing loggers |
| Konsole.shutdown() | Flush and destroy all registered loggers |
| Konsole.enableShutdownHook() | Register SIGTERM/SIGINT/beforeExit handlers (Node.js only) |
| Konsole.enableContext() | Initialize AsyncLocalStorage for context propagation (Node.js only) |
| Konsole.runWithContext(store, fn) | Run fn in an async scope whose fields are merged into every log entry |
| Konsole.getContext() | Read the current async context store |
Browser Debugging
// In app init:
Konsole.exposeToWindow();
// Then in DevTools console:
__Konsole.getLogger('Auth').viewLogs()
__Konsole.enableGlobalPrint(true) // unsilence all loggers
__Konsole.disableRedaction(true) // show real values (debug only)
__Konsole.setTimestamp('iso') // switch all loggers to ISO timestamps
__Konsole.getLogger('Auth').setTimestamp('time') // per-logger overridePerformance
Console is designed for minimal overhead. Unlike Pino, Winston, and Bunyan (Node.js only), Console works natively in the browser and Node.js with worker-thread offloading for non-blocking transport processing.
Benchmarked on Apple M5 Max, Node.js v24.15 (100K iterations).
What matters in production: structured JSON throughput
For most apps the only number that matters is "how fast can the logger actually emit a structured log line." Silent / disabled benchmarks (further down) measure call-site overhead, but you don't ship loggers silenced — you ship them writing JSON to stdout, a file, or a stream.
| Logger | ops/sec | p50 | p95 | p99 | |---|---:|---:|---:|---:| | Console (JSON → /dev/null) | 4.16M | 125 ns | 167 ns | 958 ns | | Consola (JSON → /dev/null) | 795.9K | 1.13 µs | 1.37 µs | 2.17 µs | | Bunyan (child → /dev/null) | 752.0K | 1.08 µs | 1.38 µs | 2.25 µs | | Bunyan (JSON → /dev/null) | 741.8K | 1.25 µs | 1.46 µs | 2.33 µs | | Winston (JSON → /dev/null) | 672.9K | 917 ns | 1.75 µs | 2.17 µs | | Pino (JSON → /dev/null) | 560.5K | 1.63 µs | 2.67 µs | 3.38 µs |
Console emits structured JSON ~5× faster than Consola, Bunyan, and Winston, and ~7× faster than Pino. p50 latency is roughly an order of magnitude lower than every competitor.
Microbenchmark: disabled / silent overhead
This measures how cheap a filtered-out log call is — i.e. what your code pays for logger.debug(…) in production when the level is set above debug.
| Logger | Mode | ops/sec | |---|---|---:| | Pino | child, disabled | 34.02M | | Console | child, no buffer | 32.86M | | Consola | tagged child, silent | 22.60M | | Consola | silent | 15.24M | | Pino | disabled | 13.57M | | Console | silent, no buffer | 13.45M | | Winston | silent | 2.98M | | Winston | child, silent | 2.12M |
Console, Pino, and Consola sit in the same fast-path tier (within run-to-run V8 noise). Winston is a tier below.
Bundle / install size
| | Console | Pino | Winston | Bunyan | Consola | |---|---:|---:|---:|---:|---:| | Bundle (gzip) | ~10 KB | ~32 KB | ~70 KB | ~45 KB | ~12 KB | | Install size | 135 KB | 1.17 MB | 360 KB | 212 KB | 420 KB | | Dependencies | 0 | 11 | 11 | 0 | 0 | | Browser support | Native + Worker | No | No | No | Native |
Run
npm run benchmarkto reproduce on your hardware. Install competitors withnpm install --no-save pino winston bunyan consola.
Worker Performance
With useWorker: true, log storage and HTTP transport batching run on a background worker (Web Worker in browsers, worker_threads in Node.js) — the main thread never blocks on logging:
const logger = new Konsole({
namespace: 'App',
useWorker: true,
transports: [{
name: 'backend',
url: '/api/logs',
batchSize: 50,
flushInterval: 10000,
}],
});
// Logging never blocks rendering / event loop — processed in background
logger.info('User action', { event: 'click', target: 'checkout' });No other structured logging library offers cross-platform worker offloading.
CDN / Script Tag
Console ships a UMD build — use it directly in the browser without a bundler:
<script src="https://unpkg.com/konsole-logger/dist/konsole.umd.cjs"></script>
<script>
const logger = new Konsole.Konsole({ namespace: 'App' });
logger.info('Hello from the browser!');
</script>Coming from Pino?
Console uses a Pino-compatible JSON schema. The calling conventions are similar but not identical:
// Pino // Konsole
const logger = pino() const logger = new Konsole({ namespace: 'App' })
logger.info({ userId: 1 }, 'msg') logger.info('msg', { userId: 1 })
logger.info({ msg: 'hi', userId: 1 }) logger.info({ msg: 'hi', userId: 1 })
logger.child({ reqId: 'abc' }) logger.child({ reqId: 'abc' })Note: Pino puts the object first (
obj, 'msg'). Console puts the string first ('msg', obj) or uses{ msg, ...fields }object syntax. Both produce the same JSON output.
Key differences: built-in browser support, built-in redaction, built-in file rotation, built-in async context propagation (no pino-http / cls-hooked needed), zero dependencies, and ~10 KB gzipped vs Pino's ~32 KB.
Requirements
- Node.js >= 18 for server-side use (native
fetch). Older versions must passfetchImpltoTransportConfig.
License
MIT © Sakti Kumar Chourasia
