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

@pawells/logger

v3.0.3

Published

[![npm](https://img.shields.io/npm/v/@pawells/logger)](https://www.npmjs.com/package/@pawells/logger) [![GitHub Release](https://img.shields.io/github/v/release/PhillipAWells/logger)](https://github.com/PhillipAWells/logger/releases) [![CI](https://github

Readme

Common Logger Utility

npm GitHub Release CI Node License: MIT

Structured logging library for TypeScript/Node.js with ESM, no runtime dependencies, and support for multiple transports and formatters for structured logging and log aggregation.

Features

  • Structured logging with configurable log levels (DEBUG, INFO, WARN, ERROR, FATAL, SILENT)
  • Multiple built-in transports: Console (stdout), Stream (any writable stream), and Memory (for testing)
  • Composable predicate-based filter system per transport
  • Pluggable transport system via LogTransport abstract base class for custom integrations
  • LogLevelFilter() helper for concise level-threshold filters
  • JSON formatter for log aggregation platforms
  • ISO 8601 timestamps via standard Date objects
  • Support for per-logger context identifiers and structured metadata
  • Full TypeScript support with strict typing
  • ESM-only, no runtime dependencies
  • Targets ES2022, runs on Node.js >= 22.0.0

Requirements

  • Node.js >= 22.0.0

Installation

npm install @pawells/logger
# or
yarn add @pawells/logger

Quick Start

import {
  Logger,
  LogManager,
  ConsoleTransport,
  LogLevelFilter,
  LogLevels,
} from '@pawells/logger';

// Optional: set application-level context and metadata
LogManager.Context = 'my-app';
LogManager.Metadata = { version: '1.0.0', environment: 'production' };

// Create a transport and register it to start receiving entries
const transport = new ConsoleTransport({
  filters: [LogLevelFilter(LogLevels.DEBUG)],
});
transport.Register();

// Create a logger with an optional context identifier
const logger = new Logger('user-service');

// Log messages at different levels
logger.debug('Debug information', { userId: 123 });
logger.info('Application started');
logger.warn('Memory usage is high', { usage: 85 });
logger.error('Request failed', new Error('Connection refused'));
logger.fatal('System critical error', { errno: 'EACCES' });

API Reference

Logger

High-level interface for emitting log entries. Each logger carries an optional context string that is included in every entry it produces. All log methods are synchronous.

Constructor

constructor(context?: string | string[], metadata?: Record<string, unknown>)
  • context — optional label(s) for this logger instance. Can be a string (e.g., 'payment-service') or an array of strings (e.g., ['api', 'payment-service']). Appears in the context array of every TLogEntry produced by this logger. Merged with LogManager context during Post().
  • metadata — optional metadata to include in all log entries from this logger. Merged with LogManager metadata and entry metadata during Post().

Methods

debug(message: string, metadata?: unknown): void
info(message: string, metadata?: unknown): void
warn(message: string, metadata?: unknown): void
error(message: string, metadata?: unknown): void
fatal(message: string, metadata?: unknown): void

metadata is normalised by NormalizeMetadata before inclusion in the log entry — see Metadata Normalisation for the rules.

SetContext(context: string | string[]): void

Replaces the logger's context with the provided value.

SetMetadata(metadata: Record<string, unknown>, options?: { merge?: boolean }): void

Sets or merges the logger's metadata. By default, replaces existing metadata; pass { merge: true } to merge instead.

GetMetadata(): Readonly<Record<string, unknown>>

Returns the current metadata for this logger instance.


LogManager

Static event bus that connects Logger producers to LogTransport consumers. Maintains application-level context and metadata that are automatically included in every log entry.

Properties

LogManager.Context = context: string | string[]  // setter
LogManager.Context                               // getter → readonly string[]

Sets or returns the application-level context. Accepts a string or array of strings. The context is prepended to every logger's context during Post(). Returns an empty array if not set.

LogManager.Metadata = metadata: Record<string, unknown>  // setter
LogManager.Metadata                                      // getter → Readonly<Record<string, unknown>>

Sets or returns the application-level metadata. The metadata is merged into every log entry. Returns an empty object if not set. To merge non-destructively, combine the existing metadata with new values before assigning:

LogManager.Metadata = { ...LogManager.Metadata, newKey: 'value' };

Methods

LogManager.Post(entry: TLogEntry): void

Distributes a log entry to all subscribed transports. Before posting, LogManager context is prepended to the entry's context and LogManager metadata is merged with the entry's metadata. The entry object is mutated in place; do not reuse it after calling Post.

LogManager.RegisterTransport(transport: LogTransport, name?: string): void
LogManager.UnregisterTransport(transport: LogTransport): void

Register or unregister a transport directly. Prefer calling transport.Register(name?) on the transport instance — it delegates here. UnregisterTransport is used in test teardown.

LogManager.GetTransport<T>(name?: string): T | undefined
LogManager.GetTransports<T>(name?: string): T[]

Retrieve a registered transport by optional name. GetTransport returns the first match (or undefined); GetTransports returns all matches.


LogLevels Enum

enum LogLevels {
  DEBUG  = 'debug',
  INFO   = 'info',
  WARN   = 'warn',
  ERROR  = 'error',
  FATAL  = 'fatal',
  SILENT = 'silent', // suppresses all output when used as the filter threshold
}

Severity order (lowest to highest): DEBUG (10) < INFO (20) < WARN (30) < ERROR (40) < FATAL (50) < SILENT (∞).


LogLevelFilter

function LogLevelFilter(minLevel: LogLevels): LogEntryPredicate

Creates a filter predicate that passes entries at or above the specified minimum severity. This is the standard way to restrict a transport to a minimum log level:

import { LogLevelFilter, LogLevels, ConsoleTransport } from '@pawells/logger';

const transport = new ConsoleTransport({
  filters: [LogLevelFilter(LogLevels.WARN)],
});
transport.Register();
// Only WARN, ERROR, and FATAL entries reach this transport

ConsoleTransport

Outputs log entries to the console. DEBUG and INFO route to console.log, WARN to console.warn, and ERROR/FATAL to console.error.

Transports do not auto-register. Call Register() after construction to begin receiving entries.

import { ConsoleTransport, LogLevelFilter, LogLevels } from '@pawells/logger';

// Default: TextLogFormatter, no level filter (all entries pass through)
const transport = new ConsoleTransport();
transport.Register();

// JSON output, suppress DEBUG
const jsonTransport = new ConsoleTransport({
  formatter: new JSONLogFormatter(),
  filters: [LogLevelFilter(LogLevels.INFO)],
});
jsonTransport.Register();

IConsoleTransportOptions:

  • formatter?: LogFormatter — formatter to use (default: TextLogFormatter)
  • filters?: LogEntryPredicate[] — array of filter predicates; all must return true for an entry to be output (default: no filtering)

StreamTransport

Writes all output to a writable stream (defaults to process.stderr). Useful for servers that reserve stdout for structured protocol output (MCP, JSON-RPC, LSP, etc.).

import { StreamTransport, LogLevelFilter, LogLevels } from '@pawells/logger';

const transport = new StreamTransport({
  filters: [LogLevelFilter(LogLevels.WARN)],
});
transport.Register();

// Custom writable stream
const streamTransport = new StreamTransport({ stream: myWritableStream });
streamTransport.Register('my-stream');

IStreamTransportOptions:

  • formatter?: LogFormatter — formatter to use (default: TextLogFormatter)
  • stream?: IWritableStream — writable stream to use (default: process.stderr)
  • filters?: LogEntryPredicate[] — array of filter predicates (default: no filtering)

MemoryTransport

Captures log entries in memory. Designed for unit testing — inspect captured entries without involving any I/O. Does not output anywhere.

import { MemoryTransport, Logger, LogManager, LogLevels } from '@pawells/logger';

const transport = new MemoryTransport();
transport.Register();

const logger = new Logger('auth');
logger.info('User authenticated', { userId: '42' });

const logs = transport.GetLogs();
console.log(logs.length);               // 1
console.log(logs[0].entry.message);     // 'User authenticated'
console.log(logs[0].entry.level);       // 'info'
console.log(logs[0].formatted);         // formatted string output

transport.GetEntryCount();              // 1
transport.Clear();                      // reset to empty

Methods:

  • GetLogs(): readonly IMemoryLogEntry[] — returns all captured entries
  • GetEntryCount(): number — returns the count of captured entries
  • Clear(): void — discards all captured entries

IMemoryLogEntry:

  • entry: TLogEntry — the raw log entry
  • formatted: string — the string output produced by the configured formatter

IMemoryTransportOptions:

  • formatter?: LogFormatter — formatter to use (default: TextLogFormatter)
  • filters?: LogEntryPredicate[] — array of filter predicates (default: no filtering)

Formatters

TextLogFormatter

Formats entries as human-readable text:

[2026-05-01T12:00:00.000Z] [my-app] [INFO] [user-service] User authenticated
import { TextLogFormatter } from '@pawells/logger';

const formatter = new TextLogFormatter({
  useColor: true,        // apply ANSI color codes to the level field (default: false)
  includeMetadata: true, // append JSON-serialized metadata (default: false)
});

ITextLogFormatterOptions:

  • useColor?: boolean — enable ANSI colors on the level field (default: false)
  • includeMetadata?: boolean — append serialized metadata (default: false)

JSONLogFormatter

Formats entries as a single JSON line:

{"timestamp":"2026-05-01T12:00:00.000Z","level":"info","context":["my-app","user-service"],"message":"User authenticated","metadata":{"userId":"123"}}
import { JSONLogFormatter } from '@pawells/logger';

const formatter = new JSONLogFormatter({ includeMetadata: true });

IJSONLogFormatterOptions:

  • includeMetadata?: boolean — include the metadata field in output (default: true)

LogTransport Abstract Class

Extend this to create a custom transport. Do not call Register() inside the constructor — registration is the caller's responsibility.

import {
  LogTransport,
  ILogTransportOptions,
  type TLogEntry,
  LogLevelFilter,
  LogLevels,
} from '@pawells/logger';

interface IFileTransportOptions extends ILogTransportOptions {
  filePath: string;
}

class FileTransport extends LogTransport<IFileTransportOptions> {
  constructor(options: IFileTransportOptions) {
    super(options);
    // Do not call Register() here — the caller registers after construction
  }

  public async OnPosted(entry: TLogEntry): Promise<void> {
    const contextStr = entry.context.join(' > ');
    const line = `[${entry.level}] [${contextStr}] ${entry.message}\n`;
    // write to file...
  }
}

LogManager.Context = 'my-app';

const fileTransport = new FileTransport({
  filePath: './logs/app.log',
  filters: [LogLevelFilter(LogLevels.INFO)],
});
fileTransport.Register('file');

const logger = new Logger('api');
logger.info('Server started', { port: 3000 });

Mutable Options

Transport options are mutable at runtime via the Options property. Use the getter to read the current configuration and the setter to replace it:

// Read current options
const current = transport.Options;

// Replace options (runs validation in subclasses that override the setter)
transport.Options = { ...current, formatter: new JSONLogFormatter() };

Treat the object returned by the getter as a snapshot — apply changes through the setter rather than mutating the returned reference directly, so that subclass validation is not bypassed.


TLogEntry

The canonical log record passed to all transports:

type TLogEntry = {
  timestamp: Date;                       // time the entry was created
  level: LogLevels;                      // severity level
  context: string[];                     // hierarchical context (parent-child chain)
  message: string;                       // log message text
  metadata: Record<string, unknown>;     // merged structured metadata
}

TLogEntry is a Zod-inferred type alias (via z.infer<typeof LOG_ENTRY_SCHEMA>), not a hand-written interface. The context array is the merged result of LogManager context prepended to Logger context. The metadata object is the merged result of LogManager metadata, Logger metadata, and entry-level metadata.


Metadata Normalisation

NormalizeMetadata(metadata: unknown): Record<string, unknown> | undefined

The Logger methods accept any value as metadata and apply the following rules before storing it in TLogEntry.metadata:

| Input | Result | |---|---| | null / undefined | omitted (field absent) | | Empty plain object {} | omitted (field absent) | | Error instance | { error: message, name, stack } | | Array or primitive | { value: metadata } | | Non-empty plain object | passed through as-is |

NormalizeMetadata is also exported directly for use in custom transports and formatters.


Log Aggregation Integration

Use JSONLogFormatter with a custom transport to forward structured logs to an aggregation platform:

import {
  Logger,
  LogManager,
  LogTransport,
  type TLogEntry,
  ILogTransportOptions,
  JSONLogFormatter,
  LogLevelFilter,
  LogLevels,
} from '@pawells/logger';

class AggregationTransport extends LogTransport<ILogTransportOptions> {
  private readonly formatter = new JSONLogFormatter();
  private readonly endpoint: string;

  constructor(endpoint: string) {
    super({ filters: [LogLevelFilter(LogLevels.INFO)] });
    this.endpoint = endpoint;
  }

  public async OnPosted(entry: TLogEntry): Promise<void> {
    const body = this.formatter.Format(entry);
    await fetch(this.endpoint, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body,
    });
  }
}

LogManager.Context = 'my-app';

const aggregator = new AggregationTransport('https://aggregation.example.com/api/v1/push');
aggregator.Register('aggregation');

const logger = new Logger('auth');
logger.info('User login successful', { userId: '42' });

Testing

MemoryTransport is the recommended approach for unit testing. It captures all entries in memory for assertion without any I/O, and supports the same filter system as other transports.

import { MemoryTransport, Logger, LogManager, LogLevelFilter, LogLevels } from '@pawells/logger';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';

describe('my-service', () => {
  let transport: MemoryTransport;

  beforeEach(() => {
    transport = new MemoryTransport();
    transport.Register();
  });

  afterEach(() => {
    LogManager.UnregisterTransport(transport);
    LogManager.Context = [];
    LogManager.Metadata = {};
  });

  it('logs the expected message', () => {
    const logger = new Logger('my-service');
    logger.info('Operation completed', { id: 'abc' });

    expect(transport.GetEntryCount()).toBe(1);
    const [entry] = transport.GetLogs();
    expect(entry.entry.message).toBe('Operation completed');
    expect(entry.entry.level).toBe(LogLevels.INFO);
    expect(entry.entry.metadata).toEqual({ id: 'abc' });
  });
});

Always call LogManager.UnregisterTransport(transport) in afterEach to prevent transport accumulation across tests.


TypeScript Support

All types are exported from the main entry point for custom implementations:

import {
  // Core classes
  Logger,
  LogManager,
  LogTransport,

  // Transports
  ConsoleTransport,
  StreamTransport,
  MemoryTransport,

  // Formatters
  TextLogFormatter,
  JSONLogFormatter,
  LogFormatter,

  // Types
  type TLogEntry,
  type LogEntryPredicate,
  type ILogTransportOptions,
  type IConsoleTransportOptions,
  type IStreamTransportOptions,
  type IWritableStream,
  type IMemoryLogEntry,
  type IMemoryTransportOptions,
  type ITextLogFormatterOptions,
  type IJSONLogFormatterOptions,
  type ILogPostedEvent,
  type TLogPostedEventHandler,

  // Schema
  LOG_ENTRY_SCHEMA,

  // Enum and utilities
  LogLevels,
  LogLevelsFromString,
  LogLevelsToString,
  LogLevelToNumber,
  LogLevelFilter,
  NormalizeMetadata,
} from '@pawells/logger';

Development

yarn install                          # Install dependencies
yarn nx run-many -t build             # Compile TypeScript (tsconfig.build.json) → ./build/
yarn nx run-many -t typecheck         # Type check without building
yarn nx run-many -t lint              # ESLint
yarn lint:fix                         # ESLint with auto-fix
yarn nx run-many -t test              # Run tests
yarn test:ui                          # Interactive Vitest UI
yarn nx run-many -t test -- --coverage  # Tests with coverage report

License

MIT — See LICENSE for details.