diff --git a/src/utils/logger.ts b/src/utils/logger.ts index d5dfb2e..5c2b79b 100644 --- a/src/utils/logger.ts +++ b/src/utils/logger.ts @@ -1,3 +1,5 @@ +import { inspect } from 'util'; + interface LogContext { requestId?: string; userId?: string; @@ -9,36 +11,194 @@ interface LogContext { } export class Logger { + private static getLogLevel(): string { + return process.env.LOG_LEVEL || (process.env.NODE_ENV === 'development' ? 'debug' : 'info'); + } + + private static shouldLog(level: string): boolean { + const currentLevel = this.getLogLevel().toLowerCase(); + const levels = ['error', 'warn', 'info', 'debug']; + const currentIndex = levels.indexOf(currentLevel); + const messageIndex = levels.indexOf(level.toLowerCase()); + + return messageIndex <= currentIndex; + } + private static formatMessage(level: string, message: string, context?: LogContext): string { const timestamp = new Date().toISOString(); - const baseLog = { - timestamp, - level, - message, - ...context - }; + const isDevelopment = process.env.NODE_ENV === 'development'; + const supportsColors = this.supportsColors(); + + // Create a more readable header with level, timestamp, and message + const levelSymbol = this.getLevelSymbol(level); + const levelColor = this.getLevelColor(level, isDevelopment && supportsColors); + const timestampFormatted = (isDevelopment && supportsColors) ? + `\x1b[90m${timestamp}\x1b[0m` : // Gray timestamp in dev + timestamp; + + const header = (isDevelopment && supportsColors) ? + `${levelColor}${levelSymbol} [${level}]\x1b[0m ${timestampFormatted} \x1b[1m${message}\x1b[0m` : + `${levelSymbol} [${level}] ${timestamp} ${message}`; + + // Format context data if present + if (context && Object.keys(context).length > 0) { + const contextFormatted = inspect(context, { + depth: isDevelopment ? 10 : 6, // Much deeper inspection + colors: isDevelopment && supportsColors, + compact: false, + breakLength: 100, + maxArrayLength: isDevelopment ? 20 : 10, // Show more array elements + maxStringLength: isDevelopment ? 500 : 200, // Longer strings + showHidden: false, + sorted: true, + getters: false, + showProxy: false, // Don't show proxy details + customInspect: true // Use custom inspect methods + }); + + // Add visual separation + const separator = (isDevelopment && supportsColors) ? + `\x1b[90m${'─'.repeat(80)}\x1b[0m` : + '─'.repeat(80); + + return `${header}\n${separator}\n${contextFormatted}`; + } + + return header; + } - return JSON.stringify(baseLog, null, process.env.NODE_ENV === 'development' ? 2 : 0); + private static supportsColors(): boolean { + // Check if colors are explicitly disabled + if (process.env.FORCE_COLOR === '0') return false; + if (process.env.NO_COLOR) return false; + if (process.env.TERM === 'dumb') return false; + + // Check if colors are explicitly enabled + if (process.env.FORCE_COLOR === '1' || process.env.FORCE_COLOR === '2' || process.env.FORCE_COLOR === '3') return true; + + // Check if we're in a TTY + if (process.stdout && !process.stdout.isTTY) return false; + + // Check terminal capabilities - be more permissive + if (process.env.TERM && ( + process.env.TERM.includes('256color') || + process.env.TERM.includes('truecolor') || + process.env.TERM.includes('xterm') || + process.env.TERM.includes('screen') || + process.env.TERM.includes('tmux') || + process.env.TERM.includes('color') + )) return true; + + // Check COLORTERM + if (process.env.COLORTERM) return true; + + // Default to true for development mode if we have a TTY + return process.env.NODE_ENV === 'development' && process.stdout.isTTY; + } + + private static getLevelSymbol(level: string): string { + const symbols = { + 'INFO': 'â„šī¸ ', + 'DEBUG': '🐛', + 'WARN': 'âš ī¸ ', + 'ERROR': '❌' + }; + return symbols[level as keyof typeof symbols] || '📝'; + } + + private static getLevelPrefix(level: string, supportsColors: boolean): string { + if (supportsColors) { + return ''; + } + + // Provide visual distinction without colors + const prefixes = { + 'ERROR': '>>> ', + 'WARN': '>>> ', + 'INFO': '>>> ', + 'DEBUG': '>>> ' + }; + return prefixes[level as keyof typeof prefixes] || '>>> '; + } + + private static getLevelColor(level: string, isDevelopment: boolean): string { + if (!isDevelopment) return ''; + + const colors = { + 'INFO': '\x1b[36m', // Cyan + 'DEBUG': '\x1b[35m', // Magenta + 'WARN': '\x1b[33m', // Yellow + 'ERROR': '\x1b[31m' // Red + }; + return colors[level as keyof typeof colors] || '\x1b[37m'; // White default } static info(message: string, context?: LogContext): void { - console.log(this.formatMessage('INFO', message, context)); + if (this.shouldLog('info')) { + console.log(this.formatMessage('INFO', message, context)); + } } static warn(message: string, context?: LogContext): void { - console.warn(this.formatMessage('WARN', message, context)); + if (this.shouldLog('warn')) { + console.warn(this.formatMessage('WARN', message, context)); + } } static error(message: string, context?: LogContext): void { - console.error(this.formatMessage('ERROR', message, context)); + if (this.shouldLog('error')) { + console.error(this.formatMessage('ERROR', message, context)); + } } static debug(message: string, context?: LogContext): void { - if (process.env.NODE_ENV === 'development') { + if (this.shouldLog('debug')) { console.debug(this.formatMessage('DEBUG', message, context)); } } + // Enhanced logging methods that automatically detect and format objects + static logObject(message: string, obj: any, level: 'info' | 'debug' | 'warn' | 'error' = 'info'): void { + const isDevelopment = process.env.NODE_ENV === 'development'; + const supportsColors = this.supportsColors(); + + // Create a more readable object representation + const formattedObj = inspect(obj, { + depth: isDevelopment ? 10 : 6, // Much deeper inspection + colors: isDevelopment && supportsColors, + compact: false, // Always use expanded format for objects + breakLength: 100, + maxArrayLength: isDevelopment ? 20 : 10, + maxStringLength: isDevelopment ? 500 : 200, + showHidden: false, + sorted: true, + getters: false, + showProxy: false, + customInspect: true + }); + + const context = { + objectType: obj?.constructor?.name || typeof obj, + objectSize: JSON.stringify(obj).length, + formattedObject: formattedObj + }; + + switch (level) { + case 'info': + this.info(message, context); + break; + case 'debug': + this.debug(message, context); + break; + case 'warn': + this.warn(message, context); + break; + case 'error': + this.error(message, context); + break; + } + } + // Helper for HTTP request logging static logRequest(req: any, res: any, duration: number): void { const context = { @@ -78,4 +238,37 @@ export class Logger { this.error(message, logContext); } } + + + // Helper for logging API responses with better formatting + static logApiResponse(operation: string, response: any, level: 'info' | 'debug' | 'warn' | 'error' = 'info'): void { + const context = { + operation, + responseType: response?.constructor?.name || typeof response, + statusCode: response?.status || response?.statusCode, + hasData: !!response?.data, + dataKeys: response?.data ? Object.keys(response.data) : [], + responseSize: JSON.stringify(response).length + }; + + // Add the actual response data for detailed inspection + if (process.env.NODE_ENV === 'development') { + context['responseData'] = response; + } + + switch (level) { + case 'info': + this.info(`API Response: ${operation}`, context); + break; + case 'debug': + this.debug(`API Response: ${operation}`, context); + break; + case 'warn': + this.warn(`API Response: ${operation}`, context); + break; + case 'error': + this.error(`API Response: ${operation}`, context); + break; + } + } }