PercentFormatterStyle._format() now performs %(key)s/d/f substitution against LogRecord attributes. Formatter.format() sets record.message via getMessage() and conditionally calls formatTime() for asctime. formatTime() uses JS Date with strftime-style tokens (%Y,%m,%d,%H, %M,%S) or ISO8601 fallback. formatError() returns Error.stack. BASIC_FORMAT updated to use %(levelname)s. Fixed datefmt type in config.ts.
217 lines
7.3 KiB
TypeScript
217 lines
7.3 KiB
TypeScript
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();
|