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

nexlogger

v0.1.0

Published

Zero-dep Node.js logger — library-safe silent mode, auto env-aware output, ALS correlation, OTel injection

Readme

nexlogger

Zero-dependency, structured Node.js logger with library-safe silent mode, built-in redaction, async context correlation, and OpenTelemetry injection.

Requires Node.js >= 18.

npm install nexlogger

Quick Start

App Mode

import { createLogger } from 'nexlogger'

const log = createLogger({ name: 'my-app' })

log.info('server started')
log.info({ userId: 42 }, 'user login')
log.warn('disk usage high', { percent: 92 })
log.error(new Error('connection lost'))

Output auto-detects environment: pretty with colors in development, JSON in production.

Library Mode

import { getLogger } from 'nexlogger'

const log = getLogger('my-lib')

export function doWork() {
  log.info('processing') // silent until host app calls configure()
}

Libraries ship with nexlogger but produce zero output until the consuming application opts in. No forced logging, no dependency conflicts.

Activating Library Loggers

import { configure } from 'nexlogger'

configure({
  loggers: [
    { name: 'my-lib', level: 'info' },
    { name: 'my-lib:*' },  // glob: activates my-lib:db, my-lib:cache, etc.
  ],
})

Log Levels

Six levels, ordered by severity:

| Level | Weight | Use | |-------|--------|-----| | trace | 10 | Fine-grained debugging | | debug | 20 | Development diagnostics | | info | 30 | Normal operations (default) | | warn | 40 | Recoverable issues | | error | 50 | Failures | | fatal | 60 | Unrecoverable, process should exit |

Set via option or LOG_LEVEL environment variable:

const log = createLogger({ level: 'debug' })
log.setLevel('warn') // change at runtime

Flexible Call Signatures

Every log method accepts multiple argument styles:

log.info('simple message')
log.info('user %s logged in from %s', name, ip)  // printf-style
log.info({ userId: 42 }, 'user login')            // pino-style: object first
log.info('user login', { userId: 42 })            // object second
log.info(new Error('boom'))                       // error object (uses err.message)
log.info(new Error('boom'), 'custom message')     // error + override message

Errors are serialized into meta.err with all own properties preserved (message, stack, name, code, cause, syscall, etc.).


Child Loggers

Create child loggers with bound metadata that merges into every log entry:

const log = createLogger({ name: 'app' })
const dbLog = log.child({ module: 'database' })
const queryLog = dbLog.child({ pool: 'primary' })

queryLog.info('query executed', { duration: 12 })
// output includes: module: 'database', pool: 'primary', duration: 12

Transports

Built-in Transports

JSON (default in production) - one JSON object per line to stdout:

import { jsonTransport } from 'nexlogger'
const log = createLogger({ transports: [jsonTransport()] })

Pretty (default in development) - color-coded, human-readable to stdout:

import { prettyTransport } from 'nexlogger'
const log = createLogger({ transports: [prettyTransport({ maxMetaLength: 300 })] })

Falls back to JSON automatically when stdout is not a TTY (e.g., piped to a file).

File - synchronous append to a file in JSONL format:

import { fileTransport } from 'nexlogger'
const log = createLogger({ transports: [fileTransport('logs/app.log')], file: false })

Auto-creates directories. Default path: logs/app.log.

Async File (default file transport) - buffered writes using streams for higher throughput:

import { asyncFileTransport } from 'nexlogger'

const transport = asyncFileTransport({
  path: 'logs/app.log',
  flushIntervalMs: 100,  // flush buffer every 100ms (default)
  bufferSize: 1000,       // flush when buffer hits 1000 entries (default)
})

const log = createLogger({ transports: [transport], file: false })

// Graceful shutdown:
await transport.close()

createLogger uses asyncFileTransport() by default (path: logs/app.log) unless file: false.

Rotating File - automatic log rotation by size and/or time:

import { rotatingFileTransport } from 'nexlogger'

