feat: Make LazyLoggerFactory buffer messages.

This commit is contained in:
Ruben Verborgh
2022-03-30 14:08:09 +02:00
committed by Joachim Van Herwegen
parent 2c6167e0cb
commit 238570b3d2
8 changed files with 240 additions and 192 deletions

View File

@@ -215,7 +215,6 @@ export * from './init/ServerInitializer';
export * from './init/ModuleVersionVerifier';
// Logging
export * from './logging/LazyLogger';
export * from './logging/LazyLoggerFactory';
export * from './logging/Logger';
export * from './logging/LoggerFactory';

View File

@@ -1,29 +0,0 @@
import type { LazyLoggerFactory } from './LazyLoggerFactory';
import { 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 extends Logger {
private readonly lazyLoggerFactory: LazyLoggerFactory;
private readonly label: string;
private logger: Logger | undefined;
public constructor(lazyLoggerFactory: LazyLoggerFactory, label: string) {
super();
this.lazyLoggerFactory = lazyLoggerFactory;
this.label = label;
}
public log(level: LogLevel, message: string): Logger {
if (!this.logger) {
this.logger = this.lazyLoggerFactory.loggerFactory.createLogger(this.label);
}
return this.logger.log(level, message);
}
}

View File

@@ -1,45 +1,109 @@
import { LazyLogger } from './LazyLogger';
import type { Logger } from './Logger';
import { Logger } from './Logger';
import type { BasicLogger } from './Logger';
import type { LoggerFactory } from './LoggerFactory';
import type { LogLevel } from './LogLevel';
/**
* 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}.
* 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
* until the {@link TemporaryLoggerFactory#switch} method is called.
*/
class TemporaryLoggerFactory implements LoggerFactory {
private bufferSpaces: number;
private readonly wrappers: { wrapper: WrappingLogger; label: string }[] = [];
private readonly buffer: { logger: Logger; level: LogLevel; message: string }[] = [];
public constructor(bufferSize = 1024) {
this.bufferSpaces = bufferSize;
}
public createLogger(label: string): WrappingLogger {
const wrapper = new WrappingLogger({
log: (level: LogLevel, message: string): Logger =>
this.bufferLogEntry(wrapper, level, message),
});
this.wrappers.push({ wrapper, label });
return wrapper;
}
private bufferLogEntry(logger: WrappingLogger, level: LogLevel, message: string): Logger {
// Buffer the message if spaces are still available
if (this.bufferSpaces > 0) {
this.bufferSpaces -= 1;
// If this is the last space, instead generate a warning through a new logger
if (this.bufferSpaces === 0) {
logger = this.createLogger('LazyLoggerFactory');
level = 'warn';
message = `Memory-buffered logging limit of ${this.buffer.length + 1} reached`;
}
this.buffer.push({ logger, level, message });
}
return logger;
}
/**
* Swaps all lazy loggers to new loggers from the given factory,
* and emits any buffered messages through those actual loggers.
*/
public switch(loggerFactory: LoggerFactory): void {
// Instantiate an actual logger within every lazy logger
for (const { wrapper, label } of this.wrappers.splice(0, this.wrappers.length)) {
wrapper.logger = loggerFactory.createLogger(label);
}
// Emit all buffered log messages
for (const { logger, level, message } of this.buffer.splice(0, this.buffer.length)) {
logger.log(level, message);
}
}
}
/**
* Wraps around another {@link LoggerFactory} that can be set lazily.
* This is useful when objects are instantiated (and when they create loggers)
* before the logging system has been fully instantiated,
* as is the case when using a dependency injection framework such as Components.js.
*
* 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}.
* Loggers can be created even before a {@link LoggerFactory} is set;
* any log messages will be buffered and re-emitted.
*/
export class LazyLoggerFactory implements LoggerFactory {
private static readonly instance = new LazyLoggerFactory();
private factory: LoggerFactory;
private ploggerFactory: LoggerFactory | undefined;
private constructor() {
// Singleton instance
}
public static getInstance(): LazyLoggerFactory {
return LazyLoggerFactory.instance;
}
public createLogger(label: string): Logger {
return new LazyLogger(this, label);
}
public resetLoggerFactory(): void {
this.ploggerFactory = undefined;
public constructor(options: { bufferSize?: number } = {}) {
this.factory = new TemporaryLoggerFactory(options.bufferSize);
}
public get loggerFactory(): LoggerFactory {
if (!this.ploggerFactory) {
throw new Error('No logger factory has been set. Can be caused by logger invocation during initialization.');
if (this.factory instanceof TemporaryLoggerFactory) {
throw new Error('Logger factory not yet set.');
}
return this.ploggerFactory;
return this.factory;
}
public set loggerFactory(loggerFactory: LoggerFactory) {
this.ploggerFactory = loggerFactory;
if (this.factory instanceof TemporaryLoggerFactory) {
this.factory.switch(loggerFactory);
}
this.factory = loggerFactory;
}
public createLogger(label: string): Logger {
return this.factory.createLogger(label);
}
}

View File

@@ -2,11 +2,12 @@ import { LazyLoggerFactory } from './LazyLoggerFactory';
import type { Logger } from './Logger';
import type { LoggerFactory } from './LoggerFactory';
let loggerFactoryWrapper = new LazyLoggerFactory();
let classLoggers = new WeakMap<Constructor, Logger>();
/**
* Gets a logger instance for the given class instance.
*
* The returned type of logger depends on the configured {@link LoggerFactory} in {@link Setup}.
*
* The following shows a typical pattern on how to create loggers:
* ```
* class MyClass {
@@ -21,32 +22,51 @@ import type { LoggerFactory } from './LoggerFactory';
* @param loggable - A class instance or a class string name.
*/
export function getLoggerFor(loggable: string | Instance): Logger {
return LazyLoggerFactory.getInstance()
.createLogger(typeof loggable === 'string' ? loggable : loggable.constructor.name);
let logger: Logger;
// Create a logger with a text label
if (typeof loggable === 'string') {
logger = loggerFactoryWrapper.createLogger(loggable);
// Create or reuse a logger for a specific class
} else {
const { constructor } = loggable;
if (classLoggers.has(constructor)) {
logger = classLoggers.get(constructor)!;
} else {
logger = loggerFactoryWrapper.createLogger(constructor.name);
classLoggers.set(constructor, logger);
}
}
return logger;
}
/**
* Sets the global logger factory.
* This will cause all loggers created by {@link getLoggerFor} to be delegated to a logger from the given factory.
* This causes loggers created by {@link getLoggerFor} to delegate to a logger from the given factory.
* @param loggerFactory - A logger factory.
*/
export function setGlobalLoggerFactory(loggerFactory: LoggerFactory): void {
LazyLoggerFactory.getInstance().loggerFactory = loggerFactory;
loggerFactoryWrapper.loggerFactory = loggerFactory;
}
/**
* Resets the global logger factory to undefined.
*
* This typically only needs to be called during testing.
* Call this at your own risk.
* Resets the internal logger factory, which holds the global logger factory.
* For testing purposes only.
*/
export function resetGlobalLoggerFactory(): void {
LazyLoggerFactory.getInstance().resetLoggerFactory();
export function resetInternalLoggerFactory(factory = new LazyLoggerFactory()): void {
loggerFactoryWrapper = factory;
classLoggers = new WeakMap();
}
/**
* Helper interface to identify class instances.
* Any class constructor.
*/
interface Constructor {
name: string;
}
/**
* Any class instance.
*/
interface Instance {
constructor: { name: string };
constructor: Constructor;
}

View File

@@ -1,11 +1,27 @@
import type { LogLevel } from './LogLevel';
/**
* Logs messages on a certain level.
* Logs messages on a specific level.
*
* @see getLoggerFor on how to instantiate loggers.
*/
export abstract class Logger {
export type BasicLogger = {
/**
* 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;
};
/**
* Logs messages, with convenience methods to log on a specific level.
*
* @see getLoggerFor on how to instantiate loggers.
*/
export abstract class Logger implements BasicLogger {
/**
* Log the given message at the given level.
* If the internal level is higher than the given level, the message may be voided.