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< Array > = 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, ...msg: string[]) { const message = JSON.stringify({ level, msg, }, null, 2); const styled = `${this.getStyle(level)}${message}${ANSI.RESET}\n`; this.getStream(level).writeSync(this.textEncoder.encode(styled)); } private getStream(level: LogLevel) { if (level === LogLevel.ERROR) { return Deno.stderr; } return Deno.stdout; } 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", };