const log = createLogger({
  transports: [
    rotatingFileTransport({
      path: 'logs/app.log',
      maxSize: 10 * 1024 * 1024,  // rotate at 10MB (default)
      maxFiles: 5,                 // keep 5 rotated files (default)
      rotateEvery: 'daily',        // also rotate daily (optional: 'daily' | 'hourly')
    }),
  ],
  file: false,
})

Old files are named app.<timestamp>.log and cleaned up automatically.

Per-Transport Level Filtering

Send different levels to different destinations:

const log = createLogger({
  transports: [
    prettyTransport(),                                    // all levels
    { transport: fileTransport('errors.log'), level: 'error' },  // errors only
  ],
  file: false,
})

Custom Transports

A transport is any function (entry: LogEntry) => void | Promise<void>:

import type { LogEntry } from 'nexlogger'

const myTransport = (entry: LogEntry) => {
  fetch('https://logs.example.com/ingest', {
    method: 'POST',
    body: JSON.stringify(entry),
  })
}

const log = createLogger({ transports: [myTransport] })

Async transports are fire-and-forget. Use logger.flush() to await all pending writes.


Transport Error Handling

By default, transport errors are silently swallowed to prevent logging from crashing your app. Use onError to observe failures:

const log = createLogger({
  transports: [unreliableTransport],
  onError: (err, entry) => {
    console.error('Transport failed:', err, 'for entry:', entry.msg)
  },
})

Works for both synchronous throws and async rejections.


Redaction

Scrub sensitive data before it reaches any transport.

Path-based - redact specific nested keys:

const log = createLogger({
  redact: ['user.password', 'headers.authorization'],
})

log.info({ user: { name: 'Alice', password: 's3cret' } }, 'login')
// password → [REDACTED]

Wildcard arrays - redact across array items:

const log = createLogger({
  redact: ['users[*].token'],
})

Regex patterns - match and replace values by pattern:

const log = createLogger({
  redact: [
    { pattern: /\b\d{16}\b/g, replace: '[CARD]' },
    { pattern: /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/g, replace: '[EMAIL]' },
  ],
})

Fast mode - auto-detect common PII keys (password, token, secret, apikey, api_key, authorization, ssn, creditcard, credit_card):

const log = createLogger({ redact: 'fast' })

Redaction is non-mutating: input objects are never modified.


Request Correlation (AsyncLocalStorage)

Automatically attach context (like request IDs) to every log entry across async boundaries:

import { withContext, createLogger } from 'nexlogger'

const log = createLogger()

app.use((req, res, next) => {
  withContext({ requestId: req.id, userId: req.user?.id }, () => next())
})

// Anywhere downstream, even in deeply nested async calls:
log.info('processing payment')
// output: { ..., requestId: 'abc-123', userId: 42 }

Contexts nest. Inner calls merge with outer context:

withContext({ requestId: 'abc' }, () => {
  withContext({ step: 'validate' }, () => {
    log.info('check') // has both requestId and step
  })
})

Use enterContext when you can't wrap a callback (e.g., Fastify hooks):

import { enterContext } from 'nexlogger'

enterContext({ requestId: 'abc-123' })
// All subsequent logs in this async context carry requestId

Express and Fastify Middleware

Built-in middleware auto-generates a request ID and propagates it via AsyncLocalStorage:

import { expressMiddleware } from 'nexlogger'
app.use(expressMiddleware())
// Reads X-Request-ID header or generates a UUID
// Sets X-Request-ID on the response
// All logs within the request carry the requestId
import { fastifyPlugin } from 'nexlogger'
fastify.register(fastifyPlugin)

HTTP Access Logs

Full morgan-style access logging with CLF, combined, and JSON formats:

import { createLogger, accessLogMiddleware } from 'nexlogger'

const log = createLogger({ name: 'access' })

// JSON format (default) - structured with method, url, status, duration:
app.use(accessLogMiddleware({ logger: log }))

// Common Log Format:
app.use(accessLogMiddleware({ logger: log, format: 'clf' }))

// Combined format (CLF + referrer + user-agent):
app.use(accessLogMiddleware({ logger: log, format: 'combined' }))

