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

v4.1.0

Published

Structured logging library with configurable transports, log levels, and contextual metadata

Readme

@pawells/logger

CI npm version Node License: MIT

Description

Structured logging library for TypeScript/Node.js with multiple transports, composable predicate-based filters, pluggable formatters, and zero runtime dependencies. ESM-only, targeting Node.js 22+.

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 included in every entry
LogManager.Context = 'my-app';
LogManager.Metadata = { version: '4.0.0', environment: 'production' };

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

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

// Log messages at any severity level
logger.verbose('Trace-level detail', { step: 'validation-start' });
logger.debug('Processing request', { requestId: 'req-123' });
logger.info('Application started', { port: 3000 });
logger.warn('Memory usage is high', { usage: 85 });
logger.error('Request failed', new Error('Connection refused'));
logger.fatal('System critical error', { errno: 'EACCES' });

// Flush async transports before shutdown
process.on('SIGTERM', async () => {
  await LogManager.Flush();
  process.exit(0);
});

API Reference

Architecture

Logger constructs a TLogEntry and posts it via the static LogManager. LogManager is a singleton event bus that distributes entries to all registered LogTransport instances. Each transport maintains an independent filter chain; all predicates in the chain must return true (AND logic, short-circuit) for an entry to reach the transport's OnPosted handler.

ConsoleTransport, StreamTransport, and MemoryTransport require explicit registration via transport.Register() or LogManager.RegisterTransport(). They do not self-register. The @pawells/logger-transport-file sibling package provides a FileTransport that self-registers in its constructor.


Classes

| Class | Import | Description | |---|---|---| | Logger | @pawells/logger | High-level interface for emitting log entries at any severity level | | LogManager | @pawells/logger | Static singleton event bus; distributes entries to transports | | LogManagerInstance | @pawells/logger | Instance-scoped equivalent of LogManager; useful for test isolation and multi-tenant use | | LogTransport | @pawells/logger | Abstract base class for custom transport implementations | | ConsoleTransport | @pawells/logger | Outputs entries to the console (stdout/stderr by level) | | StreamTransport | @pawells/logger | Writes entries to any writable stream (defaults to process.stderr) | | MemoryTransport | @pawells/logger | Captures entries in memory; designed for unit testing | | LogFormatter | @pawells/logger | Abstract base class for custom formatter implementations | | TextLogFormatter | @pawells/logger | Formats entries as human-readable text | | JSONLogFormatter | @pawells/logger | Formats entries as single-line JSON |


Logger

High-level interface for emitting log entries. Each instance carries an optional context string array that is merged into every entry it produces.

Constructor

constructor(context?: string | string[], metadata?: Record<string, unknown>)
  • context — optional label(s) for this logger instance. Merged with LogManager.Context when entries are posted.
  • metadata — optional default metadata merged into all entries from this logger.

Log methods

logger.verbose(message: string, metadata?: unknown): void
logger.debug(message: string, metadata?: unknown): void
logger.info(message: string, metadata?: unknown): void
logger.warn(message: string, metadata?: unknown): void
logger.error(message: string, metadata?: unknown): void  // errors are suppressed internally
logger.fatal(message: string, metadata?: unknown): void  // errors are suppressed internally

metadata is passed through NormalizeMetadata before being stored in the entry. See Metadata normalization.

Caveat: if metadata is a plain object with a top-level context or metadata key, it is instead interpreted as a per-call { context?, metadata? } override (the inner metadata value is what gets normalized and stored, not the wrapper object). Use logger.Post() for unambiguous control if your metadata payload legitimately has a top-level context or metadata field.

Context and metadata methods

logger.SetContext(context: string | string[]): void
logger.GetContext(): string[]
logger.SetMetadata(metadata: Record<string, unknown>, options?: { merge?: boolean }): void
logger.GetMetadata(): Readonly<Record<string, unknown>>

SetMetadata replaces existing metadata by default. Pass { merge: true } to merge with existing values.

Post

logger.Post(entry: TLogEntry): void

