UNPKG

@comake/skl-js-engine

Version:

Standard Knowledge Language Javascript Engine

145 lines (124 loc) 4.96 kB
import { inspect } from 'util'; export interface LoggerOptions { maxStringLength?: number; maxArrayItems?: number; } export class Logger { private static instance: Logger; private readonly isDebug: boolean; private metadata: Record<string, any>; // Maximum characters to keep for long strings private readonly maxStringLength: number; // Maximum number of items to show for long arrays private readonly maxArrayItems: number; private constructor(isDebug: boolean, metadata: Record<string, any>, options?: LoggerOptions) { this.isDebug = true; this.metadata = metadata; this.maxStringLength = options?.maxStringLength ?? 2_000; this.maxArrayItems = options?.maxArrayItems ?? 10; } /** * Returns a singleton instance of the logger. The logger honours the DEBUG env variable unless * explicitly overridden via the `isDebug` parameter. */ public static getInstance(isDebug?: boolean, metadata?: Record<string, any>, options?: LoggerOptions): Logger { if (!Logger.instance) { // eslint-disable-next-line no-process-env const debugEnabled = isDebug ?? process.env.DEBUG === 'true'; Logger.instance = new Logger(debugEnabled, metadata ?? {}, options); } return Logger.instance; } /** * Merges the given metadata with the existing metadata attached to the logger. */ public setMetadata(metadata: Record<string, any>): void { this.metadata = { ...this.metadata, ...metadata }; } public getMetadataString(): string | undefined { if (!this.metadata || Object.keys(this.metadata).length === 0) { return undefined; } return JSON.stringify(this.metadata); } // --------------------------------------------------------------------------- // Public logging APIs // --------------------------------------------------------------------------- public log(...args: any[]): void { if (this.isDebug) { const metadataString = this.getMetadataString(); const logArgs = metadataString ? [ ...this.formatArgs(args), metadataString ] : this.formatArgs(args); // eslint-disable-next-line no-console console.log(...logArgs); } } public error(...args: any[]): void { if (this.isDebug) { const metadataString = this.getMetadataString(); const logArgs = metadataString ? [ ...this.formatArgs(args), metadataString ] : this.formatArgs(args); // eslint-disable-next-line no-console console.error(...logArgs); } } public debug(...args: any[]): void { if (this.isDebug) { const metadataString = this.getMetadataString(); const logArgs = metadataString ? [ ...this.formatArgs(args), metadataString ] : this.formatArgs(args); // eslint-disable-next-line no-console console.debug(...logArgs); } } // --------------------------------------------------------------------------- // Private helpers // --------------------------------------------------------------------------- /** * Applies safe formatting to every argument before it is passed to the console. Large strings are * truncated, large arrays/objects are abbreviated and circular references are handled gracefully * by `util.inspect`. */ private formatArgs(args: any[]): any[] { return args.map((arg: any): any => this.formatValue(arg)); } private formatValue(value: any): any { try { if (typeof value === 'string') { return this.truncateString(value); } if (Array.isArray(value)) { return JSON.stringify(this.truncateArray(value)); } if (typeof value === 'object' && value !== null) { // For objects we rely on util.inspect which gives us fine-grained control over depth, // array length and string length. return inspect(value, { depth: 6, maxArrayLength: this.maxArrayItems, maxStringLength: this.maxStringLength, breakLength: 120, compact: false, colors: true }); } // Primitives, functions, etc. are returned as-is. return value; } catch (err: unknown) { // In the unlikely event that formatting fails, fall back to a best-effort stringify. // eslint-disable-next-line no-console console.warn('Logger: failed to format value', err); return String(value); } } private truncateString(str: string): string { if (str.length <= this.maxStringLength) { return str; } return `${str.slice(0, this.maxStringLength)}...(truncated ${str.length - this.maxStringLength} chars)`; } private truncateArray(arr: any[]): any[] { if (arr.length <= this.maxArrayItems) { return arr.map((item: any): any => this.formatValue(item)); } const displayedItems = arr.slice(0, this.maxArrayItems).map((item: any): any => this.formatValue(item)); return [ ...displayedItems, `...(truncated ${arr.length - this.maxArrayItems} items)` ]; } }