esm-logging/src/handler.ts
Tiara Rodney 3a6332a855
feat(handler): implement StreamHandler, ConsoleHandler, StderrHandler emit
StreamHandler.emit() writes formatted output to a Writable stream.
ConsoleHandler maps log levels to console.warn/error/log. StderrHandler
emits via console.error. handleError() logs diagnostics instead of
throwing. FileHandler throws NotImplementedError on construction.
Removed Node.js stream import and conditional require() block.
2026-03-13 23:57:37 +01:00

239 lines
6.8 KiB
TypeScript

import { LogLevel, checkLevel, NOTSET, WARNING, ERROR } from './log-level';
import { LogRecord } from './log-record';
import { Formatter, DEFAULT_FORMATTER } from './formatter';
import { Filterer } from './filter';
import { NotImplementedError } from './helper/error';
import { Writable, DEFAULT_STREAM, StderrWritable } from './helper/stream';
//---------------------------------------------------------------------------
// Handler classes and functions
//----------------------------------------------------------------------------
type Handlers = {[key: string]: Handler};
/**
* map of handler names to handlers
*/
const HANDLERS: Handlers = {};
/**
* added to allow handlers to be removed in reverse order of initialization
*/
const HANDLER_LIST: WeakRef<Handler>[] = [];
/**
* Add a handler to the internal cleanup list using a weak reference.
*
* @param handler -
*/
function addHandlerRef(handler: Handler) {
HANDLER_LIST.push(new WeakRef(handler));
}
/**
* Get a handler with the specified *name*, or None if there isn't one with
* that name.
*/
export function getHandlerByName(name: string): Handler|null {
return HANDLERS[name] ?? null
}
/**
* Return all known handler names as an immutable set
*/
export function getHandlerNames(): Handlers { return Object.freeze(HANDLERS) }
/**
* Handler instances dispatch logging events to specific destinations.
*
* The base handler class. Acts as a placeholder which defines the Handler
* interface. Handlers can optionally use Formatter instances to format
* records as desired. By default, no formatter is specified; in this case,
* the 'raw' message as determined by record.message is logged.
*/
export class Handler extends Filterer {
protected _scope: string|null = null;
protected _formatter: Formatter|null = null;
protected _level: number;
protected _closed: boolean = false;
/**
* Initializes the instance - basically setting the formatter to None
* and the filter list to empty
*/
constructor(level?: LogLevel) {
super();
this._level = checkLevel(level ?? NOTSET);
// Add the handler to the global HANDLER_LIST (for cleanup on shutdown)
addHandlerRef(this);
}
get level(): number { return this._level }
set level(level: LogLevel|string) { this._level = checkLevel(level) }
get scope(): string|null { return this._scope }
set scope(scope: string) { this._scope = scope }
get closed(): boolean { return this._closed }
/**
* Format the specified record.
*
* If a formatter is set, use it. Otherwise, use the default formatter for
* the module.
*/
format(record: LogRecord): string {
const fmt = this.formatter ?? DEFAULT_FORMATTER;
return fmt.format(record);
}
/**
* Do whatever it takes to actually log the specified logging record.
*
* This version is intended to be implemented by subclasses and so raises a
* NotImplementedError.
*/
emit(record: LogRecord) {
throw new NotImplementedError(
'emit must be implemented by Handler subclass'
)
}
/**
* Conditionally emit the specfied logging record.
*
* Emission depends on filters which may have been added to the handler.
* Wrap the actual emission of the record with acquisition/release of the
* I/O thread lock.
*/
handle(record: LogRecord) {
const rv = this.filter(record);
if (!rv) { return }
let filtered = record;
if ((rv as any) instanceof LogRecord) {
filtered = rv as unknown as LogRecord
}
this.emit(filtered)
}
/**
* Tidy up any resources used by the handler
*
* This version removes the handler from an internal map of handlers, which
* is used for handler lookup by scope. Subclasses should ensure that this
* gets called from overriden close() methods.
*/
close() {
this._closed = true;
if (this.scope && Object.keys(HANDLERS).includes(this.scope)) {
delete HANDLERS[this.scope]
}
}
/**
* Handle errors which occur during an emit() call.
*
* This method should be called from handlers when an exception is
* encountered during an emit() call.
*/
handleError(record: LogRecord) {
try {
console.error('--- Logging error ---');
console.error('Error in handler for record:', record.scope, record.msg);
}
catch (_) {
// silently ignore errors in error handling
}
}
get formatter(): Formatter|null { return this._formatter }
set formatter(fmt: Formatter) { this._formatter = fmt }
}
/**
* A handler class which writes logging records, appropriately formatted,
* to a stream. Note that this class does not close the stream, as
* the default stream may be shared.
*/
export class StreamHandler extends Handler {
protected stream: Writable;
constructor(stream?: Writable) {
super();
this.stream = stream ?? DEFAULT_STREAM;
}
emit(record: LogRecord) {
try {
const msg = this.format(record);
this.stream.write(msg);
}
catch (e) {
this.handleError(record);
}
}
}
/**
* A handler class which writes logging records to the browser console,
* mapping log levels to the appropriate console method.
*
* This is the primary handler for browser environments.
*/
export class ConsoleHandler extends Handler {
emit(record: LogRecord) {
try {
const msg = this.format(record);
if (record.levelno >= ERROR) {
console.error(msg);
}
else if (record.levelno >= WARNING) {
console.warn(msg);
}
else {
console.log(msg);
}
}
catch (e) {
this.handleError(record);
}
}
}
export interface FileHandlerOptions {
filename: string
filemode?: string
encoding?: string
errors?: string
}
export class FileHandler extends StreamHandler {
constructor(options: FileHandlerOptions) {
super();
throw new NotImplementedError(
'FileHandler is not available in browser environments. ' +
'Use ConsoleHandler or a storage-backed handler instead.'
);
}
}
/**
* This class is like a StreamHandler using sys.stderr, but always uses
* whatever sys.stderr is currently set to rather than the value of
* sys.stderr at handler construction time.
*/
export class StderrHandler extends Handler {
constructor(level: LogLevel) { super(level) }
emit(record: LogRecord) {
try {
const msg = this.format(record);
console.error(msg);
}
catch (e) {
this.handleError(record);
}
}
}