import { MyError, ValueError } from './helper/error'; import { LogRecord } from './log-record'; //--------------------------------------------------------------------------- // Formatter classes and functions //--------------------------------------------------------------------------- export interface PercentFormatterStyleOptions { fmt?: string, defaults: {[key: string]: any}; } /** * %-style formatting for log records. * * Substitutes %(name)s, %(name)d, %(name)f placeholders from record * attributes. */ class PercentFormatterStyle { public static defaultFormat = '%(message)s'; public static asctimeFormat = '%(asctime)s'; public static asctimeSearch = '%(asctime)'; public static validationPattern = /%\(\w+\)[#0+ -]*(\*|\d+)?(\.(\*|\d+))?[diouxefgcrsa%]/; public fmt: string; private defaults: {[key: string]: any}; constructor(options: PercentFormatterStyleOptions) { this.fmt = options.fmt ?? PercentFormatterStyle.defaultFormat; this.defaults = options.defaults; } usesTime(): boolean { return this.fmt.indexOf(PercentFormatterStyle.asctimeSearch) >= 0; } /** * Validate the input format, ensure it matches the correct style */ validate() { if (!PercentFormatterStyle.validationPattern.test(this.fmt)) { throw new ValueError( `Invalid format '${this.fmt}' for ` + `'${PercentFormatterStyle.defaultFormat[0]}'` ) } } protected _format(record: LogRecord): string { const values: {[key: string]: any} = { ...this.defaults, ...(record as unknown as {[key: string]: any}), }; return this.fmt.replace( /%\((\w+)\)([#0+ -]*(?:\*|\d+)?(?:\.(?:\*|\d+))?[diouxefgcrsa%])/g, (_match, key, spec) => { if (!(key in values)) { throw new ValueError(`formatting field not found in record: ${key}`); } const val = values[key]; const conversion = spec[spec.length - 1]; switch (conversion) { case 'd': case 'i': return String(Math.floor(Number(val))); case 'f': return String(Number(val)); case 's': return String(val); default: return String(val); } } ); } format(record: LogRecord): string { try { return this._format(record) } catch (e) { if (e instanceof ValueError) { throw e } throw new ValueError(`formatting field not found in record: ${e}`) } } } const BASIC_FORMAT = '%(levelname)s:%(name)s:%(message)s'; export const STYLES: {[key: string]: [{ new(options: PercentFormatterStyleOptions): PercentFormatterStyle}, string]} = { '%': [PercentFormatterStyle, BASIC_FORMAT], } export interface FormatterOptions { fmt?: string datefmt?: string style?: string validate?: boolean defaults?: {[key: string]: any} } /** * Formatter instances are used to convert a LogRecord to text. * * Formatters need to know how a LogRecord is constructed. They are * responsible for converting a LogRecord to (usually) a string which can * be interpreted by either a human or an external system. The base Formatter * allows a formatting string to be specified. If none is supplied, the * style-dependent default value, "%(message)s", "{message}", or * "${message}", is used. * * The Formatter can be initialized with a format string which makes use of * knowledge of the LogRecord attributes - e.g. the default value mentioned * above makes use of the fact that the user's message and arguments are pre- * formatted into a LogRecord's message attribute. Currently, the useful * attributes in a LogRecord are described by: * * %(name)s Name of the logger (logging channel) * %(levelno)s Numeric logging level for the message (DEBUG, INFO, * WARNING, ERROR, CRITICAL) * %(levelname)s Text logging level for the message ("DEBUG", "INFO", * "WARNING", "ERROR", "CRITICAL") * %(created)f Time when the LogRecord was created (Date.now() * return value) * %(asctime)s Textual time when the LogRecord was created * %(message)s The result of record.getMessage(), computed just as * the record is emitted */ export class Formatter { public static defaultTimeFormat = 'YYYY-MM-DDTHH:mm:ss'; public static defaultMsecFormat = '%s.%03d'; protected style: PercentFormatterStyle; protected fmt: string; protected datefmt: string|undefined; constructor(options?: FormatterOptions) { options = options ?? {}; const style = options.style ?? '%'; const validate = options.validate ?? true; if (!Object.keys(STYLES).includes(style)) { throw new ValueError(`style must be one of: ${Object.keys(STYLES).join(', ')}`) } this.style = new STYLES[style][0]({ fmt: options.fmt, defaults: options.defaults ?? {} }); if (validate) { this.style.validate() } this.fmt = this.style.fmt; this.datefmt = options.datefmt; } usesTime(): boolean { return this.style.usesTime(); } /** * Format the specified record as text. * * The record's attribute dictionary is used as the operand to a string * formatting operation which yields the returned string. Before the * formatting operation, a couple of preparatory steps are carried out. * The message attribute of the record is computed using LogRecord.getMessage(). * If the formatting string uses the time, formatTime() is called to format * the event time. */ format(record: LogRecord): string { record.message = record.getMessage(); if (this.usesTime()) { record.asctime = this.formatTime(record, this.datefmt); } return this.style.format(record); } /** * Return the creation time of the specified LogRecord as formatted text. * * If datefmt is specified, it is used as a strftime-style format string. * Supported tokens: %Y, %m, %d, %H, %M, %S. * Otherwise, an ISO8601-like format is used. */ formatTime(record: LogRecord, datefmt?: string): string { const dt = new Date(record.created); if (datefmt) { return datefmt .replace('%Y', String(dt.getFullYear())) .replace('%m', String(dt.getMonth() + 1).padStart(2, '0')) .replace('%d', String(dt.getDate()).padStart(2, '0')) .replace('%H', String(dt.getHours()).padStart(2, '0')) .replace('%M', String(dt.getMinutes()).padStart(2, '0')) .replace('%S', String(dt.getSeconds()).padStart(2, '0')); } const iso = dt.toISOString(); return iso.replace('T', ' ').replace('Z', ''); } /** * Format and return the specified exception information as a string. */ formatError(ei: MyError): string { if (ei.stack) { return ei.stack } return String(ei); } } export const DEFAULT_FORMATTER = new Formatter();