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

@rawnodes/logger

v2.7.0

Published

High-performance Pino-based logger with AsyncLocalStorage context, O(1) level rules, and timing utilities

Readme

@rawnodes/logger

Flexible Winston-based logger with AsyncLocalStorage context, level rules, and multiple output formats.

Features

  • Multiple Formats - JSON, plain (colored), logfmt, simple
  • Context Propagation - Automatic context via AsyncLocalStorage
  • Level Rules - Configure log levels per module/context in config
  • Lazy Meta - Defer metadata creation until log level check passes
  • Timing Utilities - Built-in performance measurement
  • Request ID - Generate and extract request IDs
  • Secret Masking - Automatic masking of sensitive data
  • TypeScript First - Full generic type support

Installation

pnpm add @rawnodes/logger
# or
npm install @rawnodes/logger

Quick Start

import { Logger } from '@rawnodes/logger';

const logger = Logger.create({
  level: 'info',
  console: { format: 'plain' },
});

logger.info('Hello world');
logger.info('User logged in', { userId: 123 });

Configuration

Basic Config

import { Logger } from '@rawnodes/logger';

const logger = Logger.create({
  level: 'info',                    // default log level
  console: { format: 'plain' },     // console output format
  file: {                           // optional file output
    format: 'json',
    dirname: 'logs',
    filename: 'app-%DATE%.log',
    datePattern: 'YYYY-MM-DD',
    maxFiles: '14d',
    maxSize: '20m',
  },
});

Level Rules

Configure different log levels for specific modules or contexts:

const logger = Logger.create({
  level: {
    default: 'info',
    rules: [
      { match: { context: 'auth' }, level: 'debug' },        // debug for auth module
      { match: { context: 'database' }, level: 'warn' },     // warn for database
      { match: { userId: 123 }, level: 'debug' },            // debug for user 123
      { match: { context: 'api', userId: 456 }, level: 'silly' }, // combined match
    ],
  },
  console: { format: 'plain' },
});

const authLogger = logger.for('auth');
authLogger.debug('This will be logged');  // matches rule

const dbLogger = logger.for('database');
dbLogger.info('This will NOT be logged'); // level is warn

Rules from config are readonly and cannot be removed via API.

Output Formats

| Format | Description | Example | |--------|-------------|---------| | json | Structured JSON | {"level":"info","message":"hello","timestamp":"..."} | | plain | Colored, human-readable | [2025-01-01T12:00:00] info [APP] hello | | logfmt | Key=value pairs | level=info msg=hello context=APP ts=2025-01-01T12:00:00 | | simple | Minimal | [2025-01-01T12:00:00] info: hello |

// Different formats for console and file
const logger = Logger.create({
  level: 'info',
  console: { format: 'plain' },   // colored for development
  file: {
    format: 'json',               // structured for log aggregation
    dirname: 'logs',
    filename: 'app-%DATE%.log',
  },
});

Per-Transport Level

Each transport can have its own log level:

const logger = Logger.create({
  level: 'silly',                        // accept all at logger level
  console: {
    format: 'plain',
    level: 'debug',                      // console: debug and above
  },
  file: {
    format: 'json',
    level: 'warn',                       // file: only warn and error
    dirname: 'logs',
    filename: 'app-%DATE%.log',
  },
});

| Scenario | Console | File | |----------|---------|------| | Development | debug | — | | Production | info | warn | | Troubleshooting | debug | info | | Critical alerts | info | error |

This is useful for sending only critical errors to alerting systems while keeping verbose logs in console.

Per-Transport Rules

Each transport can have its own filtering rules. Use level: 'off' with rules to create a whitelist pattern:

const logger = Logger.create({
  level: 'info',
  console: { format: 'plain' },
  file: {
    format: 'json',
    level: 'off',                              // off by default
    rules: [
      { match: { context: 'payments' }, level: 'error' },  // only errors from payments
      { match: { context: 'auth' }, level: 'warn' },       // warn+ from auth
    ],
    dirname: 'logs',
    filename: 'critical-%DATE%.log',
  },
});

