import { join, resolve } from "path"; import { appendFile, mkdir, stat } from "fs/promises"; import { existsSync } from "fs"; export enum LogLevel { DEBUG = 0, INFO = 1, WARN = 2, ERROR = 3, } const LogLevelNames = { [LogLevel.DEBUG]: "DEBUG", [LogLevel.INFO]: "INFO", [LogLevel.WARN]: "WARN", [LogLevel.ERROR]: "ERROR", }; export type LogSource = "bot" | "web" | "shared" | "system"; export interface LogEntry { timestamp: string; level: string; source: LogSource; message: string; data?: any; stack?: string; } class Logger { private logDir: string; private errorLogPath: string; private initialized: boolean = false; private initPromise: Promise | null = null; constructor() { // Use resolve with __dirname or process.cwd() but make it more robust // Since this is in shared/lib/, we can try to find the project root // For now, let's stick to a resolved path from process.cwd() or a safer alternative this.logDir = resolve(process.cwd(), "logs"); this.errorLogPath = join(this.logDir, "error.log"); } private async ensureInitialized() { if (this.initialized) return; if (this.initPromise) return this.initPromise; this.initPromise = (async () => { try { await mkdir(this.logDir, { recursive: true }); this.initialized = true; } catch (err: any) { if (err.code === "EEXIST" || err.code === "ENOTDIR") { try { const stats = await stat(this.logDir); if (stats.isDirectory()) { this.initialized = true; return; } } catch (statErr) {} } console.error(`[SYSTEM] Failed to initialize logger directory at ${this.logDir}:`, err); } finally { this.initPromise = null; } })(); return this.initPromise; } private safeStringify(data: any): string { try { return JSON.stringify(data); } catch (err) { const seen = new WeakSet(); return JSON.stringify(data, (key, value) => { if (typeof value === "object" && value !== null) { if (seen.has(value)) return "[Circular]"; seen.add(value); } return value; }); } } private formatMessage(entry: LogEntry): string { const dataStr = entry.data ? ` | Data: ${this.safeStringify(entry.data)}` : ""; const stackStr = entry.stack ? `\nStack Trace:\n${entry.stack}` : ""; return `[${entry.timestamp}] [${entry.level}] [${entry.source.toUpperCase()}] ${entry.message}${dataStr}${stackStr}`; } private async writeToErrorLog(formatted: string) { await this.ensureInitialized(); try { await appendFile(this.errorLogPath, formatted + "\n"); } catch (err) { console.error("[SYSTEM] Failed to write to error log file:", err); } } private log(level: LogLevel, source: LogSource, message: string, errorOrData?: any) { const timestamp = new Date().toISOString(); const levelName = LogLevelNames[level]; const entry: LogEntry = { timestamp, level: levelName, source, message, }; if (level === LogLevel.ERROR && errorOrData instanceof Error) { entry.stack = errorOrData.stack; entry.message = `${message}: ${errorOrData.message}`; } else if (errorOrData !== undefined) { entry.data = errorOrData; } const formatted = this.formatMessage(entry); // Print to console switch (level) { case LogLevel.DEBUG: console.debug(formatted); break; case LogLevel.INFO: console.log(formatted); break; case LogLevel.WARN: console.warn(formatted); break; case LogLevel.ERROR: console.error(formatted); break; } // Persistent error logging if (level === LogLevel.ERROR) { this.writeToErrorLog(formatted).catch(() => { // Silently fail to avoid infinite loops }); } } debug(source: LogSource, message: string, data?: any) { this.log(LogLevel.DEBUG, source, message, data); } info(source: LogSource, message: string, data?: any) { this.log(LogLevel.INFO, source, message, data); } warn(source: LogSource, message: string, data?: any) { this.log(LogLevel.WARN, source, message, data); } error(source: LogSource, message: string, error?: any) { this.log(LogLevel.ERROR, source, message, error); } } export const logger = new Logger();