UNPKG

@freemework/common

Version:

Common library of the Freemework Project.

472 lines (403 loc) 15.9 kB
import { FException } from "../exception/f_exception.js"; import { FExceptionInvalidOperation } from "../exception/f_exception_invalid_operation.js"; import { FLoggerLevel } from "./f_logger_level.js"; import { FExecutionContext } from "../execution_context/f_execution_context.js"; import { FLoggerLabelValue } from "./f_logger_labels.js"; import { FUtilUnReadonly } from "../util/index.js"; import { FLoggerLabelsExecutionContext } from "./f_logger_labels_execution_context.js"; export interface LoggerFactory { (loggerName: string): FLogger; } export interface FLoggerMessageFactory { (): string; } export abstract class FLogger { private static _loggerFactory: LoggerFactory | null = null; private static get loggerFactory(): LoggerFactory { if (this._loggerFactory === null) { console.error( "Logging subsystem used before call FLogger.setLoggerFactory(). Use FLoggerConsole as default logger. Please, consider to call FLogger.setLoggerFactory() at bootstrap phase." ); this._loggerFactory = (loggerName: string) => FLoggerConsole.create(loggerName, { level: FLoggerLevel.TRACE }); } return this._loggerFactory; } public static setLoggerFactory(factory: LoggerFactory) { if (FLogger._loggerFactory !== null) { throw new FExceptionInvalidOperation( "Cannot redefine logger factory by call setLoggerFactory(). Logger factory already set." ); } FLogger._loggerFactory = factory; } /** * Factory constructor */ public static create(loggerName: string): FLogger { return FLogger.loggerFactory(loggerName); } public abstract get isTraceEnabled(): boolean; public abstract get isDebugEnabled(): boolean; public abstract get isInfoEnabled(): boolean; public abstract get isWarnEnabled(): boolean; public abstract get isErrorEnabled(): boolean; public abstract get isFatalEnabled(): boolean; public abstract get name(): string | null; public abstract trace(executionContext: FExecutionContext, message: string, ex?: FException): void; public abstract trace(executionContext: FExecutionContext, messageFactory: FLoggerMessageFactory, ex?: FException): void; public abstract debug(executionContext: FExecutionContext, message: string, ex?: FException): void; public abstract debug(executionContext: FExecutionContext, messageFactory: FLoggerMessageFactory, ex?: FException): void; public abstract info(executionContext: FExecutionContext, message: string): void; public abstract info(executionContext: FExecutionContext, messageFactory: FLoggerMessageFactory): void; public abstract warn(executionContext: FExecutionContext, message: string): void; public abstract warn(executionContext: FExecutionContext, messageFactory: FLoggerMessageFactory): void; public abstract error(executionContext: FExecutionContext, message: string): void; public abstract error(executionContext: FExecutionContext, messageFactory: FLoggerMessageFactory): void; public abstract fatal(executionContext: FExecutionContext, message: string): void; public abstract fatal(executionContext: FExecutionContext, messageFactory: FLoggerMessageFactory): void; public abstract log( executionContextOrLabels: FExecutionContext | ReadonlyArray<FLoggerLabelValue>, level: FLoggerLevel, messageOrMessageFactory: string | FLoggerMessageFactory, ex?: FException, ): void; } export abstract class FLoggerBase extends FLogger { public override get isTraceEnabled(): boolean { return this.isLevelEnabled(FLoggerLevel.TRACE); } public override get isDebugEnabled(): boolean { return this.isLevelEnabled(FLoggerLevel.DEBUG); } public override get isInfoEnabled(): boolean { return this.isLevelEnabled(FLoggerLevel.INFO); } public override get isWarnEnabled(): boolean { return this.isLevelEnabled(FLoggerLevel.WARN); } public override get isErrorEnabled(): boolean { return this.isLevelEnabled(FLoggerLevel.ERROR); } public override get isFatalEnabled(): boolean { return this.isLevelEnabled(FLoggerLevel.FATAL); } public override get name(): string { return this._name; } public override trace( variant: FExecutionContext | ReadonlyArray<FLoggerLabelValue>, messageOrMessageFactory: string | FLoggerMessageFactory, ex?: FException, ): void { if (!this.isTraceEnabled) { return; } const loggerLabels: ReadonlyArray<FLoggerLabelValue> = FLoggerBase._resolveLoggerLabels(variant); const message: string = FLoggerBase._resolveMessage(messageOrMessageFactory); this.writeToOutput(FLoggerLevel.TRACE, loggerLabels, message, ex); } public override debug( variant: FExecutionContext | ReadonlyArray<FLoggerLabelValue>, messageOrMessageFactory: string | FLoggerMessageFactory, ex?: FException, ): void { if (!this.isDebugEnabled) { return; } const loggerLabels: ReadonlyArray<FLoggerLabelValue> = FLoggerBase._resolveLoggerLabels(variant); const message: string = FLoggerBase._resolveMessage(messageOrMessageFactory); this.writeToOutput(FLoggerLevel.DEBUG, loggerLabels, message, ex); } public override info( variant: FExecutionContext | ReadonlyArray<FLoggerLabelValue>, messageOrMessageFactory: string | FLoggerMessageFactory, ): void { if (!this.isInfoEnabled) { return; } const loggerLabels: ReadonlyArray<FLoggerLabelValue> = FLoggerBase._resolveLoggerLabels(variant); const message: string = FLoggerBase._resolveMessage(messageOrMessageFactory); this.writeToOutput(FLoggerLevel.INFO, loggerLabels, message); } public override warn( variant: FExecutionContext | ReadonlyArray<FLoggerLabelValue>, messageOrMessageFactory: string | FLoggerMessageFactory, ): void { if (!this.isWarnEnabled) { return; } const loggerLabels: ReadonlyArray<FLoggerLabelValue> = FLoggerBase._resolveLoggerLabels(variant); const message: string = FLoggerBase._resolveMessage(messageOrMessageFactory); this.writeToOutput(FLoggerLevel.WARN, loggerLabels, message); } public override error( variant: FExecutionContext | ReadonlyArray<FLoggerLabelValue>, messageOrMessageFactory: string | FLoggerMessageFactory, ): void { if (!this.isErrorEnabled) { return; } const loggerLabels: ReadonlyArray<FLoggerLabelValue> = FLoggerBase._resolveLoggerLabels(variant); const message: string = FLoggerBase._resolveMessage(messageOrMessageFactory); this.writeToOutput(FLoggerLevel.ERROR, loggerLabels, message); } public override fatal( variant: FExecutionContext | ReadonlyArray<FLoggerLabelValue>, messageOrMessageFactory: string | FLoggerMessageFactory, ): void { if (!this.isFatalEnabled) { return; } const loggerLabels: ReadonlyArray<FLoggerLabelValue> = FLoggerBase._resolveLoggerLabels(variant); const message: string = FLoggerBase._resolveMessage(messageOrMessageFactory); this.writeToOutput(FLoggerLevel.FATAL, loggerLabels, message); } public override log( executionContextOrLabels: FExecutionContext | ReadonlyArray<FLoggerLabelValue>, level: FLoggerLevel, messageOrMessageFactory: string | FLoggerMessageFactory, ex?: FException, ): void { if (!this.isLevelEnabled(level)) { return; } const loggerLabels: ReadonlyArray<FLoggerLabelValue> = FLoggerBase._resolveLoggerLabels(executionContextOrLabels); const message: string = FLoggerBase._resolveMessage(messageOrMessageFactory); this.writeToOutput(level, loggerLabels, message, ex); } protected constructor(loggerName: string) { super(); this._name = loggerName; } protected abstract isLevelEnabled(level: FLoggerLevel): boolean; /** * Unconditionally(without check logLevel settings) write message to logger output. * * Override this method to implement custom logger. */ protected abstract writeToOutput( level: FLoggerLevel, labelValues: ReadonlyArray<FLoggerLabelValue>, message: string, ex?: FException, ): void; private readonly _name: string; public static _resolveLoggerLabels( variant: FExecutionContext | ReadonlyArray<FLoggerLabelValue>, ): ReadonlyArray<FLoggerLabelValue> { if (variant === null || variant === undefined) { // Sometime users pass undefined/null value. // It is contract violation, but not a reason to crash in logger return FLoggerBase._emptyLabelValues; } else if (variant instanceof FExecutionContext) { const executionElement = FLoggerLabelsExecutionContext .of(variant); if (executionElement !== null) { return executionElement.loggerLabelValues; } else { return FLoggerBase._emptyLabelValues; // No any logger properties on execution context chain } } else { return variant; } } public static _resolveMessage(messageOrMessageFactory: string | FLoggerMessageFactory): string { if (typeof messageOrMessageFactory === "function") { try { messageOrMessageFactory = messageOrMessageFactory(); if (typeof messageOrMessageFactory !== "string") { messageOrMessageFactory = `FLoggerMessageFactory contract violation detected: Non-string result. Force stringify message: ${messageOrMessageFactory}`; } } catch (e) { messageOrMessageFactory = "FLoggerMessageFactory contract violation detected: Exception was raised." } } else { if (typeof messageOrMessageFactory !== "string") { messageOrMessageFactory = `FLogger contract violation detected: Non-string message passed. Force stringify message: ${messageOrMessageFactory}`; } } return messageOrMessageFactory; } private static readonly _emptyLabelValues: ReadonlyArray<FLoggerLabelValue> = Object.freeze([]); } export abstract class FLoggerBaseWithLevel extends FLoggerBase { public static buildLoggerLevelsMap(level: FLoggerLevel | null): Map<FLoggerLevel, boolean> { const levels: Map<FLoggerLevel, boolean> = new Map(); levels.set(FLoggerLevel.FATAL, level != null && level >= FLoggerLevel.FATAL); levels.set(FLoggerLevel.ERROR, level != null && level >= FLoggerLevel.ERROR); levels.set(FLoggerLevel.WARN, level != null && level >= FLoggerLevel.WARN); levels.set(FLoggerLevel.INFO, level != null && level >= FLoggerLevel.INFO); levels.set(FLoggerLevel.DEBUG, level != null && level >= FLoggerLevel.DEBUG); levels.set(FLoggerLevel.TRACE, level != null && level >= FLoggerLevel.TRACE); return levels; } public constructor(loggerName: string, level: FLoggerLevel) { super(loggerName); // eslint-disable-next-line func-names const specificLogLevel: FLoggerLevel | null = (function () { const logLevelEnvironmentKey = `LOG_LEVEL_${loggerName}`; if (logLevelEnvironmentKey in process.env) { const specificLogLevelStr = process.env[logLevelEnvironmentKey]; if (specificLogLevelStr !== undefined) { try { return FLoggerLevel.parse(specificLogLevelStr.toUpperCase()); } catch (e) { console.error(`Unable to parse a value of environment variable '${logLevelEnvironmentKey}'.`); } } } return null; })(); this.levels = FLoggerBaseWithLevel.buildLoggerLevelsMap( specificLogLevel !== null ? specificLogLevel : level, ); } protected isLevelEnabled(level: FLoggerLevel): boolean { const isEnabled: boolean | undefined = this.levels.get(level); return isEnabled === true; } private readonly levels: Map<FLoggerLevel, boolean>; } export abstract class FLoggerConsole extends FLoggerBaseWithLevel { /** * Factory constructor */ public static override create(loggerName: string, opts?: { readonly level?: FLoggerLevel, readonly format?: FLoggerConsole.Format; readonly output?: "stdout" | "stderr"; }): FLogger { const level: FLoggerLevel = opts !== undefined && opts.level !== undefined ? opts.level : FLoggerLevel.TRACE; const format: FLoggerConsole.Format = opts !== undefined && opts.format !== undefined ? opts.format : "text"; const output: "stdout" | "stderr" | null = opts !== undefined && opts.output !== undefined ? opts.output : null; if (format === "json") { return new FLoggerConsoleJsonImpl(loggerName, level, output); } else { return new FLoggerConsoleTextImpl(loggerName, level, output); } } } export namespace FLoggerConsole { export type Format = "text" | "json"; } class FLoggerConsoleTextImpl extends FLoggerConsole { private readonly output: "stdout" | "stderr" | null; public constructor(loggerName: string, level: FLoggerLevel, output: "stdout" | "stderr" | null) { super(loggerName, level); this.output = output; } protected writeToOutput( level: FLoggerLevel, labelValues: ReadonlyArray<FLoggerLabelValue>, message: string, ex?: FException ): void { let logMessageBuffer = `${new Date().toISOString()} ${this.name} [${level}]`; for (const { name, value } of labelValues) { logMessageBuffer += `(${name}:${value})`; } logMessageBuffer += (" "); logMessageBuffer += message; logMessageBuffer += "\n"; if (ex != undefined) { logMessageBuffer += ex.toString(); logMessageBuffer += "\n"; } if (this.output === null) { switch (level) { case FLoggerLevel.TRACE: case FLoggerLevel.DEBUG: console.debug(logMessageBuffer); break; case FLoggerLevel.INFO: console.log(logMessageBuffer); break; case FLoggerLevel.WARN: case FLoggerLevel.ERROR: case FLoggerLevel.FATAL: console.error(logMessageBuffer); break; default: throw new FExceptionInvalidOperation(`Unsupported log level '${level}'.`); } } else if (this.output === "stdout") { console.log(logMessageBuffer); } else if (this.output === "stderr") { console.error(logMessageBuffer); } } } interface FLoggerConsoleJsonLogEntryBase { readonly [label: string]: string; readonly name: string; readonly date: string; readonly level: string; readonly message: string; } interface FLoggerConsoleJsonLogEntryWithException extends FLoggerConsoleJsonLogEntryBase { readonly 'exception.name': string; readonly 'exception.message': string; } type FLoggerConsoleJsonLogEntry = FLoggerConsoleJsonLogEntryBase | FLoggerConsoleJsonLogEntryWithException; class FLoggerConsoleJsonImpl extends FLoggerBaseWithLevel { public static formatLogMessage( loggerName: string, level: FLoggerLevel, labelValues: ReadonlyArray<FLoggerLabelValue>, message: string, ex?: FException | null, ): string { const logEntryBase: Pick<FLoggerConsoleJsonLogEntry, 'name' | 'date' | 'level'> = { name: loggerName, date: new Date().toISOString(), level: level.toString(), }; const labelsObj: Record<string, string> = {}; for (const { name, value } of labelValues) { labelsObj[`label.${name}`] = value; } const logEntry: FUtilUnReadonly<FLoggerConsoleJsonLogEntry> = { ...logEntryBase, ...labelsObj, message, }; if (ex !== undefined && ex != null) { logEntry["exception.name"] = ex.name; logEntry["exception.message"] = ex.message; if (ex.stack !== undefined) { logEntry["exception.stack"] = ex.stack; } } const logMessage: string = JSON.stringify(logEntry); return logMessage; } private readonly output: "stdout" | "stderr" | null; public constructor(loggerName: string, level: FLoggerLevel, output: "stdout" | "stderr" | null) { super(loggerName, level); this.output = output; } protected writeToOutput( level: FLoggerLevel, labelValues: ReadonlyArray<FLoggerLabelValue>, message: string, ex?: FException, ): void { const logMessage: string = FLoggerConsoleJsonImpl.formatLogMessage( this.name, level, labelValues, message, ex, ); if (this.output === null) { switch (level) { case FLoggerLevel.TRACE: case FLoggerLevel.DEBUG: console.debug(logMessage); break; case FLoggerLevel.INFO: console.log(logMessage); break; case FLoggerLevel.WARN: case FLoggerLevel.ERROR: case FLoggerLevel.FATAL: console.error(logMessage); break; } } else if (this.output === "stdout") { console.log(logMessage); } else if (this.output === "stderr") { console.error(logMessage); } } }