"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.LoggingService = exports.LogLevel = void 0; const fs_1 = __importDefault(require("fs")); const path_1 = __importDefault(require("path")); const Minio = __importStar(require("minio")); var LogLevel; (function (LogLevel) { LogLevel["REQUEST"] = "REQUEST"; LogLevel["ERROR"] = "ERROR"; LogLevel["WARNING"] = "WARNING"; LogLevel["AUTH"] = "AUTH"; LogLevel["DATABASE"] = "DATABASE"; LogLevel["STARTUP"] = "STARTUP"; LogLevel["CONNECTION"] = "CONNECTION"; LogLevel["OTHER"] = "OTHER"; })(LogLevel || (exports.LogLevel = LogLevel = {})); class LoggingService { constructor() { this.minioClient = null; this.logBuffer = []; this.currentLogFile = null; this.logCount = 0; this.maxLogsPerFile = parseInt(process.env.MAX_LOGS_PER_FILE || '10000'); this.logsDir = path_1.default.join(process.cwd(), 'logs'); this.bucketName = process.env.MINIO_BUCKET_NAME || 'serpentrace-logs'; this.uploadInterval = null; this.initializeLogsDirectory(); this.initializeMinioClient(); this.createNewLogFile(); if (process.env.NODE_ENV !== 'test') { this.startPeriodicUpload(); } process.on('SIGTERM', () => this.shutdown()); process.on('SIGINT', () => this.shutdown()); process.on('beforeExit', () => this.shutdown()); } static getInstance() { if (!LoggingService.instance) { LoggingService.instance = new LoggingService(); } return LoggingService.instance; } initializeLogsDirectory() { try { if (!fs_1.default.existsSync(this.logsDir)) { fs_1.default.mkdirSync(this.logsDir, { recursive: true }); } // Create monthly subdirectory const monthlyDir = this.getMonthlyDirectory(); if (!fs_1.default.existsSync(monthlyDir)) { fs_1.default.mkdirSync(monthlyDir, { recursive: true }); } } catch (error) { console.error('Failed to initialize logs directory:', error); } } initializeMinioClient() { try { // Check if in production or development if (process.env.NODE_ENV === 'production') { if (process.env.MINIO_ENDPOINT && process.env.MINIO_ACCESS_KEY && process.env.MINIO_SECRET_KEY) { this.minioClient = new Minio.Client({ endPoint: process.env.MINIO_ENDPOINT, port: parseInt(process.env.MINIO_PORT || '9000'), useSSL: process.env.MINIO_USE_SSL === 'true', accessKey: process.env.MINIO_ACCESS_KEY, secretKey: process.env.MINIO_SECRET_KEY }); this.ensureBucketExists(); } else { console.warn('Minio configuration not found. Logs will only be stored locally and in console.'); } } else { // Development-specific Minio configuration this.minioClient = new Minio.Client({ endPoint: 'localhost', port: 9000, useSSL: false, accessKey: 'serpentrace', secretKey: 'serpentrace123!' }); this.ensureBucketExists(); } } catch (error) { console.error('Failed to initialize Minio client:', error); this.minioClient = null; } } async ensureBucketExists() { if (!this.minioClient) return; try { const exists = await this.minioClient.bucketExists(this.bucketName); if (!exists) { await this.minioClient.makeBucket(this.bucketName); this.log(LogLevel.STARTUP, `Created Minio bucket: ${this.bucketName}`); } } catch (error) { console.error('Failed to ensure bucket exists:', error); } } startPeriodicUpload() { // Upload current log file to Minio every 2 minutes this.uploadInterval = setInterval(async () => { if (this.currentLogFile && this.minioClient) { await this.uploadToMinio(this.currentLogFile); } }, 2 * 60 * 1000); // 2 minutes } getMonthlyDirectory() { const now = new Date(); const year = now.getFullYear(); const month = String(now.getMonth() + 1).padStart(2, '0'); return path_1.default.join(this.logsDir, `${year}-${month}`); } getMonthlyMinioPrefix() { const now = new Date(); const year = now.getFullYear(); const month = String(now.getMonth() + 1).padStart(2, '0'); return `${year}-${month}/`; } createNewLogFile() { const now = new Date(); const timestamp = now.toISOString().replace(/[:.]/g, '-'); const fileName = `serpentrace-${timestamp}.log`; this.currentLogFile = path_1.default.join(this.getMonthlyDirectory(), fileName); this.logCount = 0; // Write log file header const header = `# SerpentRace Backend Logs\n# Started: ${now.toISOString()}\n# Max entries per file: ${this.maxLogsPerFile}\n\n`; try { fs_1.default.writeFileSync(this.currentLogFile, header); } catch (error) { console.error('Failed to create log file:', error); } } formatLogEntry(entry) { const parts = [ entry.timestamp, `[${entry.level}]`, entry.message ]; if (entry.requestId) parts.push(`ReqId:${entry.requestId}`); if (entry.userId) parts.push(`UserId:${entry.userId}`); if (entry.ip) parts.push(`IP:${entry.ip}`); if (entry.method && entry.url) parts.push(`${entry.method} ${entry.url}`); if (entry.statusCode) parts.push(`Status:${entry.statusCode}`); if (entry.responseTime) parts.push(`Time:${entry.responseTime}ms`); if (entry.userAgent) parts.push(`UA:${entry.userAgent.substring(0, 50)}`); if (entry.metadata) parts.push(`Meta:${JSON.stringify(entry.metadata)}`); return parts.join(' | '); } async writeToLocalFile(entry) { if (!this.currentLogFile) return; try { const logLine = this.formatLogEntry(entry) + '\n'; fs_1.default.appendFileSync(this.currentLogFile, logLine); this.logCount++; // Check if we need to rotate the log file if (this.logCount >= this.maxLogsPerFile) { await this.rotateLogFile(); } } catch (error) { console.error('Failed to write to log file:', error); } } async rotateLogFile() { if (!this.currentLogFile) return; try { // Upload current file to Minio before rotating await this.uploadToMinio(this.currentLogFile); // Create new log file this.createNewLogFile(); this.log(LogLevel.OTHER, 'Log file rotated due to size limit'); } catch (error) { console.error('Failed to rotate log file:', error); } } async uploadToMinio(filePath) { if (!this.minioClient) { console.warn('Minio client not initialized, skipping upload'); return; } if (!fs_1.default.existsSync(filePath)) { console.warn(`Log file does not exist: ${filePath}`); return; } try { const fileName = path_1.default.basename(filePath); const objectName = this.getMonthlyMinioPrefix() + fileName; console.log(`Attempting to upload log file to Minio: ${objectName}`); await this.minioClient.fPutObject(this.bucketName, objectName, filePath); console.log(`Successfully uploaded log file to Minio: ${objectName}`); } catch (error) { console.error('Failed to upload to Minio:', error); console.error('Minio config:', { endpoint: this.minioClient ? 'configured' : 'not configured', bucket: this.bucketName }); } } logToConsole(entry) { const formattedEntry = this.formatLogEntry(entry); switch (entry.level) { case LogLevel.ERROR: console.error(formattedEntry); break; case LogLevel.WARNING: console.warn(formattedEntry); break; case LogLevel.REQUEST: case LogLevel.AUTH: case LogLevel.DATABASE: case LogLevel.CONNECTION: console.info(formattedEntry); break; case LogLevel.STARTUP: console.log(formattedEntry); break; default: console.log(formattedEntry); } } log(level, message, metadata, req, res, responseTime) { const entry = { timestamp: new Date().toISOString(), level, message, metadata }; // Add request context if available if (req) { entry.requestId = req.requestId || this.generateRequestId(); entry.userId = req.user?.userId; entry.ip = req.ip || req.socket?.remoteAddress || 'unknown'; entry.userAgent = req.get ? req.get('User-Agent') : 'unknown'; entry.method = req.method; entry.url = req.originalUrl || req.url; } if (res) { entry.statusCode = res.statusCode; } if (responseTime !== undefined) { entry.responseTime = responseTime; } // Log to all three destinations this.logToConsole(entry); this.writeToLocalFile(entry); // Add to buffer for potential batch processing this.logBuffer.push(entry); // Limit buffer size if (this.logBuffer.length > 1000) { this.logBuffer = this.logBuffer.slice(-500); } } generateRequestId() { return Math.random().toString(36).substr(2, 9); } async shutdown() { try { // Clear the upload interval if (this.uploadInterval) { clearInterval(this.uploadInterval); this.uploadInterval = null; } // Upload current log file to Minio if (this.currentLogFile) { await this.uploadToMinio(this.currentLogFile); } this.log(LogLevel.STARTUP, 'Logging service shutting down gracefully'); // Give time for final logs to be written await new Promise(resolve => setTimeout(resolve, 1000)); } catch (error) { console.error('Error during logging service shutdown:', error); } } // Middleware factory methods requestLoggingMiddleware() { return (req, res, next) => { const startTime = Date.now(); // Generate request ID req.requestId = this.generateRequestId(); // Log request start this.log(LogLevel.REQUEST, `Incoming request`, undefined, req); // Override res.end to log response const originalEnd = res.end.bind(res); res.end = (...args) => { const responseTime = Date.now() - startTime; LoggingService.getInstance().log(LogLevel.REQUEST, `Request completed`, undefined, req, res, responseTime); return originalEnd(...args); }; next(); }; } errorLoggingMiddleware() { return (error, req, res, next) => { this.log(LogLevel.ERROR, `Unhandled error: ${error.message}`, { stack: error.stack, name: error.name }, req, res); next(error); }; } } exports.LoggingService = LoggingService; exports.default = LoggingService; //# sourceMappingURL=LoggingService.js.map