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.
This commit is contained in:
Tiara Rodney 2026-03-13 23:14:49 +01:00
parent c0ced4cda2
commit 7b9e88ef59
No known key found for this signature in database
GPG key ID: 5CD8EC1D46106723
2 changed files with 77 additions and 76 deletions

View file

@ -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;

View file

@ -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);
}
}