import {Logger, LoggingLevel, LogMessage} from '../util' import {Singleton} from '../di' /** * A singleton service that manages loggers registered in the application, and * can be used to log output to all of them based on the configured logging level. * * This should be used in place of `console.log` as it also supports logging to * external locations. * * @example * ```typescript * logging.info('Info level!') * logging.debug('Some debugging information...') * logging.warn('A warning!', true) // true, to force it to show, regardless of logging level. * ``` */ @Singleton() export class Logging { /** Array of Logger implementations that should be logged to. */ protected registeredLoggers: Logger[] = [] /** The currently configured logging level. */ protected currentLevel: LoggingLevel = LoggingLevel.Warning /** Register a Logger implementation with this service. */ public registerLogger(logger: Logger): this { if ( !this.registeredLoggers.includes(logger) ) { this.registeredLoggers.push(logger) } return this } /** * Remove a Logger implementation from this service, if it is registered. * @param logger */ public unregisterLogger(logger: Logger): this { this.registeredLoggers = this.registeredLoggers.filter(x => x !== logger) return this } /** * Get the current logging level. */ public get level(): LoggingLevel { return this.currentLevel } /** * Set the current logging level. * @param level */ public set level(level: LoggingLevel) { this.currentLevel = level } /** * Write a success-level output to the logs. * @param output * @param force - if true, output even if outside the current logging level */ public success(output: unknown, force = false): void { this.writeLog(LoggingLevel.Success, output, force) } /** * Write an error-level output to the logs. * @param output * @param force - if true, output even if outside the current logging level */ public error(output: unknown, force = false): void { this.writeLog(LoggingLevel.Error, output, force) } /** * Write a warning-level output to the logs. * @param output * @param force - if true, output even if outside the current logging level */ public warn(output: unknown, force = false): void { this.writeLog(LoggingLevel.Warning, output, force) } /** * Write an info-level output to the logs. * @param output * @param force - if true, output even if outside the current logging level */ public info(output: unknown, force = false): void { this.writeLog(LoggingLevel.Info, output, force) } /** * Write a debugging-level output to the logs. * @param output * @param force - if true, output even if outside the current logging level */ public debug(output: unknown, force = false): void { this.writeLog(LoggingLevel.Debug, output, force) } /** * Write a verbose-level output to the logs. * @param output * @param force - if true, output even if outside the current logging level */ public verbose(output: unknown, force = false): void { this.writeLog(LoggingLevel.Verbose, output, force) } /** * Helper function to write the given output, at the given logging level, to * all of the registered loggers. * @param level * @param output * @param force - if true, output even if outside the current logging level * @protected */ protected writeLog(level: LoggingLevel, output: unknown, force = false): void { const message = this.buildMessage(level, output) if ( this.currentLevel >= level || force ) { for ( const logger of this.registeredLoggers ) { try { logger.write(message) } catch (e) { console.error('logging error', e) // eslint-disable-line no-console } } } } /** * Given a level and output item, build a formatted LogMessage with date and caller. * @param level * @param output * @protected */ protected buildMessage(level: LoggingLevel, output: unknown): LogMessage { return { level, output, date: new Date(), callerName: this.getCallerInfo(), } } /** * Get the name of the object that called the log method using error traces. * @param level * @protected */ protected getCallerInfo(level = 5): string { const e = new Error() if ( !e.stack ) { return 'Unknown' } return e.stack.split(/\s+at\s+/) .slice(level) .map((x: string): string => x.trim().split(' (')[0].split('.')[0].split(':')[0])[0] .split('/') .reverse()[0] } }