// Skip health checks:
app.use(accessLogMiddleware({
  logger: log,
  skip: (req) => req.url === '/health',
}))

JSON format output includes method, url, status, duration (ms), contentLength, userAgent, and ip.


OpenTelemetry Trace Injection

Automatically injects traceId, spanId, and traceFlags from the active OpenTelemetry span into every log entry:

const log = createLogger({ otel: 'auto' })  // default: auto-detect

| Value | Behavior | |-------|----------| | 'auto' (default) | Inject if @opentelemetry/api is installed, skip silently if not | | true | Inject, throw if @opentelemetry/api is not installed | | false | Never inject |

Install @opentelemetry/api as a peer dependency to enable:

npm install @opentelemetry/api

Source File and Line Tracking

Opt-in capture of the file and line number that produced each log entry:

const log = createLogger({ source: true })
log.info('hello')
// output: { ..., file: 'src/handlers/user.ts', line: 42 }

Configure stack depth:

const log = createLogger({ source: { enabled: true, depth: 15 } })

Internal framework frames are automatically skipped.


Log Sampling

Reduce log volume for high-frequency levels while keeping every warn/error/fatal:

const log = createLogger({
  sample: {
    trace: 0.01,  // log 1% of trace entries
    debug: 0.1,   // log 10% of debug entries
    info: 0.5,    // log 50% of info entries
  },
})

When a requestId is present in context, sampling is deterministic per request: all logs for the same request either appear or don't, ensuring consistent traces. Falls back to random sampling without context.

warn, error, and fatal are never sampled.


debug Package Compatibility

Drop-in migration path from the debug package:

// Before:
// import debug from 'debug'
// const log = debug('myapp:server')

// After:
import { debug } from 'nexlogger'
const log = debug('myapp:server')

log.info('listening on port 3000')

Respects the DEBUG environment variable:

DEBUG=myapp:* node server.js

Programmatic control:

import { debug } from 'nexlogger'

debug.enable('myapp:*,-myapp:verbose')
debug.disable()

Unlike debug, you get structured output, levels, redaction, and all other nexlogger features.


Async Flush and Graceful Shutdown

Await all pending async transport writes:

await log.flush()

createLogger automatically registers shutdown hooks on SIGTERM and beforeExit that flush pending writes with a 500ms timeout, so logs are not lost on process exit.


Auto-Detection Behavior

createLogger() with no transport configuration auto-selects output:

| Condition | Transport | |-----------|-----------| | NODE_ENV=production | jsonTransport() to stdout | | NODE_ENV != production | prettyTransport() to stdout | | stdout is not a TTY | Pretty falls back to JSON | | file option not false | asyncFileTransport() added (default: logs/app.log) |


Test Logger

Capture log entries in tests without any output:

import { createTestLogger } from 'nexlogger'

const { logger, entries, clear } = createTestLogger({ level: 'debug' })

logger.info('user created', { userId: 42 })

expect(entries).toHaveLength(1)
expect(entries[0].msg).toBe('user created')
expect(entries[0].meta.userId).toBe(42)

clear() // reset captured entries

createTestLogger accepts all logger options except transports, file, and silent.


Level Check and Lazy Evaluation

Check if a level is enabled before performing expensive operations:

if (log.isLevelEnabled('debug')) {
  log.debug('state', { snapshot: expensiveSerialize(state) })
}

Or use lazy evaluation — pass a function that only runs when the level is enabled:

log.debug(() => 'computed: ' + expensiveCompute())
log.debug(() => ['message', { data: expensiveSerialize() }])

The function is never called if the level is disabled, avoiding wasted computation.


Batch Transport

Buffer entries and send in batches — ideal for network transports (Datadog, Loki, CloudWatch):

import { batchTransport } from 'nexlogger'

const batch = batchTransport(
  async (entries) => {
    await fetch('https://logs.example.com/ingest', {
      method: 'POST',
      body: JSON.stringify(entries),
    })
  },
  {
    size: 100,        // flush when buffer hits 100 entries (default)
    intervalMs: 1000, // flush every 1s (default)
  },
)

