@comake/skl-js-engine
Version:
Standard Knowledge Language Javascript Engine
145 lines (124 loc) • 4.96 kB
text/typescript
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)` ];
}
}