Skip to content

Commit 8993d69

Browse files
authored
feat(logging): finish Formatter implementation (#3337)
Refs #3197
1 parent a4e2357 commit 8993d69

File tree

3 files changed

+106
-3
lines changed

3 files changed

+106
-3
lines changed

packages/apidom-logging/src/Formatter.ts

+94-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { ApiDOMStructuredError } from '@swagger-api/apidom-error';
22

33
import STYLES, { Style } from './styles';
4+
import type { LogRecordInstance } from './LogRecord';
45

56
export interface FormatterOptions {
67
readonly fmt?: Style['fmt'];
@@ -10,11 +11,60 @@ export interface FormatterOptions {
1011
readonly defaults?: Record<string, unknown>;
1112
}
1213

13-
class Formatter {
14+
export interface FormatterInstance {
15+
format(record: LogRecordInstance): string;
16+
}
17+
18+
export interface FormatterConstructor {
19+
new (options: FormatterOptions): FormatterInstance;
20+
}
21+
22+
const momentToIntlFormat = (momentFormat: string): Intl.DateTimeFormatOptions => {
23+
if (momentToIntlFormat.cache.has(momentFormat)) {
24+
return momentToIntlFormat.cache.get(momentFormat)!;
25+
}
26+
27+
const mapping = {
28+
YYYY: { year: 'numeric' },
29+
YY: { year: '2-digit' },
30+
MMMM: { month: 'long' },
31+
MMM: { month: 'short' },
32+
MM: { month: '2-digit' },
33+
DD: { day: '2-digit' },
34+
dddd: { weekday: 'long' },
35+
ddd: { weekday: 'short' },
36+
HH: { hour: '2-digit', hour12: false },
37+
hh: { hour: '2-digit', hour12: true },
38+
mm: { minute: '2-digit' },
39+
ss: { second: '2-digit' },
40+
A: { hour12: true },
41+
z: { timeZoneName: 'short' }, // abbreviated time zone name
42+
Z: { timeZoneName: 'short' }, // offset from GMT
43+
};
44+
type MomentToken = keyof typeof mapping;
45+
46+
const intlOptions: Intl.DateTimeFormatOptions = Object.keys(mapping).reduce((opts, token) => {
47+
if (momentFormat.includes(token)) {
48+
return { ...opts, ...mapping[token as MomentToken] };
49+
}
50+
return opts;
51+
}, {});
52+
53+
momentToIntlFormat.cache.set(momentFormat, intlOptions);
54+
55+
return intlOptions;
56+
};
57+
momentToIntlFormat.cache = new Map<string, Intl.DateTimeFormatOptions>();
58+
59+
class Formatter implements FormatterInstance {
1460
protected readonly style: Style;
1561

1662
protected readonly datefmt?: string;
1763

64+
protected readonly defaultDateTimeFormat!: 'DD MM YYYY hh:mm:ss';
65+
66+
protected readonly appendMsecInfo = true;
67+
1868
constructor(options: FormatterOptions = {}) {
1969
const style = options.style ?? '$';
2070

@@ -37,6 +87,49 @@ class Formatter {
3787

3888
this.datefmt = options.datefmt;
3989
}
90+
91+
protected usesTime(): boolean {
92+
return this.style.usesTime();
93+
}
94+
95+
protected formatTime(record: LogRecordInstance, datefmt?: string): string {
96+
const intlOptions = momentToIntlFormat(datefmt ?? this.defaultDateTimeFormat);
97+
const formattedTime = new Intl.DateTimeFormat(undefined, intlOptions).format(record.created);
98+
99+
if (this.appendMsecInfo) {
100+
return `${formattedTime},${String(record.msecs).padStart(3, '0')}`;
101+
}
102+
103+
return formattedTime;
104+
}
105+
106+
protected formatMessage(record: LogRecordInstance): string {
107+
return this.style.format(record);
108+
}
109+
110+
// eslint-disable-next-line class-methods-use-this
111+
protected formatError<T extends Error>(error: T): string {
112+
return `Error: ${error.message}\nStack: ${error.stack ?? 'No stack available'}`;
113+
}
114+
115+
public format(record: LogRecordInstance) {
116+
if (this.usesTime()) {
117+
record.asctime = this.formatTime(record, this.datefmt); // eslint-disable-line no-param-reassign
118+
}
119+
120+
const formattedMessage = this.formatMessage(record);
121+
122+
if (record.error && typeof record.error_text === 'undefined') {
123+
record.error_text = this.formatError(record.error); // eslint-disable-line no-param-reassign
124+
}
125+
126+
if (record.error_text) {
127+
const separator = formattedMessage.endsWith('\n') ? '' : '\n';
128+
return `${formattedMessage}${separator}${record.error_text}`;
129+
}
130+
131+
return formattedMessage;
132+
}
40133
}
41134

42135
export default Formatter;

packages/apidom-logging/src/LogRecord.ts

+11-2
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,16 @@ const startTime: number = Date.now();
66
export interface LogRecordInstance<T extends Error = Error> {
77
readonly name: string;
88
readonly message: string;
9+
readonly created: number;
10+
readonly msecs: number;
11+
readonly relativeCreated: number;
12+
asctime?: string;
913
readonly levelname: string;
1014
readonly levelno: number;
1115
readonly process?: number;
1216
readonly processName?: string;
1317
readonly error?: T;
18+
error_text?: string;
1419
[key: string]: unknown;
1520
}
1621

@@ -39,12 +44,16 @@ class LogRecord<T extends Error = Error> implements LogRecordInstance<T> {
3944

4045
public readonly relativeCreated: number;
4146

47+
public asctime?: string;
48+
4249
public readonly process?: number;
4350

4451
public readonly processName?: string;
4552

4653
public readonly error?: T;
4754

55+
public error_text?: string;
56+
4857
[key: string]: unknown;
4958

5059
constructor(
@@ -61,8 +70,8 @@ class LogRecord<T extends Error = Error> implements LogRecordInstance<T> {
6170
this.levelname = getLevelName(level);
6271
this.message = message;
6372
this.error = error;
64-
this.created = Math.floor(created / 1000);
65-
this.msecs = created - this.created * 1000;
73+
this.created = created;
74+
this.msecs = created % 1000;
6675
this.relativeCreated = created - startTime;
6776

6877
if (globalThis.process?.pid) {

packages/apidom-logging/src/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@ export const { CRITICAL, ERROR, WARNING, INFO, DEBUG, NOTSET } = LoggingLevel;
44
export { getLevelName, getLevelNamesMapping, addLevelName } from './LoggingLevel';
55
export { getLogRecordClass, setLogRecordClass, default as LogRecord } from './LogRecord';
66
export { default as Filter } from './Filter';
7+
export { default as Formatter } from './Formatter';

0 commit comments

Comments
 (0)