Posts a fully-formed TLogEntry directly. The logger merges its own context and metadata before forwarding to LogManager. The caller's entry object is not mutated.


LogManager

Static singleton event bus. Maintains application-level context and metadata that are prepended and merged into every log entry before distribution.

Properties

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

Application-level context prepended to every entry's context array. Accepts a string or array; the getter always returns an array.

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

Application-level metadata merged into every entry's metadata. To merge non-destructively:

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

The setter also accepts a callback, invoked fresh on every Post() call, for metadata that must be computed per-entry (e.g. from AsyncLocalStorage or an active OpenTelemetry span) without the library depending on those packages:

LogManager.Metadata = () => {
  const span = trace.getActiveSpan();
  return span ? { traceId: span.spanContext().traceId } : undefined;
};

Keep the callback cheap and synchronous — it runs on every Post(). Errors thrown by the callback are suppressed (logged to process.stderr); the affected entry falls back to an empty object rather than crashing the caller. While a callback is set, the Metadata getter returns {} — the value is only computed at Post() time, not cached.

Static methods

LogManager.Post(entry: TLogEntry): void

Distributes an entry to all registered transports. Creates an internal clone with merged context and metadata — the caller's entry object is not mutated.

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

Register or unregister a transport. Prefer calling transport.Register(name?) on the instance — it delegates here. Unregistration is idempotent; if the transport is not found, the call returns silently.

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

Retrieve registered transports by optional name. GetTransport returns the first match or undefined; GetTransports returns all matches or an empty array.

LogManager.Flush(): Promise<void>

Awaits Flush() on every registered transport. Use before application shutdown to ensure async transports (such as FileTransport) have written all pending entries. Errors from individual transports are suppressed so one failure does not block others.

LogManager.Reset(): void

Clears application-level context and metadata and unregisters all transports. Intended for test teardown only; do not call in production code.


LogManagerInstance

An instance-scoped alternative to the static LogManager. Maintains its own context, metadata, and transport registry, making it suitable for test isolation or multi-tenant scenarios where independent logging pipelines must not share state.

The API mirrors LogManager exactly, but as instance methods rather than static methods.

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

const logManager = new LogManagerInstance();
logManager.Context = 'tenant-a';
logManager.Metadata = { version: '1.0.0' };

const transport = new ConsoleTransport();
logManager.RegisterTransport(transport, 'console');
logManager.Post(entry);

await logManager.Flush();
logManager.Reset(); // clean up in afterEach

Instance methods: Post, RegisterTransport, UnregisterTransport, GetTransport<T>, GetTransports<T>, Flush, Reset.


LogLevels Enum

enum LogLevels {
  VERBOSE = 'verbose', // severity 5 — highly detailed trace diagnostics
  DEBUG   = 'debug',   // severity 10
  INFO    = 'info',    // severity 20
  WARN    = 'warn',    // severity 30
  ERROR   = 'error',   // severity 40
  FATAL   = 'fatal',   // severity 50
  SILENT  = 'silent',  // severity Infinity — suppresses all output when used as threshold
}

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


Transports

All built-in transports must be explicitly registered after construction. None self-register.

ConsoleTransport

Routes entries to the console. DEBUG/INFO go to console.log, WARN to console.warn, ERROR/FATAL to console.error. Defaults to TextLogFormatter.

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

// All entries, default text format
const transport = new ConsoleTransport();
transport.Register();

// Warn and above, JSON format
const jsonTransport = new ConsoleTransport({
  formatter: new JSONLogFormatter(),
  filters: [LogLevelFilter(LogLevels.WARN)],
});
jsonTransport.Register('console-json');

IConsoleTransportOptions:

| Field | Type | Default | Description | |---|---|---|---| | formatter | LogFormatter | TextLogFormatter | Formatter instance to use | | filters | LogEntryPredicate[] | none | Filter predicates; all must pass (AND logic) |

StreamTransport

Writes formatted entries to any writable stream. Defaults to process.stderr. Useful for servers that reserve stdout for 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:

| Field | Type | Default | Description | |---|---|---|---| | formatter | LogFormatter | TextLogFormatter | Formatter instance to use | | stream | IWritableStream | process.stderr | Writable stream to write to | | filters | LogEntryPredicate[] | none | Filter predicates; all must pass (AND logic) |

MemoryTransport

Captures entries in memory without any I/O. Designed for unit testing. See Testing for a full example.

import { MemoryTransport } from '@pawells/logger';

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

Methods:

| Method | Returns | Description | |---|---|---| | GetLogs() | readonly IMemoryLogEntry[] | All captured entries | | GetEntryCount() | number | Count of captured entries | | Clear() | void | Discards all captured entries |

IMemoryTransportOptions:

| Field | Type | Default | Description | |---|---|---|---| | formatter | LogFormatter | TextLogFormatter | Formatter instance to use | | filters | LogEntryPredicate[] | none | Filter predicates; all must pass (AND logic) | | maxEntries | number | 10000 | Maximum number of entries to retain. When exceeded, the oldest entry is evicted (FIFO). Must be a positive integer when provided. |


LogTransport Abstract Class

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

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

interface IWebhookTransportOptions extends ILogTransportOptions {
  endpoint: string;
}

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

  public async OnPosted(entry: TLogEntry): Promise<void> {
    await fetch(this.Options.endpoint, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ level: entry.level, message: entry.message }),
    });
  }
}

const webhook = new WebhookTransport({
  endpoint: 'https://example.com/logs',
  filters: [LogLevelFilter(LogLevels.ERROR)],
});
webhook.Register('webhook');

const logger = new Logger('api');
logger.error('Unhandled exception', new Error('Disk full'));

Runtime-mutable options:

Transport options can be updated after construction via the Options setter. Read the current configuration with the getter, then assign through the setter so subclass validation is not bypassed:

const current = transport.Options;
transport.Options = { ...current, filters: [LogLevelFilter(LogLevels.ERROR)] };

Flush() hook:

Override Flush(): Promise<void> if your transport buffers entries and needs to drain before shutdown. The default implementation is a no-op.


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:

| Field | Type | Default | Description | |---|---|---|---| | useColor | boolean | false | Apply ANSI color codes to the level field | | includeMetadata | boolean | false | Append serialized metadata to the output line |

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:

| Field | Type | Default | Description | |---|---|---|---| | includeMetadata | boolean | true | Include the metadata field in output |

LogFormatter Abstract Class

Extend this to create a custom formatter. Implement the single Format(entry: TLogEntry): string method.


Filters and Predicates

LogEntryPredicate

type LogEntryPredicate = (entry: TLogEntry) => boolean;

A function that receives a TLogEntry and returns true to allow the entry to proceed to the transport, or false to drop it. Multiple predicates on a transport are evaluated in order with AND logic and short-circuit on the first false.

// Filter by context
const contextFilter: LogEntryPredicate = (entry) =>
  entry.context.includes('payment');

// Filter by metadata field
const envFilter: LogEntryPredicate = (entry) =>
  entry.metadata['environment'] === 'production';

LogLevelFilter

function LogLevelFilter(minLevel: LogLevels): LogEntryPredicate

Creates a predicate that passes entries at or above minLevel. This is the standard way to apply a minimum severity threshold to a transport.

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

TLogEntry

The canonical log record passed to all transports. A Zod-inferred type alias (not a hand-written interface).

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

TLogEntry is derived from LOG_ENTRY_SCHEMA via z.infer<typeof LOG_ENTRY_SCHEMA>. Use the schema directly when you need runtime validation of untrusted input:

import { LOG_ENTRY_SCHEMA } from '@pawells/logger';

const result = LOG_ENTRY_SCHEMA.safeParse(untrustedInput);
if (result.success) {
  // result.data is TLogEntry
}

Metadata normalization

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

Logger log methods accept any value as metadata and apply the following rules before including it in the entry:

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

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


Level utilities