// Only error logs from 'payments' context go to file
logger.for('payments').error('Payment failed');  // → file
logger.for('payments').info('Processing');       // ✗ not logged to file
logger.for('other').error('Generic error');      // ✗ not logged to file (no matching rule)

Rules can also match store context (AsyncLocalStorage):

const logger = Logger.create({
  level: 'info',
  console: { format: 'plain' },
  file: {
    format: 'json',
    level: 'off',
    rules: [
      { match: { userId: 123 }, level: 'debug' },           // debug for specific user
      { match: { context: 'api', premium: true }, level: 'debug' }, // debug for premium API users
    ],
    dirname: 'logs',
    filename: 'debug-%DATE%.log',
  },
});

// Logs for user 123 go to file
store.run({ userId: 123 }, () => {
  logger.debug('User action');  // → file
});

Use level: 'off' in rules to suppress specific contexts:

const logger = Logger.create({
  level: 'debug',
  console: {
    format: 'plain',
    level: 'debug',
    rules: [
      { match: { context: 'noisy-module' }, level: 'off' },  // suppress noisy logs
    ],
  },
});

logger.for('noisy-module').debug('Spam');  // ✗ not logged
logger.for('other').debug('Useful info');  // → logged

External Transports

Discord

Send logs to Discord via webhooks with rich embeds:

const logger = Logger.create({
  level: 'info',
  console: { format: 'plain' },
  discord: {
    webhookUrl: 'https://discord.com/api/webhooks/xxx/yyy',
    level: 'error',                    // only errors to Discord
    username: 'My App Logger',         // optional bot name
    avatarUrl: 'https://...',          // optional avatar
    embedColors: {                     // optional custom colors
      error: 0xFF0000,
      warn: 0xFFAA00,
    },
    batchSize: 10,                     // messages per batch (default: 10)
    flushInterval: 2000,               // flush interval ms (default: 2000)
  },
});

Telegram

Send logs to Telegram chats/channels:

const logger = Logger.create({
  level: 'info',
  console: { format: 'plain' },
  telegram: {
    botToken: process.env.TG_BOT_TOKEN!,
    chatId: process.env.TG_CHAT_ID!,
    level: 'warn',                     // warn and above
    parseMode: 'Markdown',             // 'Markdown' | 'MarkdownV2' | 'HTML'
    disableNotification: false,        // mute non-error by default
    threadId: 123,                     // optional forum topic ID
    replyToMessageId: 456,             // optional reply to message
    batchSize: 20,                     // default: 20
    flushInterval: 1000,               // default: 1000
  },
});

Use level: 'off' with rules for selective logging:

telegram: {
  botToken: '...',
  chatId: '...',
  level: 'off',                        // off by default
  rules: [
    { match: { context: 'payments' }, level: 'error' },  // only payment errors
  ],
}

CloudWatch

Send logs to AWS CloudWatch Logs:

const logger = Logger.create({
  level: 'info',
  console: { format: 'plain' },
  cloudwatch: {
    logGroupName: '/app/my-service',
    region: 'us-east-1',
    accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
    secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
    createLogGroup: true,              // auto-create group (default: false)
    createLogStream: true,             // auto-create stream (default: true)
    batchSize: 100,                    // default: 100
    flushInterval: 1000,               // default: 1000
  },
});

Log Stream Name

The logStreamName field is optional and supports multiple configuration formats:

// Option 1: Static string
cloudwatch: {
  logGroupName: '/app/my-service',
  logStreamName: 'my-static-stream',
  // ...
}

// Option 2: Predefined pattern
cloudwatch: {
  logGroupName: '/app/my-service',
  logStreamName: { pattern: 'hostname-date' },
  // ...
}

