Skip to content

Commit 8be7607

Browse files
feat(logger): extend logger to accept many arguments
Before this commit a logger method like info could only accept one argument of type string. A logger method can now accept multiple aguments which can have any type.
1 parent 28a6849 commit 8be7607

File tree

5 files changed

+126
-54
lines changed

5 files changed

+126
-54
lines changed

README.md

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -215,12 +215,12 @@ Interface that every logger implements.
215215

216216
```typescript
217217
interface ILogger {
218-
verbose(msg: string): void;
219-
debug(msg: string): void;
220-
info(msg: string): void;
221-
warning(msg: string): void;
222-
error(err: string | Error): void;
223-
critical(err: string | Error): void;
218+
verbose(...args: unknown[]): void;
219+
debug(...args: unknown[]): void;
220+
info(...args: unknown[]): void;
221+
warning(...args: unknown[]): void;
222+
error(...args: unknown[]): void;
223+
critical(...args: unknown[]): void;
224224

225225
withContext(context: string): ILogger;
226226
addHandler(
@@ -260,7 +260,9 @@ interface ILogRecord {
260260
readonly levelName: keyof typeof LogLevel;
261261
/** Context used for this record. */
262262
readonly context: string;
263-
/** Stored message. */
263+
/** Arguments that were passed to the logger. */
264+
readonly args: unknown[];
265+
/** Arguments formatted as a string. */
264266
readonly message: string;
265267
/** Timestamp at which the log record was created. */
266268
readonly date: Date;

src/log-record.test.ts

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { LogLevel } from "./log-level";
33

44
describe("LogRecord", () => {
55
it("should construct a valid LogRecord", () => {
6-
const record = new LogRecord("test message", {
6+
const record = new LogRecord(["test message"], {
77
level: LogLevel.INFO,
88
context: "Test",
99
metadata: { key1: 123 },
@@ -13,19 +13,59 @@ describe("LogRecord", () => {
1313
expect(record.levelName).toBe("INFO");
1414
expect(record.context).toBe("Test");
1515
expect(record.message).toBe("test message");
16+
expect(record.args).toStrictEqual(["test message"]);
1617
});
1718

1819
it("should include the current date", () => {
1920
const date = new Date("2020-04-01T12:15:32.000Z");
2021
jest.useFakeTimers("modern");
2122
jest.setSystemTime(date);
2223

23-
const record = new LogRecord("test message", {
24+
const record = new LogRecord(["test message"], {
2425
level: LogLevel.INFO,
2526
context: "Test",
2627
metadata: { key1: 123 },
2728
});
2829

2930
expect(record.date).toStrictEqual(date);
3031
});
32+
33+
const cases: [args: unknown[], parsed: string][] = [
34+
[[], ""],
35+
[["log message"], "log message"],
36+
[["message1", "message2"], "message1 message2"],
37+
[[true, false], "true false"],
38+
[[1, 2], "1 2"],
39+
[[(a: number, b: number) => a + b], "[Function (anonymous)]"],
40+
[[[]], "[]"],
41+
[[{}], "{}"],
42+
[[{ key: 1, key2: 2 }], '{"key":1,"key2":2}'],
43+
[[null], "null"],
44+
[[undefined, 1], "undefined 1"],
45+
[
46+
[
47+
function sum(a: number, b: number) {
48+
return a + b;
49+
},
50+
],
51+
"[Function sum]",
52+
],
53+
[
54+
[
55+
[1, 2, 3],
56+
[4, 5, 6],
57+
],
58+
"[1,2,3] [4,5,6]",
59+
],
60+
];
61+
62+
it.each(cases)('Given arguments "%s" are parsed to "%s"', (args, parsed) => {
63+
const record = new LogRecord(args, {
64+
context: "default",
65+
level: LogLevel.INFO,
66+
metadata: {},
67+
});
68+
69+
expect(record.message).toBe(parsed);
70+
});
3171
});

src/log-record.ts

Lines changed: 49 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@ export interface ILogRecord {
1010
readonly levelName: keyof typeof LogLevel;
1111
/** Context used for this record. */
1212
readonly context: string;
13-
/** Stored message. */
13+
/** Arguments that were passed to the logger. */
14+
readonly args: unknown[];
15+
/** Arguments formatted as a string. */
1416
readonly message: string;
1517
/** Timestamp at which the log record was created. */
1618
readonly date: Date;
@@ -32,16 +34,60 @@ export class LogRecord implements ILogRecord {
3234
readonly level: LogLevel;
3335
readonly levelName: keyof typeof LogLevel;
3436
readonly context: string;
37+
readonly args: unknown[];
3538
readonly message: string;
3639
readonly date: Date;
3740
readonly metadata: Record<string, unknown>;
3841

39-
constructor(message: string, config: LogRecordConfig) {
42+
constructor(args: unknown[], config: LogRecordConfig) {
4043
this.level = config.level;
4144
this.levelName = getLogLevelName(config.level);
4245
this.context = config.context;
43-
this.message = message;
46+
this.args = args;
47+
this.message = argsToString(args);
4448
this.date = new Date();
4549
this.metadata = config.metadata;
4650
}
4751
}
52+
53+
/**@hidden */
54+
function argsToString(args: unknown[]): string {
55+
return args.reduce(
56+
(prev: string, curr) =>
57+
prev === "" ? asString(curr) : `${prev} ${asString(curr)}`,
58+
""
59+
);
60+
}
61+
62+
/**@hidden */
63+
function asString(data: unknown): string {
64+
if (typeof data === "string") {
65+
return data;
66+
}
67+
68+
if (
69+
data === null ||
70+
typeof data === "number" ||
71+
typeof data === "bigint" ||
72+
typeof data === "boolean" ||
73+
typeof data === "undefined" ||
74+
typeof data === "symbol"
75+
) {
76+
return String(data);
77+
}
78+
79+
if (data instanceof Error) {
80+
return data.stack ?? data.message;
81+
}
82+
83+
if (typeof data === "object") {
84+
return JSON.stringify(data);
85+
}
86+
87+
if (typeof data === "function") {
88+
const funcName = data.name !== "" ? data.name : "(anonymous)";
89+
return `[Function ${funcName}]`;
90+
}
91+
92+
return "undefined";
93+
}

src/logger.test.ts

Lines changed: 6 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -147,35 +147,25 @@ describe("Logger", () => {
147147
});
148148

149149
type Method = "verbose" | "debug" | "info" | "warning" | "error" | "critical";
150-
const tests: [Method, string | Error, LogLevel][] = [
150+
const tests: [level: Method, msg: string, level: LogLevel][] = [
151151
["verbose", "test message", LogLevel.VERBOSE],
152152
["debug", "test message", LogLevel.DEBUG],
153153
["info", "test message", LogLevel.INFO],
154154
["warning", "test message", LogLevel.WARNING],
155155
["error", "test message", LogLevel.ERROR],
156-
["error", new Error("test Error"), LogLevel.ERROR],
157156
["critical", "test message", LogLevel.CRITICAL],
158-
["critical", new Error("test Error"), LogLevel.CRITICAL],
159157
];
160158

161159
describe.each(tests)("%s", (method, msg, level) => {
162-
it(`creates a log record with the given message, metadata and level ${level}`, () => {
160+
it(`creates a log record with the given args, metadata and level ${level}`, () => {
163161
const handler = new TestHandler();
164162
const logger = new Logger()
165163
.addMetadata({ key1: "Test" })
166164
.addHandler(handler);
167165

168-
if (typeof msg === "string") {
169-
logger[method](msg as string);
170-
171-
expect(handler.records[0].message).toBe(msg);
172-
} else {
173-
// We have an error object as msg which is only for error or critical.
174-
logger[method as "error" | "critical"](msg);
175-
176-
expect(handler.records[0].message).toBe(msg.message);
177-
}
166+
logger[method](msg);
178167

168+
expect(handler.records[0].args).toStrictEqual([msg]);
179169
expect(handler.records[0].level).toBe(level);
180170
expect(handler.records[0].metadata).toStrictEqual({ key1: "Test" });
181171
});
@@ -185,8 +175,7 @@ describe("Logger", () => {
185175
const handler2 = new TestHandler();
186176
const logger = new Logger().addHandler(handler1).addHandler(handler2);
187177

188-
// We don't care if msg is an error in error and critical case.
189-
logger[method](msg as string);
178+
logger[method](msg);
190179

191180
expect(handler1.records).toHaveLength(1);
192181
expect(handler2.records).toHaveLength(1);
@@ -197,8 +186,7 @@ describe("Logger", () => {
197186
const handler2 = new TestHandler();
198187
const logger = new Logger().addHandler(handler1).addHandler(handler2);
199188

200-
// We don't care if msg is an error in error and critical case.
201-
logger[method](msg as string);
189+
logger[method](msg);
202190

203191
expect(handler1.records[0]).toBe(handler2.records[0]);
204192
});

src/logger.ts

Lines changed: 20 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -4,22 +4,22 @@ import { LogRecord } from "./log-record";
44

55
export interface ILogger {
66
/** Sends a log record with the level VERBOSE to all handlers.*/
7-
verbose(msg: string): void;
7+
verbose(...args: unknown[]): void;
88

99
/** Sends a log record with the level DEBUG to all handlers.*/
10-
debug(msg: string): void;
10+
debug(...args: unknown[]): void;
1111

1212
/** Sends a log record with the level INFO to all handlers.*/
13-
info(msg: string): void;
13+
info(...args: unknown[]): void;
1414

1515
/** Sends a log record with the level WARNING to all handlers.*/
16-
warning(msg: string): void;
16+
warning(...args: unknown[]): void;
1717

1818
/** Sends a log record with the level ERROR to all handlers.*/
19-
error(err: string | Error): void;
19+
error(...args: unknown[]): void;
2020

2121
/** Sends a log record with the level CRITICAL to all handlers.*/
22-
critical(err: string | Error): void;
22+
critical(...args: unknown[]): void;
2323

2424
/**
2525
* Creates a new `logger` scoped to a given `context`. All current handlers and metadata
@@ -111,8 +111,8 @@ export class Logger implements ILogger {
111111
return this;
112112
}
113113

114-
private notifyHandlers(level: LogLevel, message: string) {
115-
const record = new LogRecord(message, {
114+
private notifyHandlers(level: LogLevel, args: unknown[]) {
115+
const record = new LogRecord(args, {
116116
level,
117117
context: this._context,
118118
metadata: this._metadata,
@@ -122,31 +122,27 @@ export class Logger implements ILogger {
122122
}
123123
}
124124

125-
verbose(msg: string): void {
126-
this.notifyHandlers(LogLevel.VERBOSE, msg);
125+
verbose(...args: unknown[]): void {
126+
this.notifyHandlers(LogLevel.VERBOSE, args);
127127
}
128128

129-
debug(msg: string): void {
130-
this.notifyHandlers(LogLevel.DEBUG, msg);
129+
debug(...args: unknown[]): void {
130+
this.notifyHandlers(LogLevel.DEBUG, args);
131131
}
132132

133-
info(msg: string): void {
134-
this.notifyHandlers(LogLevel.INFO, msg);
133+
info(...args: unknown[]): void {
134+
this.notifyHandlers(LogLevel.INFO, args);
135135
}
136136

137-
warning(msg: string): void {
138-
this.notifyHandlers(LogLevel.WARNING, msg);
137+
warning(...args: unknown[]): void {
138+
this.notifyHandlers(LogLevel.WARNING, args);
139139
}
140140

141-
error(err: string | Error): void {
142-
const message = typeof err === "string" ? err : err.message;
143-
144-
this.notifyHandlers(LogLevel.ERROR, message);
141+
error(...args: unknown[]): void {
142+
this.notifyHandlers(LogLevel.ERROR, args);
145143
}
146144

147-
critical(err: string | Error): void {
148-
const message = typeof err === "string" ? err : err.message;
149-
150-
this.notifyHandlers(LogLevel.CRITICAL, message);
145+
critical(...args: unknown[]): void {
146+
this.notifyHandlers(LogLevel.CRITICAL, args);
151147
}
152148
}

0 commit comments

Comments
 (0)