import { isDebug, type ITrace, type ITraceWith, type Supplier } from '@emprespresso/pengueno'; export type LogTraceSupplier = ITraceWith | Error>; const defaultTrace = () => `TimeStamp = ${new Date().toISOString()}`; export class LogTrace implements ITrace { constructor( private readonly logger: ILogger = new LoggerImpl(), private readonly traces: Array = [defaultTrace], private readonly defaultLevel: LogLevel = LogLevel.INFO, private readonly allowedLevels: Supplier> = defaultAllowedLevels, ) {} public addTrace(trace: LogTraceSupplier): ITrace { return new LogTrace(this.logger, this.traces.concat(trace), this.defaultLevel, this.allowedLevels); } public trace(trace: LogTraceSupplier) { const { traces, level: _level } = this.foldTraces(this.traces.concat(trace)); if (!this.allowedLevels().includes(_level)) return; const level = _level === LogLevel.UNKNOWN ? this.defaultLevel : _level; this.logger.log(level, ...traces); } private foldTraces(_traces: Array) { const _logTraces = _traces.map((trace) => (typeof trace === 'function' ? trace() : trace)); const _level = _logTraces .filter((trace) => isLogLevel(trace)) .reduce((acc, level) => Math.max(logLevelOrder.indexOf(level), acc), -1); const level = logLevelOrder[_level] ?? LogLevel.UNKNOWN; const traces = _logTraces .filter((trace) => !isLogLevel(trace)) .map((trace) => { if (typeof trace === 'object') { return `TracedException.Name = ${trace.name}, TracedException.Message = ${trace.message}, TracedException.Stack = ${trace.stack}`; } return trace; }); return { level, traces, }; } } export enum LogLevel { UNKNOWN = 'UNKNOWN', INFO = 'INFO', WARN = 'WARN', DEBUG = 'DEBUG', ERROR = 'ERROR', SYS = 'SYS', } const logLevelOrder: Array = [LogLevel.DEBUG, LogLevel.INFO, LogLevel.WARN, LogLevel.ERROR, LogLevel.SYS]; export const isLogLevel = (l: unknown): l is LogLevel => typeof l === 'string' && logLevelOrder.some((level) => level === l); const defaultAllowedLevels = () => [ LogLevel.UNKNOWN, ...(isDebug() ? [LogLevel.DEBUG] : []), LogLevel.INFO, LogLevel.WARN, LogLevel.ERROR, LogLevel.SYS, ] as Array; export interface ILogger { readonly log: (level: LogLevel, ...args: string[]) => void; } class LoggerImpl implements ILogger { private readonly textEncoder = new TextEncoder(); public log(level: LogLevel, ...trace: string[]) { const message = JSON.stringify( { level, trace, }, null, 4, ); const styled = `${this.getStyle(level)}${message}${ANSI.RESET}\n`; this.getStream(level)(this.textEncoder.encode(styled)); } private getStream(level: LogLevel) { if (level === LogLevel.ERROR) { return console.error; } return console.log; } private getStyle(level: LogLevel) { switch (level) { case LogLevel.UNKNOWN: case LogLevel.INFO: return `${ANSI.MAGENTA}`; case LogLevel.DEBUG: return `${ANSI.CYAN}`; case LogLevel.WARN: return `${ANSI.BRIGHT_YELLOW}`; case LogLevel.ERROR: return `${ANSI.BRIGHT_RED}`; case LogLevel.SYS: return `${ANSI.DIM}${ANSI.BLUE}`; } } } export const ANSI = { RESET: '\x1b[0m', BOLD: '\x1b[1m', DIM: '\x1b[2m', RED: '\x1b[31m', GREEN: '\x1b[32m', YELLOW: '\x1b[33m', BLUE: '\x1b[34m', MAGENTA: '\x1b[35m', CYAN: '\x1b[36m', WHITE: '\x1b[37m', BRIGHT_RED: '\x1b[91m', BRIGHT_YELLOW: '\x1b[93m', GRAY: '\x1b[90m', };