From 5a800eb87050093a74e8810340be7021c50b6d6f Mon Sep 17 00:00:00 2001 From: Hong Minhee Date: Mon, 22 Apr 2024 00:19:46 +0900 Subject: [PATCH] Sink filter --- CHANGES.md | 1 + README.md | 85 ++++++++++++++++++++++++++++++++++++++++++++ logtape/mod.ts | 1 + logtape/sink.test.ts | 13 +++++++ logtape/sink.ts | 22 ++++++++++++ 5 files changed, 122 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index 7c7e18d..2a97b90 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -11,6 +11,7 @@ To be released. - Added `parseLogLevel()` function. - Added `isLogLevel()` function. - Added `getConfig()` function. + - Added `withFilter()` function. Version 0.2.2 diff --git a/README.md b/README.md index 1435adb..6a54acb 100644 --- a/README.md +++ b/README.md @@ -418,6 +418,91 @@ export default { [`ctx.waitUntil()`]: https://developers.cloudflare.com/workers/runtime-apis/context/#waituntil +Filters +------- + +A filter is a function that filters log messages. A filter takes a log record +and returns a boolean value. If the filter returns `true`, the log record is +passed to the sinks; otherwise, the log record is discarded. Its signature is: + +~~~~ typescript +export type Filter = (record: LogRecord) => boolean; +~~~~ + +For example, the following filter discards log messages whose property `elapsed` +is less than 100 milliseconds: + +~~~~ typescript +import { configure, type LogRecord } from "@logtape/logtape"; + +await configure({ + // Omitted for brevity + filters: { + tooSlow(record: LogRecord) { + return "elapsed" in record.properties && record.properties.elapsed >= 100; + }, + }, + loggers: [ + { + category: ["my-app", "database"], + level: "debug", + sinks: ["console"], + filters: ["tooSlow"], + } + ] +}); +~~~~ + +### Level filter + +LogTape provides a built-in level filter. You can use the level filter to +filter log messages by their log levels. The level filter factory takes +a [`LogLevel`] string and returns a level filter. For example, the following +level filter discards log messages whose log level is less than `info`: + +~~~~ typescript +import { getLevelFilter } from "@logtape/logtape"; + +await configure({ + filters: { + infoOrHigher: getLevelFilter("info"); + }, + // Omitted for brevity +}); +~~~~ + +[`LogLevel`]: https://jsr.io/@logtape/logtape/doc/~/LogLevel + +### Sink filter + +A sink filter is a filter that is applied to a specific sink. You can add a +sink filter to a sink by decorating the sink with [`withFilter()`]: + +~~~~ typescript +import { getConsoleSink, withFilter } from "@logtape/logtape"; + +await configure({ + sinks: { + filteredConsole: withFilter( + getConsoleSink(), + log => "elapsed" in log.properties && log.properties.elapsed >= 100, + ), + }, + // Omitted for brevity +}); +~~~~ + +The `filteredConsoleSink` only logs messages whose property `elapsed` is greater +than or equal to 100 milliseconds to the console. + +> [!TIP] +> The `withFilter()` function can take a [`LogLevel`] string as the second +> argument. In this case, the log messages whose log level is less than +> the specified log level are discarded. + +[`withFilter()`]: https://jsr.io/@logtape/logtape/doc/~/withFilter + + Testing ------- diff --git a/logtape/mod.ts b/logtape/mod.ts index 4616b70..f52c807 100644 --- a/logtape/mod.ts +++ b/logtape/mod.ts @@ -31,6 +31,7 @@ export { type RotatingFileSinkOptions, type Sink, type StreamSinkOptions, + withFilter, } from "./sink.ts"; // cSpell: ignore filesink diff --git a/logtape/sink.test.ts b/logtape/sink.test.ts index d9debb0..107b6eb 100644 --- a/logtape/sink.test.ts +++ b/logtape/sink.test.ts @@ -5,14 +5,27 @@ import fs from "node:fs"; import { isDeno } from "which_runtime"; import { debug, error, fatal, info, warning } from "./fixtures.ts"; import type { LogLevel } from "./level.ts"; +import type { LogRecord } from "./record.ts"; import { type FileSinkDriver, getConsoleSink, getFileSink, getStreamSink, type Sink, + withFilter, } from "./sink.ts"; +Deno.test("withFilter()", () => { + const buffer: LogRecord[] = []; + const sink = withFilter(buffer.push.bind(buffer), "warning"); + sink(debug); + sink(info); + sink(warning); + sink(error); + sink(fatal); + assertEquals(buffer, [warning, error, fatal]); +}); + interface ConsoleMock extends Console { history(): unknown[]; } diff --git a/logtape/sink.ts b/logtape/sink.ts index f4be3a1..55d3434 100644 --- a/logtape/sink.ts +++ b/logtape/sink.ts @@ -1,3 +1,4 @@ +import { type FilterLike, toFilter } from "./filter.ts"; import { type ConsoleFormatter, defaultConsoleFormatter, @@ -17,6 +18,27 @@ import type { LogRecord } from "./record.ts"; */ export type Sink = (record: LogRecord) => void; +/** + * Turns a sink into a filtered sink. The returned sink only logs records that + * pass the filter. + * + * @example Filter a console sink to only log records with the info level + * ```typescript + * const sink = withFilter(getConsoleSink(), "info"); + * ``` + * + * @param sink A sink to be filtered. + * @param filter A filter to apply to the sink. It can be either a filter + * function or a {@link LogLevel} string. + * @returns A sink that only logs records that pass the filter. + */ +export function withFilter(sink: Sink, filter: FilterLike): Sink { + const filterFunc = toFilter(filter); + return (record: LogRecord) => { + if (filterFunc(record)) sink(record); + }; +} + /** * Options for the {@link getStreamSink} function. */