From b8b960d31bb20da5c1224f9eb8897667ccd67ca8 Mon Sep 17 00:00:00 2001 From: Hong Minhee Date: Tue, 24 Sep 2024 11:13:23 +0900 Subject: [PATCH] `LogRecord.rawMessage` property Close https://github.com/dahlia/logtape/issues/17 --- CHANGES.md | 3 +++ logtape/config.test.ts | 4 ++++ logtape/fixtures.ts | 1 + logtape/logger.test.ts | 22 ++++++++++++++++++++++ logtape/logger.ts | 33 ++++++++++++++++++++++++++------- logtape/record.ts | 15 +++++++++++++++ 6 files changed, 71 insertions(+), 7 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 36465e6..9cdae6e 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -17,6 +17,8 @@ To be released. However, if a property with exactly the same name exists, it will be prioritized over space-trimmed properties. [[#16]] + - Added `LogRecord.rawMessage` property. [[#17]] + - Built-in text formatters now can be customized with a `TextFormatterOptions` object. [[#13]] @@ -31,6 +33,7 @@ To be released. [#13]: https://github.com/dahlia/logtape/issues/13 [#15]: https://github.com/dahlia/logtape/issues/15 [#16]: https://github.com/dahlia/logtape/issues/16 +[#17]: https://github.com/dahlia/logtape/issues/17 Version 0.5.2 diff --git a/logtape/config.test.ts b/logtape/config.test.ts index b8e3f55..4b278a4 100644 --- a/logtape/config.test.ts +++ b/logtape/config.test.ts @@ -79,6 +79,7 @@ Deno.test("configure()", async (t) => { level: "warning", category: ["my-app", "foo"], message: ["logged"], + rawMessage: "logged", properties: {}, timestamp: bLogs[0].timestamp, }, @@ -90,6 +91,7 @@ Deno.test("configure()", async (t) => { level: "info", category: ["my-app", "bar"], message: ["logged"], + rawMessage: "logged", properties: {}, timestamp: cLogs[0].timestamp, }, @@ -99,6 +101,7 @@ Deno.test("configure()", async (t) => { level: "warning", category: ["my-app", "foo"], message: ["logged"], + rawMessage: "logged", properties: {}, timestamp: bLogs[0].timestamp, }, @@ -108,6 +111,7 @@ Deno.test("configure()", async (t) => { level: "info", category: ["my-app", "bar"], message: ["logged"], + rawMessage: "logged", properties: {}, timestamp: cLogs[0].timestamp, }, diff --git a/logtape/fixtures.ts b/logtape/fixtures.ts index c936172..2801790 100644 --- a/logtape/fixtures.ts +++ b/logtape/fixtures.ts @@ -4,6 +4,7 @@ export const info: LogRecord = { level: "info", category: ["my-app", "junk"], message: ["Hello, ", 123, " & ", 456, "!"], + rawMessage: "Hello, {a} & {b}!", timestamp: 1700000000000, properties: {}, }; diff --git a/logtape/logger.test.ts b/logtape/logger.test.ts index eb105bc..6b31624 100644 --- a/logtape/logger.test.ts +++ b/logtape/logger.test.ts @@ -16,6 +16,10 @@ import { import type { LogRecord } from "./record.ts"; import type { Sink } from "./sink.ts"; +function templateLiteral(tpl: TemplateStringsArray, ..._: unknown[]) { + return tpl; +} + Deno.test("getLogger()", () => { assertEquals(getLogger().category, []); assertStrictEquals(getLogger(), getLogger()); @@ -215,6 +219,7 @@ Deno.test("LoggerImpl.log()", async (t) => { category: ["foo"], level: "info", message: ["Hello, ", 123, "!"], + rawMessage: "Hello, {foo}!", timestamp: logs[0].timestamp, properties: { foo: 123 }, }, @@ -241,6 +246,7 @@ Deno.test("LoggerImpl.log()", async (t) => { category: ["foo"], level: "error", message: ["Hello, ", 123, "!"], + rawMessage: "Hello, {foo}!", timestamp: logs[0].timestamp, properties: { foo: 123 }, }, @@ -278,6 +284,7 @@ Deno.test("LoggerImpl.logLazily()", async (t) => { category: ["foo"], level: "error", message: ["Hello, ", 123, "!"], + rawMessage: templateLiteral`Hello, ${null}!`, timestamp: logs[0].timestamp, properties: {}, }, @@ -310,6 +317,7 @@ Deno.test("LoggerImpl.logTemplate()", async (t) => { category: ["foo"], level: "info", message: ["Hello, ", 123, "!"], + rawMessage: templateLiteral`Hello, ${null}!`, timestamp: logs[0].timestamp, properties: {}, }, @@ -338,6 +346,7 @@ Deno.test("LoggerCtx.log()", async (t) => { category: ["foo"], level: "info", message: ["Hello, ", 1, " ", 2, " ", 3, "!"], + rawMessage: "Hello, {a} {b} {c}!", timestamp: logs[0].timestamp, properties: { a: 1, b: 2, c: 3 }, }, @@ -364,6 +373,7 @@ Deno.test("LoggerCtx.log()", async (t) => { category: ["foo"], level: "error", message: ["Hello, ", 1, " ", 2, " ", 3, "!"], + rawMessage: "Hello, {a} {b} {c}!", timestamp: logs[0].timestamp, properties: { a: 1, b: 2, c: 3 }, }, @@ -402,6 +412,7 @@ Deno.test("LoggerCtx.logLazily()", async (t) => { category: ["foo"], level: "error", message: ["Hello, ", 123, "!"], + rawMessage: templateLiteral`Hello, ${null}!`, timestamp: logs[0].timestamp, properties: { a: 1, b: 2 }, }, @@ -435,6 +446,7 @@ Deno.test("LoggerCtx.logTemplate()", async (t) => { category: ["foo"], level: "info", message: ["Hello, ", 123, "!"], + rawMessage: templateLiteral`Hello, ${null}!`, timestamp: logs[0].timestamp, properties: { a: 1, b: 2 }, }, @@ -469,6 +481,7 @@ for (const method of methods) { category: ["foo"], level: method === "warn" ? "warning" : method, message: ["Hello, ", 123, "!"], + rawMessage: templateLiteral`Hello, ${null}!`, timestamp: logs[0].timestamp, properties: {}, }, @@ -489,6 +502,7 @@ for (const method of methods) { category: ["foo"], level: method === "warn" ? "warning" : method, message: ["Hello, ", 123, "!"], + rawMessage: templateLiteral`Hello, ${null}!`, timestamp: logs[0].timestamp, properties: { a: 1, b: 2 }, }, @@ -512,6 +526,7 @@ for (const method of methods) { category: ["foo"], level: method === "warn" ? "warning" : method, message: ["Hello, ", 123, "!"], + rawMessage: templateLiteral`Hello, ${null}!`, timestamp: logs[0].timestamp, properties: {}, }, @@ -528,6 +543,7 @@ for (const method of methods) { category: ["foo"], level: method === "warn" ? "warning" : method, message: ["Hello, ", 123, "!"], + rawMessage: templateLiteral`Hello, ${null}!`, timestamp: logs[0].timestamp, properties: { a: 1, b: 2 }, }, @@ -551,6 +567,7 @@ for (const method of methods) { category: ["foo"], level: method === "warn" ? "warning" : method, message: ["Hello, ", 123, "!"], + rawMessage: "Hello, {foo}!", timestamp: logs[0].timestamp, properties: { foo: 123 }, }, @@ -565,6 +582,7 @@ for (const method of methods) { category: ["foo"], level: method === "warn" ? "warning" : method, message: ["Hello, world!"], + rawMessage: "Hello, world!", timestamp: logs[0].timestamp, properties: {}, }, @@ -579,6 +597,7 @@ for (const method of methods) { category: ["foo"], level: method === "warn" ? "warning" : method, message: ["Hello, ", 1, " ", 2, " ", 3, "!"], + rawMessage: "Hello, {a} {b} {c}!", timestamp: logs[0].timestamp, properties: { a: 1, b: 2, c: 3 }, }, @@ -591,6 +610,7 @@ for (const method of methods) { category: ["foo"], level: method === "warn" ? "warning" : method, message: ["Hello, world!"], + rawMessage: "Hello, world!", timestamp: logs[0].timestamp, properties: { a: 1, b: 2 }, }, @@ -614,6 +634,7 @@ for (const method of methods) { category: ["foo"], level: method === "warn" ? "warning" : method, message: ["Hello, ", 123, "!"], + rawMessage: "Hello, {foo}!", timestamp: logs[0].timestamp, properties: { foo: 123 }, }, @@ -632,6 +653,7 @@ for (const method of methods) { category: ["foo"], level: method === "warn" ? "warning" : method, message: ["Hello, ", 1, " ", 2, " ", 3, "!"], + rawMessage: "Hello, {a} {b} {c}!", timestamp: logs[0].timestamp, properties: { a: 1, b: 2, c: 3 }, }, diff --git a/logtape/logger.ts b/logtape/logger.ts index be38926..61fd380 100644 --- a/logtape/logger.ts +++ b/logtape/logger.ts @@ -128,6 +128,7 @@ export interface Logger { * ``` * * @param callback A callback that returns the message template prefix. + * @throws {TypeError} If no log record was made inside the callback. */ debug(callback: LogCallback): void; @@ -182,6 +183,7 @@ export interface Logger { * ``` * * @param callback A callback that returns the message template prefix. + * @throws {TypeError} If no log record was made inside the callback. */ info(callback: LogCallback): void; @@ -236,6 +238,7 @@ export interface Logger { * ``` * * @param callback A callback that returns the message template prefix. + * @throws {TypeError} If no log record was made inside the callback. */ warn(callback: LogCallback): void; @@ -290,6 +293,7 @@ export interface Logger { * ``` * * @param callback A callback that returns the message template prefix. + * @throws {TypeError} If no log record was made inside the callback. */ error(callback: LogCallback): void; @@ -344,6 +348,7 @@ export interface Logger { * ``` * * @param callback A callback that returns the message template prefix. + * @throws {TypeError} If no log record was made inside the callback. */ fatal(callback: LogCallback): void; } @@ -517,7 +522,7 @@ export class LoggerImpl implements Logger { log( level: LogLevel, - message: string, + rawMessage: string, properties: Record | (() => Record), bypassSinks?: Set, ): void { @@ -528,8 +533,9 @@ export class LoggerImpl implements Logger { level, timestamp: Date.now(), get message() { - return parseMessageTemplate(message, this.properties); + return parseMessageTemplate(rawMessage, this.properties); }, + rawMessage, get properties() { if (cachedProps == null) cachedProps = properties(); return cachedProps; @@ -539,7 +545,8 @@ export class LoggerImpl implements Logger { category: this.category, level, timestamp: Date.now(), - message: parseMessageTemplate(message, properties), + message: parseMessageTemplate(rawMessage, properties), + rawMessage, properties, }; this.emit(record, bypassSinks); @@ -550,15 +557,26 @@ export class LoggerImpl implements Logger { callback: LogCallback, properties: Record = {}, ): void { + let rawMessage: TemplateStringsArray | undefined = undefined; let msg: unknown[] | undefined = undefined; + function realizeMessage(): [unknown[], TemplateStringsArray] { + if (msg == null || rawMessage == null) { + msg = callback((tpl, ...values) => { + rawMessage = tpl; + return renderMessage(tpl, values); + }); + if (rawMessage == null) throw new TypeError("No log record was made."); + } + return [msg, rawMessage]; + } this.emit({ category: this.category, level, get message() { - if (msg == null) { - msg = callback((tpl, ...values) => renderMessage(tpl, values)); - } - return msg; + return realizeMessage()[0]; + }, + get rawMessage() { + return realizeMessage()[1]; }, timestamp: Date.now(), properties, @@ -575,6 +593,7 @@ export class LoggerImpl implements Logger { category: this.category, level, message: renderMessage(messageTemplate, values), + rawMessage: messageTemplate, timestamp: Date.now(), properties, }); diff --git a/logtape/record.ts b/logtape/record.ts index ac36f35..110da71 100644 --- a/logtape/record.ts +++ b/logtape/record.ts @@ -22,6 +22,21 @@ export interface LogRecord { */ readonly message: readonly unknown[]; + /** + * The raw log message. This is the original message template without any + * further processing. It can be either: + * + * - A string without any substitutions if the log record was created with + * a method call syntax, e.g., "Hello, {name}!" for + * `logger.info("Hello, {name}!", { name })`. + * - A template string array if the log record was created with a tagged + * template literal syntax, e.g., `["Hello, ", "!"]` for + * ``logger.info`Hello, ${name}!```. + * + * @since 0.6.0 + */ + readonly rawMessage: string | TemplateStringsArray; + /** * The timestamp of the log record in milliseconds since the Unix epoch. */