const log = createLogger({ transports: [batch.transport], file: false })

// Graceful shutdown:
await batch.flush() // flush pending
await batch.close() // flush + stop timer

Typed Metadata

TypeScript generics enforce metadata shape at compile time:

import { createLogger } from 'nexlogger'

interface AppMeta {
  userId: number
  requestId: string
}

const log = createLogger<AppMeta>({ name: 'app' })

log.info('login', { userId: 42, requestId: 'abc' })  // ✓ compiles
log.info('login', { userId: 'wrong' })                // ✗ type error

const dbLog = log.child({ module: 'database' })
// dbLog has type LoggerInstance<AppMeta & { module: string }>

Default generic is Record<string, unknown> — fully backward compatible.


Worker Thread Transport

Forward logs from worker threads to the primary thread's transports via MessagePort:

// primary.ts
import { Worker } from 'worker_threads'
import { receiveWorkerLogs, jsonTransport } from 'nexlogger'

const worker = new Worker('./worker.js')
const cleanup = receiveWorkerLogs(worker, [jsonTransport()])

// worker.ts
import { parentPort } from 'worker_threads'
import { createWorkerLogger } from 'nexlogger'

const log = createWorkerLogger(parentPort!)
log.info('processing in worker')

workerTransport(port) is also available if you need to compose with other transports.


OTel Log Records

Emit logs as native OpenTelemetry LogRecords via @opentelemetry/api-logs:

import { otelLogTransport, createLogger } from 'nexlogger'

const log = createLogger({
  transports: [otelLogTransport()],
  file: false,
})

log.info('request handled', { duration: 42 })
// Emitted as OTel LogRecord with severity, timestamp, attributes

Requires @opentelemetry/api-logs as a peer dependency:

npm install @opentelemetry/api-logs

Log levels map to OTel severity numbers (TRACE=1, DEBUG=5, INFO=9, WARN=13, ERROR=17, FATAL=21). Source file and line are mapped to code.filepath and code.lineno attributes.


Per-Transport Redaction

Apply different redaction rules to different transports:

const log = createLogger({
  transports: [
    prettyTransport(),                                         // no redaction — full data in dev console
    { transport: fileTransport('app.log'), redact: ['password'] },  // redact passwords in file
    { transport: networkTransport, redact: 'fast' },                // auto-detect PII for network
  ],
  file: false,
})

Per-transport redaction runs in addition to logger-level redaction (if configured). Supports the same rules: path-based, wildcard, regex, and 'fast' mode.


CLI Log Viewer

Pretty-print and filter JSONL log files from the terminal:

# Basic usage
npx nexlogger tail logs/app.log

# Filter by level
npx nexlogger tail logs/app.log --level error

# Grep messages
npx nexlogger tail logs/app.log --grep "payment"

# Follow mode (watch for new lines)
npx nexlogger tail logs/app.log -f

# Output as JSON (for piping)
npx nexlogger tail logs/app.log --level warn --json

Full Options Reference

import { createLogger } from 'nexlogger'

const log = createLogger({
  // Minimum level to log. Default: 'info' (or LOG_LEVEL env var)
  level: 'debug',

  // Logger name, included in output
  name: 'my-app',

  // Output destinations
  transports: [jsonTransport(), fileTransport('app.log')],

  // PII redaction rules
  redact: ['user.password'] | [{ pattern: /regex/, replace: '[X]' }] | 'fast',

  // Suppress all output
  silent: false,

  // OpenTelemetry trace injection
  otel: 'auto',  // 'auto' | true | false

  // Source file + line capture
  source: false,  // true | false | { enabled: true, depth: 10 }

  // Probabilistic sampling per level
  sample: { trace: 0.01, debug: 0.1 },

  // File transport control
  file: true,  // true | false | 'path/to/file.log'

  // Transport error handler
  onError: (err, entry) => { /* handle transport failure */ },
})

Log Entry Structure

Every transport receives a LogEntry object:

