diff --git a/web/src/server/utils/logger.ts b/web/src/server/utils/logger.ts new file mode 100644 index 00000000..07b2f8eb --- /dev/null +++ b/web/src/server/utils/logger.ts @@ -0,0 +1,180 @@ +import * as fs from 'fs'; +import * as path from 'path'; +import * as os from 'os'; +import chalk from 'chalk'; + +// Log file path +const LOG_DIR = path.join(os.homedir(), '.vibetunnel'); +const LOG_FILE = path.join(LOG_DIR, 'log.txt'); + +// Debug mode flag +let debugMode = false; + +// File handle for log file +let logFileHandle: fs.WriteStream | null = null; + +// ANSI color codes for stripping from file output +// eslint-disable-next-line no-control-regex +const ANSI_PATTERN = /\x1b\[[0-9;]*m/g; + +/** + * Initialize the logger - creates log directory and file + */ +export function initLogger(debug: boolean = false): void { + debugMode = debug; + + try { + // Ensure log directory exists + if (!fs.existsSync(LOG_DIR)) { + fs.mkdirSync(LOG_DIR, { recursive: true }); + } + + // Delete old log file if it exists + if (fs.existsSync(LOG_FILE)) { + fs.unlinkSync(LOG_FILE); + } + + // Create new log file write stream + logFileHandle = fs.createWriteStream(LOG_FILE, { flags: 'a' }); + } catch (error) { + // Don't throw, just log to console + console.error('Failed to initialize log file:', error); + } +} + +/** + * Close the logger + */ +export function closeLogger(): void { + if (logFileHandle) { + logFileHandle.end(); + logFileHandle = null; + } +} + +/** + * Format log message with timestamp + */ +function formatMessage( + level: string, + module: string, + args: any[] +): { console: string; file: string } { + const timestamp = new Date().toISOString(); + + // Format arguments + const message = args + .map((arg) => { + if (typeof arg === 'object') { + try { + // Use JSON.stringify with 2-space indent for objects + return JSON.stringify(arg, null, 2); + } catch { + return String(arg); + } + } + return String(arg); + }) + .join(' '); + + // Console format with colors + let consoleFormat: string; + const moduleColor = chalk.cyan(`[${module}]`); + const timestampColor = chalk.gray(timestamp); + + switch (level) { + case 'ERROR': + consoleFormat = `${timestampColor} ${chalk.red(level)} ${moduleColor} ${chalk.red(message)}`; + break; + case 'WARN': + consoleFormat = `${timestampColor} ${chalk.yellow(level)} ${moduleColor} ${chalk.yellow(message)}`; + break; + case 'DEBUG': + consoleFormat = `${timestampColor} ${chalk.magenta(level)} ${moduleColor} ${chalk.gray(message)}`; + break; + default: // LOG + consoleFormat = `${timestampColor} ${chalk.green(level)} ${moduleColor} ${message}`; + } + + // File format (no colors) + const fileFormat = `${timestamp} ${level.padEnd(5)} [${module}] ${message}`; + + return { console: consoleFormat, file: fileFormat }; +} + +/** + * Write to log file + */ +function writeToFile(message: string): void { + if (logFileHandle) { + try { + // Strip ANSI color codes from message + const cleanMessage = message.replace(ANSI_PATTERN, ''); + logFileHandle.write(cleanMessage + '\n'); + } catch { + // Silently ignore file write errors + } + } +} + +/** + * Enable or disable debug mode + */ +export function setDebugMode(enabled: boolean): void { + debugMode = enabled; +} + +/** + * Log from a specific module (used by client-side API) + */ +export function logFromModule(level: string, module: string, args: any[]): void { + if (level === 'DEBUG' && !debugMode) return; + + const { console: consoleMsg, file: fileMsg } = formatMessage(level, module, args); + + // Log to console + switch (level) { + case 'ERROR': + console.error(consoleMsg); + break; + case 'WARN': + console.warn(consoleMsg); + break; + default: + console.log(consoleMsg); + } + + // Log to file + writeToFile(fileMsg); +} + +/** + * Create a logger for a specific module + * This is the main factory function that should be used + */ +export function createLogger(moduleName: string) { + return { + log: (...args: any[]) => { + const { console: consoleMsg, file: fileMsg } = formatMessage('LOG', moduleName, args); + console.log(consoleMsg); + writeToFile(fileMsg); + }, + warn: (...args: any[]) => { + const { console: consoleMsg, file: fileMsg } = formatMessage('WARN', moduleName, args); + console.warn(consoleMsg); + writeToFile(fileMsg); + }, + error: (...args: any[]) => { + const { console: consoleMsg, file: fileMsg } = formatMessage('ERROR', moduleName, args); + console.error(consoleMsg); + writeToFile(fileMsg); + }, + debug: (...args: any[]) => { + if (debugMode) { + const { console: consoleMsg, file: fileMsg } = formatMessage('DEBUG', moduleName, args); + console.log(consoleMsg); + writeToFile(fileMsg); + } + }, + }; +}