@pawells/logger
v3.0.3
Published
[](https://www.npmjs.com/package/@pawells/logger) [](https://github.com/PhillipAWells/logger/releases) [
- 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
LogTransportabstract base class for custom integrations LogLevelFilter()helper for concise level-threshold filters- JSON formatter for log aggregation platforms
- ISO 8601 timestamps via standard
Dateobjects - 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/loggerQuick 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 thecontextarray of everyTLogEntryproduced by this logger. Merged with LogManager context duringPost().metadata— optional metadata to include in all log entries from this logger. Merged with LogManager metadata and entry metadata duringPost().
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): voidmetadata is normalised by NormalizeMetadata before inclusion in the log entry — see
Metadata Normalisation for the rules.
SetContext(context: string | string[]): voidReplaces the logger's context with the provided value.
SetMetadata(metadata: Record<string, unknown>, options?: { merge?: boolean }): voidSets 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): voidDistributes 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): voidRegister 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): LogEntryPredicateCreates 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 transportConsoleTransport
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 returntruefor 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 emptyMethods:
GetLogs(): readonly IMemoryLogEntry[]— returns all captured entriesGetEntryCount(): number— returns the count of captured entriesClear(): void— discards all captured entries
IMemoryLogEntry:
entry: TLogEntry— the raw log entryformatted: 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 authenticatedimport { 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 reportLicense
MIT — See LICENSE for details.