{
  level: 'info',              // log level
  msg: 'user login',          // message string
  time: '2024-01-15T10:30:00.000Z',  // ISO 8601 timestamp
  pid: 12345,                 // process ID
  hostname: 'server-1',       // system hostname
  name: 'my-app',             // logger name (if set)
  meta: { userId: 42 },       // merged metadata (bindings + args + context)
  traceId: 'abc...',          // OpenTelemetry trace ID (if active)
  spanId: 'def...',           // OpenTelemetry span ID (if active)
  traceFlags: 1,              // OpenTelemetry trace flags (if active)
  file: 'src/auth.ts',        // source file (if source enabled)
  line: 42,                   // source line (if source enabled)
}

JSON and file transports flatten meta into the top level. Pretty transport shows meta inline after the message.


Dual CJS + ESM

Works in both module systems with full TypeScript types:

// ESM
import { createLogger } from 'nexlogger'

// CJS
const { createLogger } = require('nexlogger')

All types are exported:

import type {
  LogLevel, LogEntry, Transport, TransportWithLevel,
  LoggerOptions, LoggerInstance,
  ConfigureOptions, ConfigureLoggerEntry,
  RedactRule, RedactPattern, SampleConfig, SourceConfig,
  AsyncFileOptions, RotatingFileOptions,
  AccessLogFormat, AccessLogOptions,
  TestLoggerResult, BatchTransport, BatchTransportOptions,
} from 'nexlogger'

Feature Summary

| Feature | Details | |---------|---------| | Zero dependencies | Node.js built-ins only. Optional @opentelemetry/api peer dep. | | Dual CJS + ESM | Full support with proper .d.ts, .d.mts, .d.cts | | Library-safe silent mode | getLogger() is a noop until configure() activates it | | Auto env-aware output | Pretty in dev, JSON in prod, JSON fallback when not TTY | | 6 log levels | trace, debug, info, warn, error, fatal | | Flexible call signatures | String, printf, error, object-first (pino), object-second | | Child loggers | .child({ key: value }) with inherited bindings | | AsyncLocalStorage context | withContext() propagates across async boundaries | | Semantic redaction | Path-based, wildcard arrays, regex patterns, fast auto-detect | | OpenTelemetry injection | Auto traceId/spanId/traceFlags from active span | | Source file + line | Opt-in stack capture with configurable depth | | Log sampling | Per-level rates, deterministic per requestId | | JSON transport | Circular reference + BigInt safe (all transports) | | Pretty transport | Color-coded, TTY-aware, error stack display | | File transport (sync) | Synchronous append, auto directory creation | | Async file transport | Buffered stream writes, configurable flush interval (default in createLogger) | | Rotating file transport | Size-based + time-based rotation, maxFiles cleanup | | Per-transport level filter | Route error-only to a file, all levels to stdout | | Custom transports | Any (entry) => void \| Promise<void> function | | Transport error handling | onError callback for sync throws + async rejections | | Async flush / drain | flush() awaits all pending async transports | | Graceful shutdown | Auto SIGTERM/beforeExit hooks with 500ms timeout | | Express middleware | Auto requestId generation + ALS propagation | | Fastify plugin | Auto requestId generation + ALS propagation | | HTTP access logs | CLF, combined, JSON formats with skip filter | | debug package compat | DEBUG=app:* env var, debug.enable() / debug.disable() | | Runtime level change | logger.setLevel('warn') | | Level check | isLevelEnabled() avoids expensive computation | | Lazy evaluation | log.debug(() => expensive()) — fn skipped when level disabled | | Test logger | createTestLogger() captures entries for assertions | | Batch transport | Buffer + flush for network destinations | | Typed metadata | createLogger<TMeta>() with compile-time type checking | | Worker thread transport | MessagePort-based log forwarding from workers | | OTel LogRecord emission | Native OTel log records via @opentelemetry/api-logs | | Per-transport redaction | Different redaction rules per transport destination | | CLI log viewer | npx nexlogger tail with level/grep/follow filtering | | TypeScript strict | Full type safety with generics and function overloads |


License

MIT