From 7b9e88ef59b1350b6bd9aad651e0498ad0f6f26d Mon Sep 17 00:00:00 2001 From: Tiara Rodney Date: Fri, 13 Mar 2026 23:14:49 +0100 Subject: [PATCH] feat(formatter): implement %-style formatting, formatTime, formatError 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. --- src/config.ts | 2 +- src/formatter.ts | 151 ++++++++++++++++++++++++----------------------- 2 files changed, 77 insertions(+), 76 deletions(-) diff --git a/src/config.ts b/src/config.ts index d33cfa4..57ba2d1 100644 --- a/src/config.ts +++ b/src/config.ts @@ -121,7 +121,7 @@ export function basicConfig(options: BasicConfigOptions) { const filename = options.filename ?? null; const stream = options.stream ?? null; const filemode = options.filemode ?? 'a'; - const dateformat = options.datefmt ?? null; + const dateformat = options.datefmt; const style = options.style ?? '%'; const level = options.level ?? null; diff --git a/src/formatter.ts b/src/formatter.ts index cf1d6e6..542d3ac 100644 --- a/src/formatter.ts +++ b/src/formatter.ts @@ -10,14 +10,20 @@ export interface PercentFormatterStyleOptions { 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 = + public static validationPattern = /%\(\w+\)[#0+ -]*(\*|\d+)?(\.(\*|\d+))?[diouxefgcrsa%]/; - private fmt: string; + public fmt: string; private defaults: {[key: string]: any}; constructor(options: PercentFormatterStyleOptions) { @@ -26,7 +32,7 @@ class PercentFormatterStyle { } usesTime(): boolean { - return this.fmt.match(PercentFormatterStyle.asctimeFormat) ? true : false + return this.fmt.indexOf(PercentFormatterStyle.asctimeSearch) >= 0; } /** @@ -42,12 +48,32 @@ class PercentFormatterStyle { } protected _format(record: LogRecord): string { - var defaults = this.defaults; - var values: {[key: string]: any}|null; - if (defaults) { values = {...this.defaults, ...Object.entries(record)} } - else { values = Object.entries(record) } - //TODO: implement formatting - return 'would do some formatting'; + 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 { @@ -55,12 +81,13 @@ class PercentFormatterStyle { 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 = '%(level)s:%(name)s:%(message)s'; +const BASIC_FORMAT = '%(levelname)s:%(name)s:%(message)s'; export const STYLES: {[key: string]: [{ new(options: PercentFormatterStyleOptions): PercentFormatterStyle}, string]} = { '%': [PercentFormatterStyle, BASIC_FORMAT], @@ -68,7 +95,7 @@ export const STYLES: {[key: string]: [{ new(options: PercentFormatterStyleOption export interface FormatterOptions { fmt?: string - datefmt?: any + datefmt?: string style?: string validate?: boolean defaults?: {[key: string]: any} @@ -95,53 +122,26 @@ export interface FormatterOptions { * WARNING, ERROR, CRITICAL) * %(levelname)s Text logging level for the message ("DEBUG", "INFO", * "WARNING", "ERROR", "CRITICAL") - * %(pathname)s Full pathname of the source file where the logging - * call was issued (if available) - * %(filename)s Filename portion of pathname - * %(module)s Module (name portion of filename) - * %(lineno)d Source line number where the logging call was issued - * (if available) - * %(funcName)s Function name - * %(created)f Time when the LogRecord was created (time.time_ns() / 1e9 + * %(created)f Time when the LogRecord was created (Date.now() * return value) * %(asctime)s Textual time when the LogRecord was created - * %(msecs)d Millisecond portion of the creation time - * %(relativeCreated)d Time in milliseconds when the LogRecord was created, - * relative to the time the logging module was loaded - * (typically at application startup time) - * %(thread)d Thread ID (if available) - * %(threadName)s Thread name (if available) - * %(taskName)s Task name (if available) - * %(process)d Process ID (if available) * %(message)s The result of record.getMessage(), computed just as * the record is emitted */ export class Formatter { - public static defaultTimeFormat = '%Y-%M'; - public static defaultMsecFormat = '%s,%30d'; + public static defaultTimeFormat = 'YYYY-MM-DDTHH:mm:ss'; + public static defaultMsecFormat = '%s.%03d'; - protected style: any; + protected style: PercentFormatterStyle; protected fmt: string; - protected datefmt: any; + protected datefmt: string|undefined; - /** - * Initialize the formatter with specified format strings. - * - * Initialize the formatter either with the specified format string, or a - * default as described above. Allow for specialized date formatting with - * the optional datefmt argument. If datefmt is omitted, you get an - * ISO8601-like (or RFC 3339-like) format. - * - * Use a style parameter of '%', '{' or '$' to specify that you want to - * use one of %-formatting, :meth:`str.format` (``{}``) formatting or - * :class:`string.Template` formatting in your format string. - */ constructor(options?: FormatterOptions) { options = options ?? {}; - var style = options.style ?? '%'; - var validate = options.validate ?? true; + const style = options.style ?? '%'; + const validate = options.validate ?? true; - if (!Object.keys(STYLES).includes(style ?? '')) { + if (!Object.keys(STYLES).includes(style)) { throw new ValueError(`style must be one of: ${Object.keys(STYLES).join(', ')}`) } @@ -157,23 +157,10 @@ export class Formatter { this.datefmt = options.datefmt; } - /** - * Return the creation time of the specified LogRecord as formatted text. - * - * This method should be called from format() by a formatter which - * wants to make use of a formatted time. This method can be overridden - * in formatters to provide for any specific requirement, but the - * basic behaviour is as follows: if datefmt (a string) is specified, - * it is used with time.strftime() to format the creation time of the - * record. Otherwise, an ISO8601-like (or RFC 3339-like) format is used. - * The resulting string is returned. This function uses a user-configurable - * function to convert the creation time to a tuple. By default, - * time.localtime() is used; to change this for a particular formatter - * instance, set the 'converter' attribute to a function with the same - * signature as time.localtime() or time.gmtime(). To change it for all - * formatters, for example if you want all logging times to be shown in GMT, - * set the 'converter' attribute in the Formatter class. - */ + usesTime(): boolean { + return this.style.usesTime(); + } + /** * Format the specified record as text. * @@ -185,31 +172,45 @@ export class Formatter { * 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); } - formatTime(record: LogRecord, datefmt?: any): string { + /** + * 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); - //TODO: record.created if (datefmt) { - //TODO: time.strftime - } - else { - //TODO: time.strftime + 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')); } - return 'some time'; + const iso = dt.toISOString(); + return iso.replace('T', ' ').replace('Z', ''); } /** * Format and return the specified exception information as a string. - - * This default implementation just uses - * traceback.print_exception() */ formatError(ei: MyError): string { - //TODO - return 'some error'; + if (ei.stack) { return ei.stack } + return String(ei); } }