Skip to content

Commit

Permalink
refactor: Make Logger an interface.
Browse files Browse the repository at this point in the history
  • Loading branch information
RubenVerborgh committed Apr 1, 2022
1 parent 67affa4 commit 69477d0
Show file tree
Hide file tree
Showing 6 changed files with 152 additions and 81 deletions.
23 changes: 3 additions & 20 deletions src/logging/LazyLoggerFactory.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,10 @@
import { Logger } from './Logger';
import type { BasicLogger } from './Logger';
import { WrappingLogger } from './Logger';
import type { Logger } from './Logger';
import type { LoggerFactory } from './LoggerFactory';
import type { LogLevel } from './LogLevel';

/**
* Wraps around another {@link Logger} that can be set lazily.
*/
class WrappingLogger extends Logger {
public logger: BasicLogger;

public constructor(logger: BasicLogger) {
super();
this.logger = logger;
}

public log(level: LogLevel, message: string): Logger {
this.logger.log(level, message);
return this;
}
}

/**
* Temporary {@link LoggerFactory} that creates buffered {@link WrappingLogger}s
* Temporary {@link LoggerFactory} that buffers log messages in memory
* until the {@link TemporaryLoggerFactory#switch} method is called.
*/
class TemporaryLoggerFactory implements LoggerFactory {
Expand Down
78 changes: 58 additions & 20 deletions src/logging/Logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,83 +5,121 @@ import type { LogLevel } from './LogLevel';
*
* @see getLoggerFor on how to instantiate loggers.
*/
export type BasicLogger = {
export interface SimpleLogger {
/**
* Log the given message at the given level.
* If the internal level is higher than the given level, the message may be voided.
* @param level - The level to log at.
* @param message - The message to log.
* @param meta - Optional metadata to include in the log message.
*/
log: (level: LogLevel, message: string) => BasicLogger;
};
log: (level: LogLevel, message: string) => SimpleLogger;
}

/**
* Logs messages, with convenience methods to log on a specific level.
*
* @see getLoggerFor on how to instantiate loggers.
*/
export abstract class Logger implements BasicLogger {
export interface Logger extends SimpleLogger {
/**
* Log the given message at the given level.
* If the internal level is higher than the given level, the message may be voided.
* @param level - The level to log at.
* @param message - The message to log.
* @param meta - Optional metadata to include in the log message.
*/
public abstract log(level: LogLevel, message: string): Logger;
log: (level: LogLevel, message: string) => Logger;

/**
* Log a message at the 'error' level.
* @param message - The message to log.
* @param meta - Optional metadata to include in the log message.
*/
public error(message: string): Logger {
return this.log('error', message);
}
error: (message: string) => Logger;

/**
* Log a message at the 'warn' level.
* @param message - The message to log.
* @param meta - Optional metadata to include in the log message.
*/
public warn(message: string): Logger {
return this.log('warn', message);
}
warn: (message: string) => Logger;

/**
* Log a message at the 'info' level.
* @param message - The message to log.
* @param meta - Optional metadata to include in the log message.
*/
public info(message: string): Logger {
return this.log('info', message);
}
info: (message: string) => Logger;

/**
* Log a message at the 'verbose' level.
* @param message - The message to log.
* @param meta - Optional metadata to include in the log message.
*/
public verbose(message: string): Logger {
return this.log('verbose', message);
}
verbose: (message: string) => Logger;

/**
* Log a message at the 'debug' level.
* @param message - The message to log.
* @param meta - Optional metadata to include in the log message.
*/
public debug(message: string): Logger {
return this.log('debug', message);
}
debug: (message: string) => Logger;

/**
* Log a message at the 'silly' level.
* @param message - The message to log.
* @param meta - Optional metadata to include in the log message.
*/
silly: (message: string) => Logger;
}

/**
* Base class that implements all additional {@link BaseLogger} methods,
* leaving only the implementation of {@link SimpleLogger}.
*/
export abstract class BaseLogger implements Logger {
public abstract log(level: LogLevel, message: string): Logger;

public error(message: string): Logger {
return this.log('error', message);
}

public warn(message: string): Logger {
return this.log('warn', message);
}

public info(message: string): Logger {
return this.log('info', message);
}

public verbose(message: string): Logger {
return this.log('verbose', message);
}

public debug(message: string): Logger {
return this.log('debug', message);
}

public silly(message: string): Logger {
return this.log('silly', message);
}
}

/**
* Implements {@link BaseLogger} around a {@link SimpleLogger},
* which can be swapped out a runtime.
*/
export class WrappingLogger extends BaseLogger {
public logger: SimpleLogger;

public constructor(logger: SimpleLogger) {
super();
this.logger = logger;
}

public log(level: LogLevel, message: string): this {
this.logger.log(level, message);
return this;
}
}
6 changes: 3 additions & 3 deletions src/logging/VoidLogger.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { Logger } from './Logger';
import { BaseLogger } from './Logger';
import type { LogLevel } from './LogLevel';

/**
* A logger that does nothing on a log message.
*/
export class VoidLogger extends Logger {
export class VoidLogger extends BaseLogger {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
public log(level: LogLevel, message: string, meta?: any): Logger {
public log(level: LogLevel, message: string, meta?: any): this {
// Do nothing
return this;
}
Expand Down
3 changes: 1 addition & 2 deletions src/logging/VoidLoggerFactory.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import type { Logger } from './Logger';
import type { LoggerFactory } from './LoggerFactory';
import { VoidLogger } from './VoidLogger';

Expand All @@ -9,7 +8,7 @@ export class VoidLoggerFactory implements LoggerFactory {
private readonly logger = new VoidLogger();

// eslint-disable-next-line @typescript-eslint/no-unused-vars
public createLogger(label: string): Logger {
public createLogger(label: string): VoidLogger {
return this.logger;
}
}
4 changes: 2 additions & 2 deletions src/logging/WinstonLogger.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import type { Logger as WinstonInnerLogger } from 'winston';
import { Logger } from './Logger';
import { BaseLogger } from './Logger';
import type { LogLevel } from './LogLevel';

/**
* A WinstonLogger implements the {@link Logger} interface using a given winston logger.
*/
export class WinstonLogger extends Logger {
export class WinstonLogger extends BaseLogger {
private readonly logger: WinstonInnerLogger;

public constructor(logger: WinstonInnerLogger) {
Expand Down
119 changes: 85 additions & 34 deletions test/unit/logging/Logger.test.ts
Original file line number Diff line number Diff line change
@@ -1,44 +1,95 @@
import { Logger } from '../../../src/logging/Logger';
import { BaseLogger, WrappingLogger } from '../../../src/logging/Logger';
import type { SimpleLogger } from '../../../src/logging/Logger';

describe('Logger', (): void => {
let logger: Logger;
beforeEach(async(): Promise<void> => {
logger = new (Logger as any)();
logger.log = jest.fn();
});
describe('a BaseLogger', (): void => {
let logger: BaseLogger;

it('Error delegates to log.', async(): Promise<void> => {
logger.error('my message');
expect(logger.log).toHaveBeenCalledTimes(1);
expect(logger.log).toHaveBeenCalledWith('error', 'my message');
});
beforeEach(async(): Promise<void> => {
logger = new (BaseLogger as any)();
logger.log = jest.fn();
});

it('Warn delegates to log.', async(): Promise<void> => {
logger.warn('my message');
expect(logger.log).toHaveBeenCalledTimes(1);
expect(logger.log).toHaveBeenCalledWith('warn', 'my message');
});
it('delegates error to log.', async(): Promise<void> => {
logger.error('my message');
expect(logger.log).toHaveBeenCalledTimes(1);
expect(logger.log).toHaveBeenCalledWith('error', 'my message');
});

it('Info delegates to log.', async(): Promise<void> => {
logger.info('my message');
expect(logger.log).toHaveBeenCalledTimes(1);
expect(logger.log).toHaveBeenCalledWith('info', 'my message');
});
it('Verbose delegates to log.', async(): Promise<void> => {
logger.verbose('my message');
expect(logger.log).toHaveBeenCalledTimes(1);
expect(logger.log).toHaveBeenCalledWith('verbose', 'my message');
});
it('warn delegates to log.', async(): Promise<void> => {
logger.warn('my message');
expect(logger.log).toHaveBeenCalledTimes(1);
expect(logger.log).toHaveBeenCalledWith('warn', 'my message');
});

it('info delegates to log.', async(): Promise<void> => {
logger.info('my message');
expect(logger.log).toHaveBeenCalledTimes(1);
expect(logger.log).toHaveBeenCalledWith('info', 'my message');
});

it('verbose delegates to log.', async(): Promise<void> => {
logger.verbose('my message');
expect(logger.log).toHaveBeenCalledTimes(1);
expect(logger.log).toHaveBeenCalledWith('verbose', 'my message');
});

it('debug delegates to log.', async(): Promise<void> => {
logger.debug('my message');
expect(logger.log).toHaveBeenCalledTimes(1);
expect(logger.log).toHaveBeenCalledWith('debug', 'my message');
});

it('Debug delegates to log.', async(): Promise<void> => {
logger.debug('my message');
expect(logger.log).toHaveBeenCalledTimes(1);
expect(logger.log).toHaveBeenCalledWith('debug', 'my message');
it('silly delegates to log.', async(): Promise<void> => {
logger.silly('my message');
expect(logger.log).toHaveBeenCalledTimes(1);
expect(logger.log).toHaveBeenCalledWith('silly', 'my message');
});
});

it('Silly delegates to log.', async(): Promise<void> => {
logger.silly('my message');
expect(logger.log).toHaveBeenCalledTimes(1);
expect(logger.log).toHaveBeenCalledWith('silly', 'my message');
describe('a WrappingLogger', (): void => {
let logger: SimpleLogger;
let wrapper: WrappingLogger;

beforeEach(async(): Promise<void> => {
logger = { log: jest.fn() };
wrapper = new WrappingLogger(logger);
});

it('error delegates to the internal logger.', async(): Promise<void> => {
wrapper.error('my message');
expect(logger.log).toHaveBeenCalledTimes(1);
expect(logger.log).toHaveBeenCalledWith('error', 'my message');
});

it('warn delegates to the internal logger.', async(): Promise<void> => {
wrapper.warn('my message');
expect(logger.log).toHaveBeenCalledTimes(1);
expect(logger.log).toHaveBeenCalledWith('warn', 'my message');
});

it('info delegates to the internal logger.', async(): Promise<void> => {
wrapper.info('my message');
expect(logger.log).toHaveBeenCalledTimes(1);
expect(logger.log).toHaveBeenCalledWith('info', 'my message');
});

it('verbose delegates to the internal logger.', async(): Promise<void> => {
wrapper.verbose('my message');
expect(logger.log).toHaveBeenCalledTimes(1);
expect(logger.log).toHaveBeenCalledWith('verbose', 'my message');
});

it('debug delegates to the internal logger.', async(): Promise<void> => {
wrapper.debug('my message');
expect(logger.log).toHaveBeenCalledTimes(1);
expect(logger.log).toHaveBeenCalledWith('debug', 'my message');
});

it('silly delegates to the internal logger.', async(): Promise<void> => {
wrapper.silly('my message');
expect(logger.log).toHaveBeenCalledTimes(1);
expect(logger.log).toHaveBeenCalledWith('silly', 'my message');
});
});
});

0 comments on commit 69477d0

Please sign in to comment.