import { isDebug, type ITrace, type ITraceWith, type SideEffect, type Supplier, } from "@emprespresso/pengueno"; export interface ILogger { log: (...args: unknown[]) => void; debug: (...args: unknown[]) => void; warn: (...args: unknown[]) => void; error: (...args: unknown[]) => void; } export enum LogLevel { UNKNOWN = "UNKNOWN", INFO = "INFO", WARN = "WARN", DEBUG = "DEBUG", ERROR = "ERROR", } const logLevelOrder: Array = [ LogLevel.DEBUG, LogLevel.INFO, LogLevel.WARN, LogLevel.ERROR, ]; export const isLogLevel = (l: string): l is LogLevel => logLevelOrder.some((level) => level === l); const defaultAllowedLevels = () => [ LogLevel.UNKNOWN, ...(isDebug() ? [LogLevel.DEBUG] : []), LogLevel.INFO, LogLevel.WARN, LogLevel.ERROR, ] as Array; export const logWithLevel = ( logger: ILogger, level: LogLevel, ): SideEffect => { switch (level) { case LogLevel.UNKNOWN: case LogLevel.INFO: return logger.log; case LogLevel.DEBUG: return logger.debug; case LogLevel.WARN: return logger.warn; case LogLevel.ERROR: return logger.error; } }; export type LogTraceSupplier = ITraceWith>; const defaultTrace = () => `[${new Date().toISOString()}]`; export const LoggerImpl = console; export class LogTrace implements ITrace { constructor( private readonly logger: ILogger = LoggerImpl, private readonly traces: Array = [defaultTrace], private readonly allowedLevels: Supplier< Array > = defaultAllowedLevels, private readonly defaultLevel: LogLevel = LogLevel.INFO, ) {} public addTrace(trace: LogTraceSupplier): ITrace { return new LogTrace( this.logger, this.traces.concat(trace), this.allowedLevels, this.defaultLevel, ); } public trace(trace: LogTraceSupplier) { const { line, level: _level } = this.foldTraces(this.traces.concat(trace)); if (!this.allowedLevels().includes(_level)) return; const level = _level === LogLevel.UNKNOWN ? this.defaultLevel : _level; logWithLevel(this.logger, level)(`[${level}]${line}`); } private foldTraces(traces: Array) { const { line, level } = traces.reduce( (acc: { line: string; level: number }, t) => { const val = typeof t === "function" ? t() : t; if (isLogLevel(val)) { return { ...acc, level: Math.max(logLevelOrder.indexOf(val), acc.level), }; } const line = [acc.line, val].join(" "); return { ...acc, line }; }, { line: "", level: -1 }, ); return { line, level: logLevelOrder[level] ?? LogLevel.UNKNOWN }; } }