Spaces:
Runtime error
Runtime error
| // utils/logger.js - PRODUCTION LOGGER | |
| import fs from 'fs'; | |
| import path from 'path'; | |
| import { fileURLToPath } from 'url'; | |
| const __filename = fileURLToPath(import.meta.url); | |
| const __dirname = path.dirname(__filename); | |
| class Logger { | |
| constructor(namespace, options = {}) { | |
| this.namespace = namespace; | |
| this.options = { | |
| writeToFile: options.writeToFile ?? (process.env.LOG_TO_FILE === 'true' || process.env.NODE_ENV === 'production'), | |
| logDir: options.logDir || path.join(__dirname, '..', 'logs'), | |
| maxFileSize: options.maxFileSize || 10 * 1024 * 1024, // 10MB | |
| maxFiles: options.maxFiles || 7, // Keep 7 days | |
| consoleOutput: options.consoleOutput ?? true, | |
| ...options | |
| }; | |
| this.colors = { | |
| INFO: '\x1b[36m', // Cyan | |
| ERROR: '\x1b[31m', // Red | |
| WARN: '\x1b[33m', // Yellow | |
| DEBUG: '\x1b[35m', // Magenta | |
| SUCCESS: '\x1b[32m', // Green | |
| REQUEST: '\x1b[34m', // Blue | |
| RESET: '\x1b[0m', | |
| }; | |
| this.icons = { | |
| INFO: 'ℹ️', | |
| ERROR: '❌', | |
| WARN: '⚠️', | |
| DEBUG: '🔍', | |
| SUCCESS: '✅', | |
| REQUEST: '🌐', | |
| }; | |
| if (this.options.writeToFile) { | |
| this._ensureLogDir(); | |
| this._cleanupOldLogs(); | |
| } | |
| } | |
| _ensureLogDir() { | |
| if (!fs.existsSync(this.options.logDir)) { | |
| fs.mkdirSync(this.options.logDir, { recursive: true }); | |
| } | |
| } | |
| _getLogFilePath(level) { | |
| const date = new Date().toISOString().split('T')[0]; // YYYY-MM-DD | |
| const filename = level === 'REQUEST' ? 'requests' : level === 'ERROR' ? 'error' : 'combined'; | |
| return path.join(this.options.logDir, `${date}-${filename}.log`); | |
| } | |
| _rotateLogIfNeeded(filepath) { | |
| if (!fs.existsSync(filepath)) return; | |
| const stats = fs.statSync(filepath); | |
| if (stats.size > this.options.maxFileSize) { | |
| const timestamp = Date.now(); | |
| const rotatedPath = filepath.replace('.log', `.${timestamp}.log`); | |
| fs.renameSync(filepath, rotatedPath); | |
| console.log(`🔄 Log rotated: ${path.basename(rotatedPath)}`); | |
| } | |
| } | |
| _writeToFile(level, logEntry) { | |
| if (!this.options.writeToFile) return; | |
| try { | |
| const filepath = this._getLogFilePath(level); | |
| this._rotateLogIfNeeded(filepath); | |
| const logLine = JSON.stringify(logEntry) + '\n'; | |
| fs.appendFileSync(filepath, logLine, 'utf8'); | |
| } catch (error) { | |
| console.error('Failed to write log:', error.message); | |
| } | |
| } | |
| _formatConsoleMessage(level, requestId, message, data) { | |
| const timestamp = new Date().toISOString().split('T')[1].split('.')[0]; | |
| const color = this.colors[level] || ''; | |
| const icon = this.icons[level] || ''; | |
| const reset = this.colors.RESET; | |
| const prefix = `${color}${icon} [${timestamp}] [${this.namespace}] [${requestId}]${reset}`; | |
| let dataStr = ''; | |
| if (data) { | |
| if (typeof data === 'object') { | |
| const keys = Object.keys(data); | |
| if (keys.length <= 3) { | |
| dataStr = ' ' + JSON.stringify(data); | |
| } else { | |
| dataStr = '\n' + JSON.stringify(data, null, 2); | |
| } | |
| } else { | |
| dataStr = ' ' + data; | |
| } | |
| } | |
| return `${prefix} ${message}${dataStr}`; | |
| } | |
| _log(level, requestId, message, data) { | |
| const timestamp = new Date().toISOString(); | |
| // Create structured log entry | |
| const logEntry = { | |
| timestamp, | |
| level, | |
| namespace: this.namespace, | |
| requestId, | |
| message, | |
| ...(data && { data }), | |
| pid: process.pid, | |
| memory: process.memoryUsage().heapUsed | |
| }; | |
| // Console output | |
| if (this.options.consoleOutput) { | |
| const consoleMsg = this._formatConsoleMessage(level, requestId, message, data); | |
| const logMethod = level === 'ERROR' ? console.error : console.log; | |
| logMethod(consoleMsg); | |
| } | |
| // File output | |
| this._writeToFile(level, logEntry); | |
| } | |
| info(requestId, message, data) { | |
| this._log('INFO', requestId, message, data); | |
| } | |
| error(requestId, message, error) { | |
| const errorData = error ? { | |
| message: error?.message || error, | |
| name: error?.name, | |
| code: error?.code, | |
| statusCode: error?.statusCode, | |
| ...(error?.stack ? { stack: error.stack.split('\n').slice(0, 5) } : {}) | |
| } : null; | |
| this._log('ERROR', requestId, message, errorData); | |
| } | |
| warn(requestId, message, data) { | |
| this._log('WARN', requestId, message, data); | |
| } | |
| debug(requestId, message, data) { | |
| if (process.env.DEBUG === 'true') { | |
| this._log('DEBUG', requestId, message, data); | |
| } | |
| } | |
| success(requestId, message, data) { | |
| this._log('SUCCESS', requestId, message, data); | |
| } | |
| request(requestId, message, params) { | |
| const sanitized = { ...params }; | |
| // Don't log sensitive data | |
| if (sanitized.meow) sanitized.meow = '***'; | |
| if (sanitized.bhaw) sanitized.bhaw = '***'; | |
| if (sanitized.apiKey) sanitized.apiKey = '***'; | |
| this._log('REQUEST', requestId, message, sanitized); | |
| } | |
| // Store response data to file | |
| storeResponse(requestId, message, responseData) { | |
| if (!this.options.writeToFile) return; | |
| try { | |
| const date = new Date().toISOString().split('T')[0]; | |
| const filepath = path.join(this.options.logDir, `${date}-prs-responses.log`); | |
| this._rotateLogIfNeeded(filepath); | |
| const logEntry = { | |
| timestamp: new Date().toISOString(), | |
| namespace: this.namespace, | |
| requestId, | |
| message, | |
| responseData, | |
| pid: process.pid | |
| }; | |
| const logLine = JSON.stringify(logEntry, null, 2) + '\n---\n'; | |
| fs.appendFileSync(filepath, logLine, 'utf8'); | |
| } catch (error) { | |
| console.error('Failed to store response:', error.message); | |
| } | |
| } | |
| // Cleanup old logs | |
| _cleanupOldLogs() { | |
| try { | |
| const files = fs.readdirSync(this.options.logDir); | |
| const cutoffDate = Date.now() - (this.options.maxFiles * 24 * 60 * 60 * 1000); | |
| let deleted = 0; | |
| files.forEach(file => { | |
| const filepath = path.join(this.options.logDir, file); | |
| const stats = fs.statSync(filepath); | |
| if (stats.mtimeMs < cutoffDate) { | |
| fs.unlinkSync(filepath); | |
| deleted++; | |
| } | |
| }); | |
| if (deleted > 0) { | |
| console.log(`🧹 Log cleanup: Deleted ${deleted} old log files`); | |
| } | |
| } catch (error) { | |
| console.error('Failed to cleanup logs:', error.message); | |
| } | |
| } | |
| cleanupOldLogs() { | |
| this._cleanupOldLogs(); | |
| } | |
| getStats() { | |
| try { | |
| const files = fs.readdirSync(this.options.logDir); | |
| let totalSize = 0; | |
| files.forEach(file => { | |
| const filepath = path.join(this.options.logDir, file); | |
| totalSize += fs.statSync(filepath).size; | |
| }); | |
| return { | |
| logDir: this.options.logDir, | |
| files: files.length, | |
| totalSize: `${(totalSize / 1024 / 1024).toFixed(2)} MB`, | |
| maxFileSize: `${(this.options.maxFileSize / 1024 / 1024).toFixed(2)} MB`, | |
| maxFiles: this.options.maxFiles | |
| }; | |
| } catch (error) { | |
| return { error: error.message }; | |
| } | |
| } | |
| } | |
| export const createLogger = (namespace, options) => new Logger(namespace, options); | |
| export default new Logger('APP'); |