// Option 3: Custom template
cloudwatch: {
  logGroupName: '/app/my-service',
  logStreamName: { template: '{hostname}/{env}/{date}' },
  // ...
}

// Option 4: Omit for default (hostname)
cloudwatch: {
  logGroupName: '/app/my-service',
  // logStreamName defaults to hostname
  // ...
}

Available patterns:

| Pattern | Example Output | |---------|----------------| | hostname | my-server | | hostname-date | my-server/2025-01-15 | | hostname-uuid | my-server-a1b2c3d4 | | date | 2025-01-15 | | uuid | a1b2c3d4-e5f6-7890-abcd |

Template variables:

| Variable | Description | |----------|-------------| | {hostname} | Server hostname (config.hostname → HOSTNAME env → os.hostname()) | | {date} | Current date (YYYY-MM-DD) | | {datetime} | Current datetime (YYYY-MM-DD-HH-mm-ss) | | {uuid} | 8-char UUID (generated once at startup) | | {pid} | Process ID | | {env} | NODE_ENV (default: "development") |

Hostname resolution priority:

  1. config.hostname (from logger config)
  2. process.env.HOSTNAME (useful in Docker/Kubernetes)
  3. os.hostname()

Examples:

// Kubernetes-friendly: pod name as stream
cloudwatch: {
  logGroupName: '/app/my-service/production',
  logStreamName: { pattern: 'hostname' },  // -> "my-app-pod-abc123"
  // ...
}

// Daily rotation with hostname
cloudwatch: {
  logGroupName: '/app/my-service',
  logStreamName: { pattern: 'hostname-date' },  // -> "my-server/2025-01-15"
  // ...
}

// Custom format with environment
cloudwatch: {
  logGroupName: '/app/my-service',
  logStreamName: { template: '{env}/{hostname}/{date}' },  // -> "production/my-server/2025-01-15"
  // ...
}

Transport Options

All external transports support:

| Option | Default | Description | |--------|---------|-------------| | level | — | Log level filter | | rules | — | Per-transport filtering rules | | batchSize | varies | Max messages per batch | | flushInterval | varies | Flush interval in ms | | maxRetries | 3 | Retry count on failure | | retryDelay | 1000 | Base retry delay in ms |

Multiple Transports

You can configure multiple instances of the same transport type using arrays:

const logger = Logger.create({
  level: 'info',
  console: { format: 'plain' },
  discord: [
    {
      webhookUrl: 'https://discord.com/api/webhooks/payments/xxx',
      level: 'off',
      rules: [{ match: { context: 'payments' }, level: 'error' }],
    },
    {
      webhookUrl: 'https://discord.com/api/webhooks/auth/yyy',
      level: 'off',
      rules: [{ match: { context: 'auth' }, level: 'error' }],
    },
  ],
  telegram: [
    { botToken: 'token1', chatId: 'errors-chat', level: 'error' },
    { botToken: 'token2', chatId: 'alerts-chat', level: 'warn' },
  ],
});

// Payment errors go to payments Discord channel
logger.for('payments').error('Payment failed');

// Auth errors go to auth Discord channel
logger.for('auth').error('Login failed');

Graceful Shutdown

External transports (Discord, Telegram, CloudWatch) buffer messages before sending. To ensure no logs are lost on process exit, use graceful shutdown.

Automatic (Recommended)

Enable autoShutdown in config to automatically handle SIGTERM/SIGINT:

const logger = Logger.create({
  level: 'info',
  console: { format: 'plain' },
  cloudwatch: { /* ... */ },
  autoShutdown: true,  // Auto-register shutdown handlers
});

// Or with options:
const logger = Logger.create({
  level: 'info',
  console: { format: 'plain' },
  cloudwatch: { /* ... */ },
  autoShutdown: {
    timeout: 10000,              // Max wait time (default: 5000ms)
    signals: ['SIGTERM'],        // Signals to handle (default: ['SIGTERM', 'SIGINT'])
  },
});

