diff --git a/src/logging/LazyLogger.ts b/src/logging/LazyLogger.ts new file mode 100644 index 000000000..aebc7e185 --- /dev/null +++ b/src/logging/LazyLogger.ts @@ -0,0 +1,29 @@ +import type { LazyLoggerFactory } from './LazyLoggerFactory'; +import type { Logger } from './Logger'; +import type { LogLevel } from './LogLevel'; + +/** + * Creates a logger lazily using a reference to {@link LazyLoggerFactory}. + * + * An error will be thrown if {@link LazyLogger.log} is invoked + * before a {@link LoggerFactory} is set in {@link LazyLoggerFactory}. + */ +export class LazyLogger implements Logger { + private readonly lazyLoggerFactory: LazyLoggerFactory; + private readonly label: string; + + private logger: Logger | undefined; + + public constructor(lazyLoggerFactory: LazyLoggerFactory, label: string) { + this.lazyLoggerFactory = lazyLoggerFactory; + this.label = label; + } + + public log(level: LogLevel, message: string, meta: any): Logger { + if (!this.logger) { + this.logger = this.lazyLoggerFactory.getLoggerFactoryOrThrow() + .createLogger(this.label); + } + return this.logger.log(level, message, meta); + } +} diff --git a/src/logging/LazyLoggerFactory.ts b/src/logging/LazyLoggerFactory.ts new file mode 100644 index 000000000..cfbc2a8f2 --- /dev/null +++ b/src/logging/LazyLoggerFactory.ts @@ -0,0 +1,41 @@ +import { LazyLogger } from './LazyLogger'; +import type { Logger } from './Logger'; +import type { LoggerFactory } from './LoggerFactory'; + +/** + * Wraps over another {@link LoggerFactory} that can be set lazily. + * This is a singleton class, for which the instance can be retrieved using {@link LazyLoggerFactory.getInstance}. + * + * Loggers can safely be created before a {@link LoggerFactory} is set. + * But an error will be thrown if {@link Logger.log} is invoked before a {@link LoggerFactory} is set. + * + * This creates instances of {@link LazyLogger}. + */ +export class LazyLoggerFactory implements LoggerFactory { + private static readonly instance = new LazyLoggerFactory(); + + private loggerFactory: LoggerFactory | undefined; + + private constructor() { + // Singleton instance + } + + public static getInstance(): LazyLoggerFactory { + return LazyLoggerFactory.instance; + } + + public createLogger(label: string): Logger { + return new LazyLogger(this, label); + } + + public setLoggerFactory(loggerFactory: LoggerFactory | undefined): void { + this.loggerFactory = loggerFactory; + } + + public getLoggerFactoryOrThrow(): LoggerFactory { + if (!this.loggerFactory) { + throw new Error('Illegal logging during initialization'); + } + return this.loggerFactory; + } +} diff --git a/test/unit/logging/LazyLogger.test.ts b/test/unit/logging/LazyLogger.test.ts new file mode 100644 index 000000000..b69c60903 --- /dev/null +++ b/test/unit/logging/LazyLogger.test.ts @@ -0,0 +1,53 @@ +import { LazyLogger } from '../../../src/logging/LazyLogger'; +import { LazyLoggerFactory } from '../../../src/logging/LazyLoggerFactory'; + +describe('LazyLogger', (): void => { + let lazyLoggerFactory: LazyLoggerFactory; + let logger: LazyLogger; + beforeEach(async(): Promise => { + lazyLoggerFactory = LazyLoggerFactory.getInstance(); + lazyLoggerFactory.setLoggerFactory(undefined); + logger = new LazyLogger(lazyLoggerFactory, 'MyLabel'); + }); + + it('throws when no logger factory is set in the lazy logger factory.', async(): Promise => { + expect((): any => logger.log('debug', 'my message', { abc: true })) + .toThrow(new Error('Illegal logging during initialization')); + }); + + it('creates a new logger using the factory.', async(): Promise => { + const dummyLogger: any = { + log: jest.fn((): any => dummyLogger), + }; + const dummyLoggerFactory: any = { + createLogger: jest.fn((): any => dummyLogger), + }; + lazyLoggerFactory.setLoggerFactory(dummyLoggerFactory); + + expect(logger.log('debug', 'my message', { abc: true })).toBe(dummyLogger); + expect(dummyLoggerFactory.createLogger).toHaveBeenCalledTimes(1); + expect(dummyLoggerFactory.createLogger).toHaveBeenCalledWith('MyLabel'); + expect(dummyLogger.log).toHaveBeenCalledTimes(1); + expect(dummyLogger.log).toHaveBeenCalledWith('debug', 'my message', { abc: true }); + }); + + it('reuses the logger for repeated calls.', async(): Promise => { + const dummyLogger: any = { + log: jest.fn((): any => dummyLogger), + }; + const dummyLoggerFactory: any = { + createLogger: jest.fn((): any => dummyLogger), + }; + lazyLoggerFactory.setLoggerFactory(dummyLoggerFactory); + + expect(logger.log('debug', 'my message 1', { abc: true })).toBe(dummyLogger); + expect(logger.log('debug', 'my message 2', { abc: true })).toBe(dummyLogger); + expect(logger.log('debug', 'my message 3', { abc: true })).toBe(dummyLogger); + expect(dummyLoggerFactory.createLogger).toHaveBeenCalledTimes(1); + expect(dummyLoggerFactory.createLogger).toHaveBeenCalledWith('MyLabel'); + expect(dummyLogger.log).toHaveBeenCalledTimes(3); + expect(dummyLogger.log).toHaveBeenNthCalledWith(1, 'debug', 'my message 1', { abc: true }); + expect(dummyLogger.log).toHaveBeenNthCalledWith(2, 'debug', 'my message 2', { abc: true }); + expect(dummyLogger.log).toHaveBeenNthCalledWith(3, 'debug', 'my message 3', { abc: true }); + }); +}); diff --git a/test/unit/logging/LazyLoggerFactory.test.ts b/test/unit/logging/LazyLoggerFactory.test.ts new file mode 100644 index 000000000..a53a9da38 --- /dev/null +++ b/test/unit/logging/LazyLoggerFactory.test.ts @@ -0,0 +1,65 @@ +import { LazyLogger } from '../../../src/logging/LazyLogger'; +import { LazyLoggerFactory } from '../../../src/logging/LazyLoggerFactory'; + +describe('LazyLoggerFactory', (): void => { + let dummyLogger: any; + let dummyLoggerFactory: any; + beforeEach(async(): Promise => { + LazyLoggerFactory.getInstance().setLoggerFactory(undefined); + dummyLogger = { + log: jest.fn((): any => dummyLogger), + }; + dummyLoggerFactory = { + createLogger: jest.fn((): any => dummyLogger), + }; + }); + + it('is a singleton.', async(): Promise => { + expect(LazyLoggerFactory.getInstance()).toBeInstanceOf(LazyLoggerFactory); + }); + + it('allows LazyLoggers to be created before an inner factory was set.', async(): Promise => { + const logger = LazyLoggerFactory.getInstance().createLogger('MyLabel'); + expect(logger).toBeInstanceOf(LazyLogger); + }); + + it('allows LazyLoggers to be created after an inner factory was set.', async(): Promise => { + LazyLoggerFactory.getInstance().setLoggerFactory(dummyLoggerFactory); + const logger = LazyLoggerFactory.getInstance().createLogger('MyLabel'); + expect(logger).toBeInstanceOf(LazyLogger); + }); + + it('throws when retrieving the inner factory if none has been set.', async(): Promise => { + expect((): any => LazyLoggerFactory.getInstance().getLoggerFactoryOrThrow()) + .toThrow(new Error('Illegal logging during initialization')); + }); + + it('Returns the inner factory if one has been set.', async(): Promise => { + LazyLoggerFactory.getInstance().setLoggerFactory(dummyLoggerFactory); + expect(LazyLoggerFactory.getInstance().getLoggerFactoryOrThrow()).toBe(dummyLoggerFactory); + }); + + it('allows LazyLoggers to be invoked if a factory has been set beforehand.', async(): Promise => { + LazyLoggerFactory.getInstance().setLoggerFactory(dummyLoggerFactory); + const logger = LazyLoggerFactory.getInstance().createLogger('MyLabel'); + logger.log('debug', 'my message', { abc: true }); + + expect(dummyLogger.log).toHaveBeenCalledTimes(1); + expect(dummyLogger.log).toHaveBeenCalledWith('debug', 'my message', { abc: true }); + }); + + it('allows LazyLoggers to be invoked if a factory has been after lazy logger creation.', async(): Promise => { + const logger = LazyLoggerFactory.getInstance().createLogger('MyLabel'); + LazyLoggerFactory.getInstance().setLoggerFactory(dummyLoggerFactory); + logger.log('debug', 'my message', { abc: true }); + + expect(dummyLogger.log).toHaveBeenCalledTimes(1); + expect(dummyLogger.log).toHaveBeenCalledWith('debug', 'my message', { abc: true }); + }); + + it('errors on invoking LazyLoggers if a factory has not been set yet.', async(): Promise => { + const logger = LazyLoggerFactory.getInstance().createLogger('MyLabel'); + expect((): any => logger.log('debug', 'my message', { abc: true })) + .toThrow(new Error('Illegal logging during initialization')); + }); +});