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); + } + } +}