Manual

For more control, use registerShutdown or call shutdown() directly:

import { Logger, registerShutdown } from '@rawnodes/logger';

const logger = Logger.create(config);

// Option 1: Register handlers manually
registerShutdown(logger, {
  timeout: 10000,
  onShutdown: async () => {
    console.log('Flushing logs...');
  },
});

// Option 2: Call shutdown directly (e.g., in your own signal handler)
process.on('SIGTERM', async () => {
  await logger.shutdown();  // Flush all buffered messages
  process.exit(0);
});

In Tests

For tests, call shutdown() in afterAll to flush pending logs:

afterAll(async () => {
  await logger.shutdown();
});

Singleton Pattern

For app-wide logging:

// src/logger.ts
import { createSingletonLogger, type LoggerContext } from '@rawnodes/logger';

export interface AppContext extends LoggerContext {
  userId?: number;
  requestId?: string;
}

export const AppLogger = createSingletonLogger<AppContext>();

// src/main.ts
import { AppLogger } from './logger.js';

AppLogger.init({
  level: {
    default: 'info',
    rules: [{ match: { context: 'debug-module' }, level: 'debug' }],
  },
  console: { format: 'plain' },
});

// Anywhere in your app
const logger = AppLogger.for('UserService');
logger.info('User created', { userId: 123 });

Context Propagation

Automatically include context in all logs within an async scope:

// Express middleware
app.use((req, res, next) => {
  const context = {
    userId: req.user?.id,
    requestId: req.headers['x-request-id'] || generateRequestId(),
  };
  AppLogger.getStore().run(context, () => next());
});

// All logs within this request will include userId and requestId
logger.info('Processing request');
// Output: [2025-01-01T12:00:00] info [APP] Processing request
//   userId: 123
//   requestId: abc-123

Dynamic Level Overrides

Add/remove level overrides at runtime:

// Enable debug for specific user (e.g., for troubleshooting)
logger.setLevelOverride({ userId: 123 }, 'debug');

// Enable debug for specific module
logger.setLevelOverride({ context: 'payments' }, 'debug');

// Remove override
logger.removeLevelOverride({ userId: 123 });

// Clear all dynamic overrides (keeps config rules)
logger.clearLevelOverrides();

// Get all overrides
const overrides = logger.getLevelOverrides();
// [{ match: { context: 'payments' }, level: 'debug', readonly: false }]

Lazy Meta

Defer expensive object creation:

// BAD: Object created even if debug is disabled
logger.debug('Data processed', { result: expensiveSerialize(data) });

// GOOD: Function only called when debug is enabled
logger.debug('Data processed', () => ({ result: expensiveSerialize(data) }));

Child Loggers

Create scoped loggers:

const logger = AppLogger.for('PaymentService');
logger.info('Processing payment');
// Output: [timestamp] info [PaymentService] Processing payment

const stripeLogger = logger.for('Stripe');
stripeLogger.info('Charging card');
// Output: [timestamp] info [Stripe] Charging card

Utilities

Timing

import { measureAsync, measureSync } from '@rawnodes/logger';

// Async
const { result, timing } = await measureAsync('fetch-users', async () => {
  return await userService.findAll();
});
console.log(timing); // { label: 'fetch-users', durationMs: 45.23, durationFormatted: '45.23ms' }

// Sync
const { result, timing } = measureSync('compute', () => {
  return heavyComputation();
});

Request ID

import { generateRequestId, extractRequestId, getOrGenerateRequestId } from '@rawnodes/logger';

// Generate new
generateRequestId();                          // "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
generateRequestId({ short: true });           // "a1b2c3d4"
generateRequestId({ prefix: 'req' });         // "req-a1b2c3d4-e5f6-..."

// Extract from headers (checks x-request-id, x-correlation-id, x-trace-id)
extractRequestId(req.headers);                // string | undefined

