npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

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

Readme

Console

The only structured logger that runs natively in browsers and Node.js — with zero dependencies.

npm version License: MIT Bundle Size TypeScript Zero Dependencies

Docs & Live Demo | Changelog


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 propagationrunWithContext() auto-binds requestId/traceId through async scope via AsyncLocalStorage — 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-logger

Also works with yarn add konsole-logger or pnpm add konsole-logger

Quick Start

Note: The exported class is named Konsole (with a K) because Console is 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 failed

Pipe / 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');        // ✅ logged

Change 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 logger

Child 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=4

Child 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 unusedAsyncLocalStorage 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 untouched

Redaction 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 paths

Disable 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 redaction

Advanced: 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 survives

Custom 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, mutual cause chains, 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. from JSON.parse('{"__proto__":...}')) are preserved as data properties without mutating any prototype.
  • Fetch Headers / Map-like header containers are flattened by interface so redaction paths like req.headers.authorization actually 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 override

Performance

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 benchmark to reproduce on your hardware. Install competitors with npm 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 pass fetchImpl to TransportConfig.

License

MIT © Sakti Kumar Chourasia


Report Bug | Request Feature | Docs