From 24252bf966b1b1eedacffff0143c063331d1ae6f Mon Sep 17 00:00:00 2001 From: Hong Minhee Date: Wed, 20 Nov 2024 13:41:25 +0900 Subject: [PATCH] Renewed API to configure lowest level of loggers Close https://github.com/dahlia/logtape/issues/26 --- CHANGES.md | 10 ++++ docs/bun.lockb | Bin 121567 -> 121581 bytes docs/manual/categories.md | 9 ++-- docs/manual/config.md | 8 +-- docs/manual/filters.md | 70 +++++++++++++++++++++++++- docs/manual/levels.md | 101 ++++++++++++++++++++++++++++++++++++-- docs/manual/library.md | 4 +- docs/manual/sinks.md | 2 +- docs/manual/start.md | 2 +- logtape/config.test.ts | 6 ++- logtape/config.ts | 29 ++++++++++- logtape/context.test.ts | 8 +-- logtape/level.test.ts | 13 ++++- logtape/level.ts | 24 ++++++++- logtape/logger.test.ts | 88 +++++++++++++++++++++++---------- logtape/logger.ts | 12 ++++- logtape/mod.ts | 7 ++- 17 files changed, 338 insertions(+), 55 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 6efd31a..f501aa2 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -8,6 +8,16 @@ Version 0.8.0 To be released. + - Renewed the API to configure the lowest severity level of loggers. [[#26]] + + - Added `LoggerConfig.lowestLevel` property. + - Deprecated `LoggerConfig.level` property in favor of + `LoggerConfig.lowestLevel`. + + - Added `compareLogLevel()` function. + +[#26]: https://github.com/dahlia/logtape/issues/26 + Version 0.7.1 ------------- diff --git a/docs/bun.lockb b/docs/bun.lockb index 7df0a6312b4968f217c0a3cfb2e3da158990e467..f65bb1f3e3c3810f05430f2fea56480dddc365ee 100755 GIT binary patch delta 11866 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 YWBri@utxBq?_50W#qNHwl(~!kA7i0q$^ZZW delta 11830 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 100; + }, + }, + loggers: [ + { + category: ["my-app"], + sinks: ["console"], + filters: ["hasUserInfo"], // [!code highlight] + }, + { + category: ["my-app", "database"], + sinks: [], + filters: ["tooSlow"], // [!code highlight] + } + ] +}); +~~~~ + +In this example, any log messages under the `["my-app"]` category including +the `["my-app", "database"]` category are passed to the console sink only if +they have the `userInfo` property. In addition, the log messages under the +`["my-app", "database"]` category are passed to the console sink only if they +have the `elapsed` with a value greater than or equal to 100 milliseconds. + + Level filter ------------ @@ -58,7 +106,25 @@ import { configure, getLevelFilter } from "@logtape/logtape"; await configure({ filters: { - infoOrHigher: getLevelFilter("info"), // [!code highlight] + infoAndAbove: getLevelFilter("info"), // [!code highlight] + }, + // Omitted for brevity +}); +~~~~ + +The `~Config.filters` takes a map of filter names to `FilterLike`, instead of +just `Filter`, where `FilterLike` is either a `Filter` function or a severity +level string. The severity level string will be resolved to a `Filter` that +filters log records with the specified severity level and above. Hence, you +can simplify the above example as follows: + +~~~~ typescript twoslash +// @noErrors: 2345 +import { configure } from "@logtape/logtape"; +// ---cut-before--- +await configure({ + filters: { + infoAndAbove: "info", // [!code highlight] }, // Omitted for brevity }); diff --git a/docs/manual/levels.md b/docs/manual/levels.md index a0697c6..bdcc99b 100644 --- a/docs/manual/levels.md +++ b/docs/manual/levels.md @@ -112,23 +112,33 @@ When deciding which level to use, consider: Configuring severity levels --------------------------- +*This API is available since LogTape 0.8.0.* + You can control which severity levels are logged in different parts of your application. For example: -~~~~ typescript twoslash -// @noErrors: 2345 +~~~~ typescript{6,11} twoslash +// @noErrors: 2345 2353 import { configure } from "@logtape/logtape"; // ---cut-before--- await configure({ +// ---cut-start--- + sinks: { + console(record) { }, + file(record) { }, + }, +// ---cut-end--- // ... other configuration ... loggers: [ { category: ["app"], - level: "info", // This will log info and above + lowestLevel: "info", // This will log info and above + sinks: ["console"], }, { category: ["app", "database"], - level: "debug", // This will log everything for database operations + lowestLevel: "debug", // This will log everything for database operations + sinks: ["file"], } ] }); @@ -137,6 +147,89 @@ await configure({ This configuration will log all levels from `"info"` up for most of the app, but will include `"debug"` logs for database operations. +> [!NOTE] +> The `~LoggerConfig.lowestLevel` is applied to the logger itself, not to its +> sinks. In other words, the `~LoggerConfig.lowestLevel` property determines +> which log records are emitted by the logger. For example, if the parent +> logger has a `~LoggerConfig.lowestLevel` of `"debug"` with a sink `"console"`, +> and the child logger has a `~LoggerConfig.lowestLevel` of `"info"`, +> the child logger still won't emit `"debug"` records to the `"console"` sink. + +The `~LoggerConfig.lowestLevel` property does not inherit from parent loggers, +but it is `"debug"` by default for all loggers. If you want to make child +loggers inherit the severity level from their parent logger, you can use the +`~LoggerConfig.filters` option instead. + +If you want make child loggers inherit the severity level from their parent +logger, you can use the `~LoggerConfig.filters` option instead: + +~~~~ typescript{4,9,13} twoslash +// @noErrors: 2345 2353 +import { configure } from "@logtape/logtape"; +// ---cut-before--- +await configure({ + // ... other configuration ... + filters: { + infoAndAbove: "info", + }, + loggers: [ + { + category: ["app"], + filters: ["infoAndAbove"], // This will log info and above + }, + { + category: ["app", "database"], + // This also logs info and above, because it inherits from the parent logger + } + ] +}); +~~~~ + +In this example, the database logger will inherit the `aboveAndInfo` filter from +the parent logger, so it will log all levels from `"info"` up. + +> [!TIP] +> The `~LoggerConfig.filters` option takes a map of filter names to +> `FilterLike`, where `FilterLike` is either a `Filter` function or a severity +> level string. The severity level string will be resolved to a `Filter` that +> filters log records with the specified severity level and above. +> +> See also the [*Level filter* section](./filters.md#level-filter). + + +Comparing two severity levels +----------------------------- + +*This API is available since LogTape 0.8.0.* + +You can compare two severity levels to see which one is more severe by using +the `compareLogLevel()` function. Since this function returns a number where +negative means the first argument is less severe, zero means they are equal, +and positive means the first argument is more severe, you can use it with +[`Array.sort()`] or [`Array.toSorted()`] to sort severity levels: + +~~~~ typescript twoslash +// @noErrors: 2724 +import { type LogLevel, compareLogLevel } from "@logtape/logtape"; + +const levels: LogLevel[] = ["info", "debug", "error", "warning", "fatal"]; +levels.sort(compareLogLevel); +for (const level of levels) console.log(level); +~~~~ + +The above code will output: + +~~~~ +debug +info +warning +error +fatal +~~~~ + +[`Array.sort()`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort +[`Array.toSorted()`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/toSorted + Best practices -------------- diff --git a/docs/manual/library.md b/docs/manual/library.md index d3ac878..1a20216 100644 --- a/docs/manual/library.md +++ b/docs/manual/library.md @@ -134,12 +134,12 @@ await configure({ loggers: [ { category: ["my-awesome-lib"], - level: "info", + lowestLevel: "info", sinks: ["console", "file"] }, { category: ["my-awesome-lib", "database"], - level: "debug", + lowestLevel: "debug", sinks: ["file"], filters: ["excludeDebug"] } diff --git a/docs/manual/sinks.md b/docs/manual/sinks.md index 51668f5..f09e30d 100644 --- a/docs/manual/sinks.md +++ b/docs/manual/sinks.md @@ -288,7 +288,7 @@ await configure({ }, filters: {}, loggers: [ - { category: [], sinks: ["otel"], level: "debug" }, + { category: [], sinks: ["otel"], lowestLevel: "debug" }, ], }); ~~~~ diff --git a/docs/manual/start.md b/docs/manual/start.md index 33401b9..6f56861 100644 --- a/docs/manual/start.md +++ b/docs/manual/start.md @@ -13,7 +13,7 @@ import { configure, getConsoleSink } from "@logtape/logtape"; await configure({ sinks: { console: getConsoleSink() }, loggers: [ - { category: "my-app", level: "debug", sinks: ["console"] } + { category: "my-app", lowestLevel: "debug", sinks: ["console"] } ] }); ~~~~ diff --git a/logtape/config.test.ts b/logtape/config.test.ts index 4b278a4..ce057ae 100644 --- a/logtape/config.test.ts +++ b/logtape/config.test.ts @@ -54,7 +54,8 @@ Deno.test("configure()", async (t) => { category: ["my-app", "bar"], sinks: ["c"], filters: ["debug"], - level: "info", + level: "info", // deprecated + lowestLevel: "info", }, ], }; @@ -63,11 +64,14 @@ Deno.test("configure()", async (t) => { const logger = LoggerImpl.getLogger("my-app"); assertEquals(logger.sinks, [a]); assertEquals(logger.filters, [x]); + assertEquals(logger.lowestLevel, "debug"); const foo = LoggerImpl.getLogger(["my-app", "foo"]); assertEquals(foo.sinks, [b]); assertEquals(foo.filters, [y]); + assertEquals(foo.lowestLevel, "debug"); const bar = LoggerImpl.getLogger(["my-app", "bar"]); assertEquals(bar.sinks, [c]); + assertEquals(bar.lowestLevel, "info"); bar.debug("ignored"); assertEquals(aLogs, []); assertEquals(bLogs, []); diff --git a/logtape/config.ts b/logtape/config.ts index c0ec7dc..f0e6c2e 100644 --- a/logtape/config.ts +++ b/logtape/config.ts @@ -73,8 +73,17 @@ export interface LoggerConfig< /** * The log level to filter by. If `null`, the logger will reject all * records. + * @deprecated Use `filters` instead for backward compatibility, or use + * `lowestLevel` for less-misleading behavior. */ level?: LogLevel | null; + + /** + * The lowest log level to accept. If `null`, the logger will reject all + * records. + * @since 0.8.0 + */ + lowestLevel?: LogLevel | null; } /** @@ -151,6 +160,7 @@ export async function configure< currentConfig = config; let metaConfigured = false; + let levelUsed = false; for (const cfg of config.loggers) { if ( @@ -172,7 +182,13 @@ export async function configure< logger.sinks.push(sink); } logger.parentSinks = cfg.parentSinks ?? "inherit"; - if (cfg.level !== undefined) logger.filters.push(toFilter(cfg.level)); + if (cfg.lowestLevel !== undefined) { + logger.lowestLevel = cfg.lowestLevel; + } + if (cfg.level !== undefined) { + levelUsed = true; + logger.filters.push(toFilter(cfg.level)); + } for (const filterId of cfg.filters ?? []) { const filter = config.filters?.[filterId]; if (filter === undefined) { @@ -221,9 +237,18 @@ export async function configure< "It's recommended to configure the meta logger with a separate sink " + "so that you can easily notice if logging itself fails or is " + "misconfigured. To turn off this message, configure the meta logger " + - "with higher log levels than {dismissLevel}.", + "with higher log levels than {dismissLevel}. See also " + + ".", { metaLoggerCategory: ["logtape", "meta"], dismissLevel: "info" }, ); + + if (levelUsed) { + meta.warn( + "The level option is deprecated in favor of lowestLevel option. " + + "Please update your configuration. See also " + + ".", + ); + } } /** diff --git a/logtape/context.test.ts b/logtape/context.test.ts index 6553838..1bc7ab3 100644 --- a/logtape/context.test.ts +++ b/logtape/context.test.ts @@ -15,8 +15,8 @@ Deno.test("withContext()", async (t) => { buffer: buffer.push.bind(buffer), }, loggers: [ - { category: "my-app", sinks: ["buffer"], level: "debug" }, - { category: ["logtape", "meta"], sinks: [], level: "warning" }, + { category: "my-app", sinks: ["buffer"], lowestLevel: "debug" }, + { category: ["logtape", "meta"], sinks: [], lowestLevel: "warning" }, ], contextLocalStorage: new AsyncLocalStorage(), reset: true, @@ -139,11 +139,11 @@ Deno.test("withContext()", async (t) => { metaBuffer: metaBuffer.push.bind(metaBuffer), }, loggers: [ - { category: "my-app", sinks: ["buffer"], level: "debug" }, + { category: "my-app", sinks: ["buffer"], lowestLevel: "debug" }, { category: ["logtape", "meta"], sinks: ["metaBuffer"], - level: "warning", + lowestLevel: "warning", }, ], reset: true, diff --git a/logtape/level.test.ts b/logtape/level.test.ts index bf499d5..012b577 100644 --- a/logtape/level.test.ts +++ b/logtape/level.test.ts @@ -2,7 +2,12 @@ import { assert } from "@std/assert/assert"; import { assertEquals } from "@std/assert/assert-equals"; import { assertFalse } from "@std/assert/assert-false"; import { assertThrows } from "@std/assert/assert-throws"; -import { isLogLevel, parseLogLevel } from "./level.ts"; +import { + compareLogLevel, + isLogLevel, + type LogLevel, + parseLogLevel, +} from "./level.ts"; Deno.test("parseLogLevel()", () => { assertEquals(parseLogLevel("debug"), "debug"); @@ -31,3 +36,9 @@ Deno.test("isLogLevel()", () => { assertFalse(isLogLevel("DEBUG")); assertFalse(isLogLevel("invalid")); }); + +Deno.test("compareLogLevel()", () => { + const levels: LogLevel[] = ["info", "debug", "error", "warning", "fatal"]; + levels.sort(compareLogLevel); + assertEquals(levels, ["debug", "info", "warning", "error", "fatal"]); +}); diff --git a/logtape/level.ts b/logtape/level.ts index afb8438..20a1faf 100644 --- a/logtape/level.ts +++ b/logtape/level.ts @@ -1,7 +1,9 @@ +const logLevels = ["debug", "info", "warning", "error", "fatal"] as const; + /** * The severity level of a {@link LogRecord}. */ -export type LogLevel = "debug" | "info" | "warning" | "error" | "fatal"; +export type LogLevel = typeof logLevels[number]; /** * Parses a log level from a string. @@ -43,3 +45,23 @@ export function isLogLevel(level: string): level is LogLevel { return false; } } + +/** + * Compares two log levels. + * @param a The first log level. + * @param b The second log level. + * @returns A negative number if `a` is less than `b`, a positive number if `a` + * is greater than `b`, or zero if they are equal. + * @since 0.8.0 + */ +export function compareLogLevel(a: LogLevel, b: LogLevel): number { + const aIndex = logLevels.indexOf(a); + if (aIndex < 0) { + throw new TypeError(`Invalid log level: ${JSON.stringify(a)}.`); + } + const bIndex = logLevels.indexOf(b); + if (bIndex < 0) { + throw new TypeError(`Invalid log level: ${JSON.stringify(b)}.`); + } + return aIndex - bIndex; +} diff --git a/logtape/logger.test.ts b/logtape/logger.test.ts index 6b31624..992d525 100644 --- a/logtape/logger.test.ts +++ b/logtape/logger.test.ts @@ -134,17 +134,17 @@ Deno.test("LoggerImpl.emit()", async (t) => { const fooBar = foo.getChild("bar"); const fooBarBaz = fooBar.getChild("baz"); - await t.step("test", () => { - const rootRecords: LogRecord[] = []; - root.sinks.push(rootRecords.push.bind(rootRecords)); - root.filters.push(toFilter("warning")); - const fooRecords: LogRecord[] = []; - foo.sinks.push(fooRecords.push.bind(fooRecords)); - foo.filters.push(toFilter("info")); - const fooBarRecords: LogRecord[] = []; - fooBar.sinks.push(fooBarRecords.push.bind(fooBarRecords)); - fooBar.filters.push(toFilter("error")); - + const rootRecords: LogRecord[] = []; + root.sinks.push(rootRecords.push.bind(rootRecords)); + root.filters.push(toFilter("warning")); + const fooRecords: LogRecord[] = []; + foo.sinks.push(fooRecords.push.bind(fooRecords)); + foo.filters.push(toFilter("info")); + const fooBarRecords: LogRecord[] = []; + fooBar.sinks.push(fooBarRecords.push.bind(fooBarRecords)); + fooBar.filters.push(toFilter("error")); + + await t.step("filter and sink", () => { root.emit(info); assertEquals(rootRecords, []); assertEquals(fooRecords, []); @@ -171,35 +171,73 @@ Deno.test("LoggerImpl.emit()", async (t) => { assertEquals(rootRecords, [warning, info, error]); assertEquals(fooRecords, [info, error]); assertEquals(fooBarRecords, [error]); + }); + + while (rootRecords.length > 0) rootRecords.pop(); + while (fooRecords.length > 0) fooRecords.pop(); + while (fooBarRecords.length > 0) fooBarRecords.pop(); + + const errorSink: Sink = () => { + throw new Error("This is an error"); + }; + fooBarBaz.sinks.push(errorSink); - const errorSink: Sink = () => { - throw new Error("This is an error"); - }; - fooBarBaz.sinks.push(errorSink); + await t.step("error handling", () => { fooBarBaz.emit(error); - assertEquals(rootRecords.length, 5); - assertEquals(rootRecords.slice(0, 4), [warning, info, error, error]); - assertEquals(fooRecords, [info, error, error]); - assertEquals(fooBarRecords, [error, error]); - assertEquals(rootRecords[4].category, ["logtape", "meta"]); - assertEquals(rootRecords[4].level, "fatal"); - assertEquals(rootRecords[4].message, [ + assertEquals(rootRecords.length, 2); + assertEquals(rootRecords[0], error); + assertEquals(fooRecords, [error]); + assertEquals(fooBarRecords, [error]); + assertEquals(rootRecords[1].category, ["logtape", "meta"]); + assertEquals(rootRecords[1].level, "fatal"); + assertEquals(rootRecords[1].message, [ "Failed to emit a log record to sink ", errorSink, ": ", - rootRecords[4].properties.error, + rootRecords[1].properties.error, "", ]); - assertEquals(rootRecords[4].properties, { + assertEquals(rootRecords[1].properties, { record: error, sink: errorSink, - error: rootRecords[4].properties.error, + error: rootRecords[1].properties.error, }); root.sinks.push(errorSink); fooBarBaz.emit(error); }); + while (rootRecords.length > 0) rootRecords.pop(); + while (fooRecords.length > 0) fooRecords.pop(); + while (fooBarRecords.length > 0) fooBarRecords.pop(); + while (root.filters.length > 0) root.filters.pop(); + while (foo.filters.length > 0) foo.filters.pop(); + while (fooBar.filters.length > 0) fooBar.filters.pop(); + root.sinks.pop(); + + root.lowestLevel = "debug"; + foo.lowestLevel = "error"; + fooBar.lowestLevel = "info"; + + await t.step("lowestLevel", () => { + fooBar.emit(debug); + assertEquals(rootRecords, []); + assertEquals(fooRecords, []); + assertEquals(fooBarRecords, []); + + foo.emit(debug); + assertEquals(rootRecords, []); + assertEquals(fooRecords, []); + + root.emit(debug); + assertEquals(rootRecords, [debug]); + + fooBar.emit(info); + assertEquals(rootRecords, [debug, info]); + assertEquals(fooRecords, [info]); + assertEquals(fooBarRecords, [info]); + }); + await t.step("tear down", () => { root.resetDescendants(); }); diff --git a/logtape/logger.ts b/logtape/logger.ts index 69bda5e..ec31bf4 100644 --- a/logtape/logger.ts +++ b/logtape/logger.ts @@ -1,6 +1,6 @@ import type { ContextLocalStorage } from "./context.ts"; import type { Filter } from "./filter.ts"; -import type { LogLevel } from "./level.ts"; +import { compareLogLevel, type LogLevel } from "./level.ts"; import type { LogRecord } from "./record.ts"; import type { Sink } from "./sink.ts"; @@ -413,6 +413,7 @@ export class LoggerImpl implements Logger { readonly sinks: Sink[]; parentSinks: "inherit" | "override" = "inherit"; readonly filters: Filter[]; + lowestLevel: LogLevel | null = "debug"; contextLocalStorage?: ContextLocalStorage>; static getLogger(category: string | readonly string[] = []): LoggerImpl { @@ -470,6 +471,7 @@ export class LoggerImpl implements Logger { while (this.sinks.length > 0) this.sinks.shift(); this.parentSinks = "inherit"; while (this.filters.length > 0) this.filters.shift(); + this.lowestLevel = "debug"; } /** @@ -504,7 +506,13 @@ export class LoggerImpl implements Logger { } emit(record: LogRecord, bypassSinks?: Set): void { - if (!this.filter(record)) return; + if ( + this.lowestLevel === null || + compareLogLevel(record.level, this.lowestLevel) < 0 || + !this.filter(record) + ) { + return; + } for (const sink of this.getSinks()) { if (bypassSinks?.has(sink)) continue; try { diff --git a/logtape/mod.ts b/logtape/mod.ts index 6d5a50d..bc01d00 100644 --- a/logtape/mod.ts +++ b/logtape/mod.ts @@ -29,7 +29,12 @@ export { type TextFormatter, type TextFormatterOptions, } from "./formatter.ts"; -export { isLogLevel, type LogLevel, parseLogLevel } from "./level.ts"; +export { + compareLogLevel, + isLogLevel, + type LogLevel, + parseLogLevel, +} from "./level.ts"; export { getLogger, type Logger } from "./logger.ts"; export type { LogRecord } from "./record.ts"; export {