| Function | Signature | Description | |---|---|---| | LogLevelsFromString | (level: string) => LogLevels | Parses a case-insensitive string to LogLevels; throws on unknown input | | LogLevelsToString | (level: LogLevels) => string | Returns the string value of a LogLevels enum member | | LogLevelToNumber | (level: LogLevels) => number | Maps a level to its numeric severity (VERBOSE=5 … SILENT=Infinity) |


Validation utilities

These exported functions are useful when building custom transports or integrating with validation pipelines.

| Function | Throws | Description | |---|---|---| | AssertLogEntryPredicate(value) | TypeError | Asserts value is a function (valid LogEntryPredicate) | | AssertLogTransportOptions(value) | TypeError | Asserts value is a valid ILogTransportOptions object | | ValidateLogTransportOptions(value) | — | Non-throwing alternative; returns boolean | | AssertConsoleTransportOptions(value) | TypeError | Asserts value is valid IConsoleTransportOptions | | ValidateConsoleTransportOptions(value) | — | Non-throwing alternative; returns boolean | | AssertStreamTransportOptions(value) | TypeError | Asserts value is valid IStreamTransportOptions | | ValidateStreamTransportOptions(value) | — | Non-throwing alternative; returns boolean | | AssertMemoryTransportOptions(value) | TypeError | Asserts value is valid IMemoryTransportOptions | | ValidateMemoryTransportOptions(value) | — | Non-throwing alternative; returns boolean |


All exported types

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

  // Transports
  ConsoleTransport,
  StreamTransport,
  MemoryTransport,

  // Formatters
  LogFormatter,
  TextLogFormatter,
  JSONLogFormatter,

  // 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
  LogLevels,

  // Functions
  LogLevelFilter,
  NormalizeMetadata,
  LogLevelsFromString,
  LogLevelsToString,
  LogLevelToNumber,

  // Validation guards
  AssertLogEntryPredicate,
  AssertLogTransportOptions,
  ValidateLogTransportOptions,
  AssertConsoleTransportOptions,
  ValidateConsoleTransportOptions,
  AssertStreamTransportOptions,
  ValidateStreamTransportOptions,
  AssertMemoryTransportOptions,
  ValidateMemoryTransportOptions,
} from '@pawells/logger';

Testing

MemoryTransport is the recommended approach for unit tests. It captures all entries without any I/O, supports the same filter system as production transports, and gives you direct access to both the raw TLogEntry and the formatted string output.

Always unregister the transport in afterEach to prevent accumulation across tests. Use LogManager.Reset() to clear context and metadata in a single call.

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

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

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

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

  it('logs the expected message at INFO level', () => {
    const logger = new Logger('user-service');
    logger.info('User authenticated', { userId: '42' });

    expect(transport.GetEntryCount()).toBe(1);
    const [log] = transport.GetLogs();
    expect(log.entry.message).toBe('User authenticated');
    expect(log.entry.level).toBe(LogLevels.INFO);
    expect(log.entry.metadata).toEqual({ userId: '42' });
  });

  it('normalizes Error metadata', () => {
    const logger = new Logger('user-service');
    logger.error('Auth failed', new Error('Token expired'));

    const [log] = transport.GetLogs();
    expect(log.entry.metadata).toHaveProperty('error', 'Token expired');
    expect(log.entry.metadata).toHaveProperty('name', 'Error');
  });
});

Tip: Use LogManagerInstance in tests that need fully isolated logging state:

const logManager = new LogManagerInstance();
const transport = new MemoryTransport();
logManager.RegisterTransport(transport);
// logManager posts do not appear in global LogManager transports
afterEach(() => logManager.Reset());

Development

Run these commands from the repository root:

yarn nx run @pawells/logger:typecheck         # type check
yarn nx run @pawells/logger:lint              # lint
yarn nx run @pawells/logger:lint -- --fix     # lint with auto-fix
yarn nx run @pawells/logger:test              # run tests
yarn nx run @pawells/logger:test -- --coverage  # tests with coverage report
yarn nx run @pawells/logger:build            # compile TypeScript -> dist/

Compiled output is written to packages/logger/dist/. The dist/ directory is gitignored and included in the npm files array.

License

MIT — see LICENSE for details.