// Extract or generate
getOrGenerateRequestId(req.headers);          // always returns string

Secret Masking

Automatically masks sensitive fields in logs:

import { maskSecrets, createMasker } from '@rawnodes/logger';

maskSecrets({
  user: 'admin',
  password: 'secret123',
  apiKey: 'key_abc123',
});
// { user: 'admin', password: '***', apiKey: '***' }

// Custom masker
const masker = createMasker({
  patterns: ['ssn', 'creditCard'],
  mask: '[REDACTED]'
});

Default masked patterns: password, secret, token, apikey, api_key, api-key, auth, credential, private

Logfmt Utilities

Helper functions for logfmt format:

import { flattenObject, formatLogfmt, formatLogfmtValue } from '@rawnodes/logger';

flattenObject({ user: { id: 123, name: 'John' } });
// { 'user.id': 123, 'user.name': 'John' }

formatLogfmt({ level: 'info', msg: 'hello', userId: 123 });
// "level=info msg=hello userId=123"

API Reference

Logger

class Logger<TContext> {
  static create(config: LoggerConfig, store?: LoggerStore): Logger;

  for(context: string): Logger;
  getStore(): LoggerStore<TContext>;

  // Logging
  error(message: string, error?: Error, meta?: Meta): void;
  warn(message: string, meta?: Meta): void;
  info(message: string, meta?: Meta): void;
  http(message: string, meta?: Meta): void;
  verbose(message: string, meta?: Meta): void;
  debug(message: string, meta?: Meta): void;
  silly(message: string, meta?: Meta): void;

  // Level overrides
  setLevelOverride(match: LevelOverrideMatch, level: LogLevel): void;
  removeLevelOverride(match: LevelOverrideMatch): boolean;
  clearLevelOverrides(): void;
  getLevelOverrides(): LevelOverride[];

  // Winston profiling
  profile(id: string, meta?: object): void;
}

Types

type LogLevel = 'error' | 'warn' | 'info' | 'http' | 'verbose' | 'debug' | 'silly' | 'off';
type LogFormat = 'json' | 'plain' | 'logfmt' | 'simple';
type Meta = object | (() => object);

interface LoggerConfig {
  level: LogLevel | {
    default: LogLevel;
    rules?: LevelRule[];
  };
  console: {
    format: LogFormat;
    level?: LogLevel;      // optional, overrides global level
    rules?: LevelRule[];   // optional, per-transport filtering
  };
  file?: {
    format: LogFormat;
    level?: LogLevel;      // optional, overrides global level
    rules?: LevelRule[];   // optional, per-transport filtering
    dirname: string;
    filename: string;
    datePattern?: string;
    maxFiles?: string;
    maxSize?: string;
    zippedArchive?: boolean;
  };
}

interface LevelRule {
  match: Record<string, unknown> & { context?: string };
  level: LogLevel;
}

Integration Examples

Express

import express from 'express';
import { AppLogger, generateRequestId } from './logger';

const app = express();

app.use((req, res, next) => {
  const context = {
    requestId: req.headers['x-request-id'] || generateRequestId({ short: true }),
    userId: req.user?.id,
  };
  AppLogger.getStore().run(context, () => next());
});

NestJS

import { Injectable, NestMiddleware } from '@nestjs/common';
import { AppLogger, generateRequestId } from './logger';

@Injectable()
export class LoggerMiddleware implements NestMiddleware {
  use(req: any, res: any, next: () => void) {
    const context = {
      requestId: req.headers['x-request-id'] || generateRequestId({ short: true }),
    };
    AppLogger.getStore().run(context, () => next());
  }
}

Telegraf

import { Telegraf } from 'telegraf';
import { AppLogger } from './logger';

bot.use((ctx, next) => {
  const context = {
    telegramUserId: ctx.from?.id,
    chatId: ctx.chat?.id,
  };
  return AppLogger.getStore().run(context, () => next());
});

License

MIT