From f10799d0a9e07812f2e4153537c4b131715db706 Mon Sep 17 00:00:00 2001 From: Alex Smolya Date: Thu, 3 Sep 2026 10:41:07 +0200 Subject: [PATCH] feat(logger): filter in-memory entries by level --- src/index.ts | 1 + src/logger/runtime-logger.ts | 26 +++++--- .../console-runtime-logger-level.test.ts | 36 +++++++++- tests/logger/runtime-logger.test.ts | 66 +++++++++++++++++++ 4 files changed, 119 insertions(+), 10 deletions(-) diff --git a/src/index.ts b/src/index.ts index 2a95847..9ff00f9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -50,6 +50,7 @@ export type { } from "./events/runtime-events.js"; export type { ConsoleRuntimeLoggerOptions, + InMemoryRuntimeLoggerOptions, RuntimeLogger, RuntimeLogLevel } from "./logger/runtime-logger.js"; diff --git a/src/logger/runtime-logger.ts b/src/logger/runtime-logger.ts index 4cc49a9..7fac016 100644 --- a/src/logger/runtime-logger.ts +++ b/src/logger/runtime-logger.ts @@ -15,6 +15,13 @@ const LOG_LEVEL_PRIORITY: Record = { error: 3 }; +function shouldLog( + level: RuntimeLogLevel, + minimumLevel: RuntimeLogLevel +): boolean { + return LOG_LEVEL_PRIORITY[level] >= LOG_LEVEL_PRIORITY[minimumLevel]; +} + export interface ConsoleRuntimeLoggerOptions { level?: RuntimeLogLevel; redactKeys?: RegExp; @@ -57,33 +64,29 @@ export class ConsoleRuntimeLogger implements RuntimeLogger { } public info(message: string, metadata?: Record): void { - if (this.shouldLog("info")) { + if (shouldLog("info", this.minimumLevel)) { console.info(message, this.prepareMetadata(metadata)); } } public warn(message: string, metadata?: Record): void { - if (this.shouldLog("warn")) { + if (shouldLog("warn", this.minimumLevel)) { console.warn(message, this.prepareMetadata(metadata)); } } public debug(message: string, metadata?: Record): void { - if (this.shouldLog("debug")) { + if (shouldLog("debug", this.minimumLevel)) { console.debug(message, this.prepareMetadata(metadata)); } } public error(message: string, metadata?: Record): void { - if (this.shouldLog("error")) { + if (shouldLog("error", this.minimumLevel)) { console.error(message, this.prepareMetadata(metadata)); } } - private shouldLog(level: RuntimeLogLevel): boolean { - return LOG_LEVEL_PRIORITY[level] >= LOG_LEVEL_PRIORITY[this.minimumLevel]; - } - private prepareMetadata( metadata?: Record ): Record { @@ -95,6 +98,8 @@ export class ConsoleRuntimeLogger implements RuntimeLogger { } export interface InMemoryRuntimeLoggerOptions { + /** Minimum level to retain; defaults to debug to preserve all-level recording. */ + level?: RuntimeLogLevel; maxEntries?: number; } @@ -106,9 +111,11 @@ interface InMemoryLogEntry { export class InMemoryRuntimeLogger implements RuntimeLogger { public readonly entries: InMemoryLogEntry[] = []; + private readonly minimumLevel: RuntimeLogLevel; private readonly maxEntries: number; public constructor(options: InMemoryRuntimeLoggerOptions = {}) { + this.minimumLevel = options.level ?? "debug"; this.maxEntries = options.maxEntries ?? 5_000; } @@ -141,6 +148,9 @@ export class InMemoryRuntimeLogger implements RuntimeLogger { message: string, metadata?: Record ): void { + if (!shouldLog(level, this.minimumLevel)) { + return; + } if (this.maxEntries > 0 && this.entries.length >= this.maxEntries) { this.entries.shift(); } diff --git a/tests/logger/console-runtime-logger-level.test.ts b/tests/logger/console-runtime-logger-level.test.ts index 1797336..9de1db7 100644 --- a/tests/logger/console-runtime-logger-level.test.ts +++ b/tests/logger/console-runtime-logger-level.test.ts @@ -1,17 +1,26 @@ -import { describe, it, expect, vi, beforeEach } from "vitest"; -import { ConsoleRuntimeLogger } from "../../src/logger/runtime-logger.js"; +import { afterEach, describe, it, expect, vi, beforeEach } from "vitest"; +import { + ConsoleRuntimeLogger, + InMemoryRuntimeLogger +} from "../../src/logger/runtime-logger.js"; describe("ConsoleRuntimeLogger level filtering", () => { let infoSpy: ReturnType; + let debugSpy: ReturnType; let warnSpy: ReturnType; let errorSpy: ReturnType; beforeEach(() => { infoSpy = vi.spyOn(console, "info").mockImplementation(() => {}); + debugSpy = vi.spyOn(console, "debug").mockImplementation(() => {}); warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {}); errorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); }); + afterEach(() => { + vi.restoreAllMocks(); + }); + it("logs all levels when level=info (default)", () => { const logger = new ConsoleRuntimeLogger(); logger.info("i"); @@ -41,4 +50,27 @@ describe("ConsoleRuntimeLogger level filtering", () => { expect(warnSpy).toHaveBeenCalledTimes(1); expect(errorSpy).toHaveBeenCalledTimes(1); }); + + it("matches in-memory filtering at the same configured threshold", () => { + const consoleLogger = new ConsoleRuntimeLogger({ level: "warn" }); + const memoryLogger = new InMemoryRuntimeLogger({ level: "warn" }); + + consoleLogger.debug("debug"); + consoleLogger.info("info"); + consoleLogger.warn("warn"); + consoleLogger.error("error"); + memoryLogger.debug("debug"); + memoryLogger.info("info"); + memoryLogger.warn("warn"); + memoryLogger.error("error"); + + expect(debugSpy).not.toHaveBeenCalled(); + expect(infoSpy).not.toHaveBeenCalled(); + expect(warnSpy).toHaveBeenCalledTimes(1); + expect(errorSpy).toHaveBeenCalledTimes(1); + expect(memoryLogger.entries.map((entry) => entry.level)).toEqual([ + "warn", + "error" + ]); + }); }); diff --git a/tests/logger/runtime-logger.test.ts b/tests/logger/runtime-logger.test.ts index 17da06a..9aa6f18 100644 --- a/tests/logger/runtime-logger.test.ts +++ b/tests/logger/runtime-logger.test.ts @@ -5,6 +5,72 @@ import { } from "../../src/logger/runtime-logger.js"; describe("InMemoryRuntimeLogger", () => { + it("records all supported levels by default", () => { + const logger = new InMemoryRuntimeLogger(); + + logger.debug("debug"); + logger.info("info"); + logger.warn("warn"); + logger.error("error"); + + expect(logger.entries.map((entry) => entry.level)).toEqual([ + "debug", + "info", + "warn", + "error" + ]); + expect(logger.size()).toBe(4); + }); + + it("filters entries below the configured threshold", () => { + const logger = new InMemoryRuntimeLogger({ level: "warn" }); + + logger.debug("debug"); + logger.info("info"); + logger.warn("warn"); + logger.error("error"); + + expect(logger.entries.map((entry) => entry.level)).toEqual([ + "warn", + "error" + ]); + expect(logger.size()).toBe(2); + }); + + it("preserves retained order and clear semantics after filtering", () => { + const logger = new InMemoryRuntimeLogger({ level: "warn" }); + + logger.info("ignored"); + logger.warn("first"); + logger.debug("also ignored"); + logger.error("second"); + + expect(logger.entries.map((entry) => entry.message)).toEqual([ + "first", + "second" + ]); + expect(logger.size()).toBe(2); + + logger.clear(); + + expect(logger.entries).toEqual([]); + expect(logger.size()).toBe(0); + }); + + it("does not let filtered entries affect maxEntries eviction", () => { + const logger = new InMemoryRuntimeLogger({ level: "warn", maxEntries: 2 }); + + logger.warn("first"); + logger.error("second"); + logger.info("ignored"); + logger.warn("third"); + + expect(logger.entries.map((entry) => entry.message)).toEqual([ + "second", + "third" + ]); + }); + it("records info entries with correct level and message", () => { const logger = new InMemoryRuntimeLogger(); logger.info("test message");