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[] = []; /** * 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); } } }