From 21e579d1d1d0b4b040824a32798e7be8dc97dd70 Mon Sep 17 00:00:00 2001 From: jekabs-karklins Date: Sun, 18 Oct 2026 15:58:55 +0200 Subject: [PATCH] feat(logger): add LevelFilterLogger to forward only messages at or above a minimum level Wraps any Logger, so the level threshold can be set per destination, e.g. everything to console and only WARN and above to Graylog. Also exports the LEVEL enum so callers can pass a level. Co-Authored-By: Claude Opus 5.5 --- packages/logger/README.md | 22 ++++++ packages/logger/package.json | 2 +- packages/logger/src/index.ts | 2 + .../implementation/LevelFilterLogger.spec.ts | 79 +++++++++++++++++++ .../implementation/LevelFilterLogger.ts | 57 +++++++++++++ 5 files changed, 161 insertions(+), 1 deletion(-) create mode 100644 packages/logger/src/loggers/implementation/LevelFilterLogger.spec.ts create mode 100644 packages/logger/src/loggers/implementation/LevelFilterLogger.ts diff --git a/packages/logger/README.md b/packages/logger/README.md index 8755308..b81cdde 100644 --- a/packages/logger/README.md +++ b/packages/logger/README.md @@ -37,6 +37,28 @@ setLogger( ); ``` +Example for forwarding only some log levels +```typescript +import { + ConsoleLogger, + GrayLogLogger, + LEVEL, + LevelFilterLogger, + setLogger, +} from '@user-office-software/duo-logger'; + +// log everything to console, but send only WARN and above to Graylog +setLogger([ + new ConsoleLogger(), + new LevelFilterLogger( + new GrayLogLogger('my-graylog-server.com', 12201, {}, []), + LEVEL.WARN + ), +]); +``` + +Severity order is `DEBUG < INFO < WARN < ERROR < EXCEPTION < FATAL`. + ## Contributing Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change. diff --git a/packages/logger/package.json b/packages/logger/package.json index e0136d1..4feb9c9 100644 --- a/packages/logger/package.json +++ b/packages/logger/package.json @@ -1,6 +1,6 @@ { "name": "@user-office-software/duo-logger", - "version": "2.3.2", + "version": "2.4.0", "description": "Logger implementation", "author": "SWAP", "license": "ISC", diff --git a/packages/logger/src/index.ts b/packages/logger/src/index.ts index 623344c..092a9df 100644 --- a/packages/logger/src/index.ts +++ b/packages/logger/src/index.ts @@ -2,5 +2,7 @@ export * from './loggers/Logger'; export * from './loggers/implementation/GrayLogLogger'; export * from './loggers/implementation/ConsoleLogger'; export * from './loggers/implementation/MultiLogger'; +export * from './loggers/implementation/LevelFilterLogger'; export * from './loggers/implementation/WinstonLogger'; export * from './logger'; +export * from './enum/Level'; diff --git a/packages/logger/src/loggers/implementation/LevelFilterLogger.spec.ts b/packages/logger/src/loggers/implementation/LevelFilterLogger.spec.ts new file mode 100644 index 0000000..55608a5 --- /dev/null +++ b/packages/logger/src/loggers/implementation/LevelFilterLogger.spec.ts @@ -0,0 +1,79 @@ +import { LevelFilterLogger } from './LevelFilterLogger'; +import { LEVEL } from '../../enum/Level'; +import { Logger } from '../Logger'; + +describe('LevelFilterLogger', () => { + let inner: jest.Mocked; + + beforeEach(() => { + inner = { + logInfo: jest.fn(), + logWarn: jest.fn(), + logDebug: jest.fn(), + logError: jest.fn(), + logException: jest.fn(), + }; + }); + + const logAllLevels = (logger: Logger) => { + logger.logDebug('debug', {}); + logger.logInfo('info', {}); + logger.logWarn('warn', {}); + logger.logError('error', {}); + logger.logException('exception', new Error('boom'), {}); + }; + + test('forwards all messages when min level is DEBUG', () => { + logAllLevels(new LevelFilterLogger(inner, LEVEL.DEBUG)); + + expect(inner.logDebug).toHaveBeenCalledTimes(1); + expect(inner.logInfo).toHaveBeenCalledTimes(1); + expect(inner.logWarn).toHaveBeenCalledTimes(1); + expect(inner.logError).toHaveBeenCalledTimes(1); + expect(inner.logException).toHaveBeenCalledTimes(1); + }); + + test('drops messages below WARN when min level is WARN', () => { + logAllLevels(new LevelFilterLogger(inner, LEVEL.WARN)); + + expect(inner.logDebug).not.toHaveBeenCalled(); + expect(inner.logInfo).not.toHaveBeenCalled(); + expect(inner.logWarn).toHaveBeenCalledTimes(1); + expect(inner.logError).toHaveBeenCalledTimes(1); + expect(inner.logException).toHaveBeenCalledTimes(1); + }); + + test('forwards exceptions when min level is ERROR', () => { + logAllLevels(new LevelFilterLogger(inner, LEVEL.ERROR)); + + expect(inner.logWarn).not.toHaveBeenCalled(); + expect(inner.logError).toHaveBeenCalledTimes(1); + expect(inner.logException).toHaveBeenCalledTimes(1); + }); + + test('drops everything when min level is FATAL', () => { + logAllLevels(new LevelFilterLogger(inner, LEVEL.FATAL)); + + expect(inner.logDebug).not.toHaveBeenCalled(); + expect(inner.logInfo).not.toHaveBeenCalled(); + expect(inner.logWarn).not.toHaveBeenCalled(); + expect(inner.logError).not.toHaveBeenCalled(); + expect(inner.logException).not.toHaveBeenCalled(); + }); + + test('passes arguments through unchanged', () => { + const logger = new LevelFilterLogger(inner, LEVEL.DEBUG); + const context = { key: 'value' }; + const exception = new Error('boom'); + + logger.logInfo('message', context); + logger.logException('failed', exception, context); + + expect(inner.logInfo).toHaveBeenCalledWith('message', context); + expect(inner.logException).toHaveBeenCalledWith( + 'failed', + exception, + context + ); + }); +}); diff --git a/packages/logger/src/loggers/implementation/LevelFilterLogger.ts b/packages/logger/src/loggers/implementation/LevelFilterLogger.ts new file mode 100644 index 0000000..ebbe0d7 --- /dev/null +++ b/packages/logger/src/loggers/implementation/LevelFilterLogger.ts @@ -0,0 +1,57 @@ +import { LEVEL } from '../../enum/Level'; +import { Logger } from '../Logger'; + +const SEVERITY: Record = { + [LEVEL.DEBUG]: 0, + [LEVEL.INFO]: 1, + [LEVEL.WARN]: 2, + [LEVEL.ERROR]: 3, + [LEVEL.EXCEPTION]: 4, + [LEVEL.FATAL]: 5, +}; + +/** + * A logger that forwards to another logger only the messages + * at or above the given minimum level. + * Severity order: DEBUG < INFO < WARN < ERROR < EXCEPTION < FATAL + */ +export class LevelFilterLogger implements Logger { + constructor( + private logger: Logger, + private minLevel: LEVEL + ) {} + + private isEnabled(level: LEVEL) { + return SEVERITY[level] >= SEVERITY[this.minLevel]; + } + + logInfo(message: string, context: Record): void { + if (this.isEnabled(LEVEL.INFO)) { + this.logger.logInfo(message, context); + } + } + logWarn(message: string, context: Record): void { + if (this.isEnabled(LEVEL.WARN)) { + this.logger.logWarn(message, context); + } + } + logDebug(message: string, context: Record): void { + if (this.isEnabled(LEVEL.DEBUG)) { + this.logger.logDebug(message, context); + } + } + logError(message: string, context: Record): void { + if (this.isEnabled(LEVEL.ERROR)) { + this.logger.logError(message, context); + } + } + logException( + message: string, + exception: unknown, + context?: Record | undefined + ): void { + if (this.isEnabled(LEVEL.EXCEPTION)) { + this.logger.logException(message, exception, context); + } + } +}