From 8993d69c3f7da146e791e81d4aaab2cdab508120 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vladim=C3=ADr=20Gorej?= Date: Mon, 30 Oct 2023 15:58:35 +0100 Subject: [PATCH] feat(logging): finish Formatter implementation (#3337) Refs #3197 --- packages/apidom-logging/src/Formatter.ts | 95 +++++++++++++++++++++++- packages/apidom-logging/src/LogRecord.ts | 13 +++- packages/apidom-logging/src/index.ts | 1 + 3 files changed, 106 insertions(+), 3 deletions(-) diff --git a/packages/apidom-logging/src/Formatter.ts b/packages/apidom-logging/src/Formatter.ts index 2c17fa79a2..eaf6f71137 100644 --- a/packages/apidom-logging/src/Formatter.ts +++ b/packages/apidom-logging/src/Formatter.ts @@ -1,6 +1,7 @@ import { ApiDOMStructuredError } from '@swagger-api/apidom-error'; import STYLES, { Style } from './styles'; +import type { LogRecordInstance } from './LogRecord'; export interface FormatterOptions { readonly fmt?: Style['fmt']; @@ -10,11 +11,60 @@ export interface FormatterOptions { readonly defaults?: Record; } -class Formatter { +export interface FormatterInstance { + format(record: LogRecordInstance): string; +} + +export interface FormatterConstructor { + new (options: FormatterOptions): FormatterInstance; +} + +const momentToIntlFormat = (momentFormat: string): Intl.DateTimeFormatOptions => { + if (momentToIntlFormat.cache.has(momentFormat)) { + return momentToIntlFormat.cache.get(momentFormat)!; + } + + const mapping = { + YYYY: { year: 'numeric' }, + YY: { year: '2-digit' }, + MMMM: { month: 'long' }, + MMM: { month: 'short' }, + MM: { month: '2-digit' }, + DD: { day: '2-digit' }, + dddd: { weekday: 'long' }, + ddd: { weekday: 'short' }, + HH: { hour: '2-digit', hour12: false }, + hh: { hour: '2-digit', hour12: true }, + mm: { minute: '2-digit' }, + ss: { second: '2-digit' }, + A: { hour12: true }, + z: { timeZoneName: 'short' }, // abbreviated time zone name + Z: { timeZoneName: 'short' }, // offset from GMT + }; + type MomentToken = keyof typeof mapping; + + const intlOptions: Intl.DateTimeFormatOptions = Object.keys(mapping).reduce((opts, token) => { + if (momentFormat.includes(token)) { + return { ...opts, ...mapping[token as MomentToken] }; + } + return opts; + }, {}); + + momentToIntlFormat.cache.set(momentFormat, intlOptions); + + return intlOptions; +}; +momentToIntlFormat.cache = new Map(); + +class Formatter implements FormatterInstance { protected readonly style: Style; protected readonly datefmt?: string; + protected readonly defaultDateTimeFormat!: 'DD MM YYYY hh:mm:ss'; + + protected readonly appendMsecInfo = true; + constructor(options: FormatterOptions = {}) { const style = options.style ?? '$'; @@ -37,6 +87,49 @@ class Formatter { this.datefmt = options.datefmt; } + + protected usesTime(): boolean { + return this.style.usesTime(); + } + + protected formatTime(record: LogRecordInstance, datefmt?: string): string { + const intlOptions = momentToIntlFormat(datefmt ?? this.defaultDateTimeFormat); + const formattedTime = new Intl.DateTimeFormat(undefined, intlOptions).format(record.created); + + if (this.appendMsecInfo) { + return `${formattedTime},${String(record.msecs).padStart(3, '0')}`; + } + + return formattedTime; + } + + protected formatMessage(record: LogRecordInstance): string { + return this.style.format(record); + } + + // eslint-disable-next-line class-methods-use-this + protected formatError(error: T): string { + return `Error: ${error.message}\nStack: ${error.stack ?? 'No stack available'}`; + } + + public format(record: LogRecordInstance) { + if (this.usesTime()) { + record.asctime = this.formatTime(record, this.datefmt); // eslint-disable-line no-param-reassign + } + + const formattedMessage = this.formatMessage(record); + + if (record.error && typeof record.error_text === 'undefined') { + record.error_text = this.formatError(record.error); // eslint-disable-line no-param-reassign + } + + if (record.error_text) { + const separator = formattedMessage.endsWith('\n') ? '' : '\n'; + return `${formattedMessage}${separator}${record.error_text}`; + } + + return formattedMessage; + } } export default Formatter; diff --git a/packages/apidom-logging/src/LogRecord.ts b/packages/apidom-logging/src/LogRecord.ts index bd34d9f237..143c1333de 100644 --- a/packages/apidom-logging/src/LogRecord.ts +++ b/packages/apidom-logging/src/LogRecord.ts @@ -6,11 +6,16 @@ const startTime: number = Date.now(); export interface LogRecordInstance { readonly name: string; readonly message: string; + readonly created: number; + readonly msecs: number; + readonly relativeCreated: number; + asctime?: string; readonly levelname: string; readonly levelno: number; readonly process?: number; readonly processName?: string; readonly error?: T; + error_text?: string; [key: string]: unknown; } @@ -39,12 +44,16 @@ class LogRecord implements LogRecordInstance { public readonly relativeCreated: number; + public asctime?: string; + public readonly process?: number; public readonly processName?: string; public readonly error?: T; + public error_text?: string; + [key: string]: unknown; constructor( @@ -61,8 +70,8 @@ class LogRecord implements LogRecordInstance { this.levelname = getLevelName(level); this.message = message; this.error = error; - this.created = Math.floor(created / 1000); - this.msecs = created - this.created * 1000; + this.created = created; + this.msecs = created % 1000; this.relativeCreated = created - startTime; if (globalThis.process?.pid) { diff --git a/packages/apidom-logging/src/index.ts b/packages/apidom-logging/src/index.ts index 0cd943e2ea..55c8715d22 100644 --- a/packages/apidom-logging/src/index.ts +++ b/packages/apidom-logging/src/index.ts @@ -4,3 +4,4 @@ export const { CRITICAL, ERROR, WARNING, INFO, DEBUG, NOTSET } = LoggingLevel; export { getLevelName, getLevelNamesMapping, addLevelName } from './LoggingLevel'; export { getLogRecordClass, setLogRecordClass, default as LogRecord } from './LogRecord'; export { default as Filter } from './Filter'; +export { default as Formatter } from './Formatter';