UNPKG

nestjs-universal-logger-v2

Version:

Universal logging package for NestJS - Automatic system-wide logging with per-service MongoDB collections and TTL-based cleanup

456 lines (455 loc) 16.4 kB
"use strict"; var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; var __metadata = (this && this.__metadata) || function (k, v) { if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v); }; Object.defineProperty(exports, "__esModule", { value: true }); exports.UniversalLoggerStandalone = void 0; const common_1 = require("@nestjs/common"); const mongoose_1 = require("mongoose"); const winston_1 = require("winston"); const uuid_1 = require("uuid"); let UniversalLoggerStandalone = class UniversalLoggerStandalone { constructor(logModel, config, serviceName, environment, version) { this.logModel = logModel; this.config = config; this.serviceName = serviceName || config.logging?.serviceName || 'default-service'; this.environment = environment || config.logging?.environment || 'development'; this.version = version || config.logging?.version || '1.0.0'; this.winstonLogger = (0, winston_1.createLogger)({ level: config.logging?.level || 'info', format: winston_1.format.combine(winston_1.format.timestamp(), winston_1.format.errors({ stack: true }), winston_1.format.json()), defaultMeta: { service: this.serviceName, environment: this.environment, version: this.version }, transports: this.createTransports(config.logging || {}) }); } createTransports(loggingConfig) { const transportArray = []; if (loggingConfig.enableConsole !== false) { transportArray.push(new winston_1.transports.Console({ format: winston_1.format.combine(winston_1.format.colorize(), winston_1.format.simple()) })); } if (loggingConfig.enableFile) { const logDir = loggingConfig.logDirectory || './logs'; transportArray.push(new winston_1.transports.File({ filename: `${logDir}/error.log`, level: 'error', maxsize: loggingConfig.maxFileSize || 5242880, maxFiles: loggingConfig.maxFiles || 5 }), new winston_1.transports.File({ filename: `${logDir}/combined.log`, maxsize: loggingConfig.maxFileSize || 5242880, maxFiles: loggingConfig.maxFiles || 5 })); } return transportArray; } async log(message, context, metadata) { this.winstonLogger.info(message, { context, ...metadata }); await this.createLogEntry('info', message, metadata, context); } async error(message, trace, context, metadata) { this.winstonLogger.error(message, { trace, context, ...metadata }); await this.createLogEntry('error', message, { trace, ...metadata }, context); } async warn(message, context, metadata) { this.winstonLogger.warn(message, { context, ...metadata }); await this.createLogEntry('warn', message, metadata, context); } async debug(message, context, metadata) { this.winstonLogger.debug(message, { context, ...metadata }); await this.createLogEntry('debug', message, metadata, context); } async verbose(message, context, metadata) { this.winstonLogger.verbose(message, { context, ...metadata }); await this.createLogEntry('verbose', message, metadata, context); } async logApiCall(req, res, duration, statusCode) { if (!this.config.api?.enabled) return; const apiConfig = this.config.api; const requestId = req.headers?.['x-request-id'] || (0, uuid_1.v4)(); const apiCall = { method: req.method, path: req.originalUrl, response: { statusCode, headers: apiConfig.logHeaders ? this.sanitizeHeaders(res.getHeaders?.() || {}) : undefined } }; if (apiConfig.logQuery && req.query) { apiCall.query = req.query; } if (apiConfig.logBody && req.body && this.isBodyLoggable(req.body, apiConfig.maxBodySize || 1024)) { apiCall.body = req.body; } if (apiConfig.logHeaders) { apiCall.headers = this.sanitizeHeaders(req.headers || {}); } if (statusCode >= 400 || apiConfig.logResponses) { apiCall.response.body = this.getResponseBody(res); } await this.log(`API Call: ${req.method} ${req.originalUrl}`, 'API', { requestId, duration, apiCall, userId: req.user?.id, sessionId: req.session?.id, ip: req.ip, userAgent: req.headers?.['user-agent'] }); if (duration > (apiConfig.slowRequestThreshold || 1000)) { await this.warn(`Slow API request: ${req.method} ${req.originalUrl} took ${duration}ms`, 'PERFORMANCE', { requestId, duration, threshold: apiConfig.slowRequestThreshold || 1000 }); } } async logPerformance(operation, duration, metadata) { if (!this.config.performance?.enabled) return; const level = duration > 1000 ? 'warn' : 'info'; if (level === 'warn') { await this.warn(`Performance: ${operation} took ${duration}ms`, 'PERFORMANCE', { operation, duration, ...metadata }); } else { await this.log(`Performance: ${operation} took ${duration}ms`, 'PERFORMANCE', { operation, duration, ...metadata }); } } async logDatabaseQuery(query, duration, table, operation) { if (!this.config.performance?.enabled) return; const performanceData = { databaseQueries: [{ query, duration, table, operation }] }; await this.log(`Database query executed in ${duration}ms`, 'DATABASE', { query, duration, table, operation, performance: performanceData }); } async logExternalCall(url, method, duration, statusCode, response) { if (!this.config.performance?.enabled) return; const performanceData = { externalCalls: [{ url, method, duration, statusCode }] }; await this.log(`External call: ${method} ${url}`, 'EXTERNAL', { url, method, duration, statusCode, response, performance: performanceData }); } async logSecurity(event, metadata) { if (!this.config.security?.enabled) return; await this.warn(`Security event: ${event}`, 'SECURITY', { event, ...metadata }); } async logAuthEvent(event, userId, success = true, metadata) { if (!this.config.security?.enabled) return; const level = success ? 'info' : 'warn'; if (level === 'warn') { await this.warn(`Auth event: ${event}`, 'AUTH', { event, userId, success, ...metadata }); } else { await this.log(`Auth event: ${event}`, 'AUTH', { event, userId, success, ...metadata }); } } async logBusinessLogic(operation, data, context) { if (!this.config.business?.enabled) return; await this.log(`Business logic: ${operation}`, context, { operation, data, tags: ['business-logic'] }); } async logUserAction(action, userId, metadata) { if (!this.config.business?.enabled) return; await this.log(`User action: ${action}`, 'USER_ACTION', { action, userId, ...metadata }); } async logFeatureUsage(feature, userId, metadata) { if (!this.config.business?.enabled) return; await this.log(`Feature usage: ${feature}`, 'FEATURE_USAGE', { feature, userId, ...metadata }); } async logSystemMetrics() { if (!this.config.performance?.enabled) return; const memUsage = process.memoryUsage(); const cpuUsage = process.cpuUsage(); const performanceData = { memoryUsage: { heapUsed: Math.round(memUsage.heapUsed / 1024 / 1024), heapTotal: Math.round(memUsage.heapTotal / 1024 / 1024), external: Math.round(memUsage.external / 1024 / 1024), rss: Math.round(memUsage.rss / 1024 / 1024) }, cpuUsage: { user: cpuUsage.user, system: cpuUsage.system } }; await this.debug('System metrics', 'SYSTEM', { performance: performanceData }); } async getLogs(query) { const filter = {}; if (query.service) filter.service = query.service; if (query.level) filter.level = query.level; if (query.userId) filter.userId = query.userId; if (query.requestId) filter.requestId = query.requestId; if (query.severity) filter.severity = query.severity; if (query.tags) filter.tags = { $in: query.tags }; if (query.path) filter['apiCall.path'] = query.path; if (query.method) filter['apiCall.method'] = query.method; if (query.statusCode) filter.statusCode = query.statusCode; if (query.startDate || query.endDate) { filter.timestamp = {}; if (query.startDate) filter.timestamp.$gte = query.startDate; if (query.endDate) filter.timestamp.$lte = query.endDate; } return this.logModel .find(filter) .sort({ timestamp: -1 }) .limit(query.limit || 100) .skip(query.skip || 0) .exec(); } async getLogStats(timeRange) { return this.logModel.aggregate([ { $match: { timestamp: { $gte: timeRange.start, $lte: timeRange.end } } }, { $group: { _id: { level: '$level', service: '$service', severity: '$severity' }, count: { $sum: 1 }, avgDuration: { $avg: '$duration' }, maxDuration: { $max: '$duration' }, minDuration: { $min: '$duration' } } }, { $sort: { count: -1 } } ]); } async getErrorTrends(days = 7) { const startDate = new Date(); startDate.setDate(startDate.getDate() - days); return this.logModel.aggregate([ { $match: { level: 'error', timestamp: { $gte: startDate } } }, { $group: { _id: { date: { $dateToString: { format: '%Y-%m-%d', date: '$timestamp' } }, service: '$service' }, errorCount: { $sum: 1 }, uniqueErrors: { $addToSet: '$message' } } }, { $sort: { '_id.date': 1 } } ]); } async getTopErrors(limit = 10, days = 7) { const startDate = new Date(); startDate.setDate(startDate.getDate() - days); return this.logModel.aggregate([ { $match: { level: 'error', timestamp: { $gte: startDate } } }, { $group: { _id: '$message', count: { $sum: 1 }, services: { $addToSet: '$service' }, lastOccurrence: { $max: '$timestamp' }, avgDuration: { $avg: '$duration' } } }, { $sort: { count: -1 } }, { $limit: limit } ]); } async getPerformanceMetrics(hours = 24) { const startDate = new Date(); startDate.setHours(startDate.getHours() - hours); return this.logModel.aggregate([ { $match: { duration: { $exists: true, $ne: null }, timestamp: { $gte: startDate } } }, { $group: { _id: { service: '$service', path: '$apiCall.path', method: '$apiCall.method' }, avgDuration: { $avg: '$duration' }, maxDuration: { $max: '$duration' }, minDuration: { $min: '$duration' }, count: { $sum: 1 } } }, { $sort: { avgDuration: -1 } } ]); } async cleanupOldLogs(daysToKeep = 30) { const cutoffDate = new Date(); cutoffDate.setDate(cutoffDate.getDate() - daysToKeep); const result = await this.logModel.deleteMany({ timestamp: { $lt: cutoffDate } }); return result.deletedCount || 0; } async createLogEntry(level, message, metadata, context) { const logEntry = { id: (0, uuid_1.v4)(), timestamp: new Date(), level, service: this.serviceName, module: context || 'unknown', message, metadata, environment: this.environment, version: this.version, tags: metadata?.['tags'] || [], severity: this.getSeverityLevel(level) }; try { await this.logModel.create(logEntry); } catch (error) { console.error('Failed to save log to MongoDB:', error); } return logEntry; } getSeverityLevel(level) { switch (level) { case 'error': return 'critical'; case 'warn': return 'high'; case 'info': return 'medium'; case 'debug': case 'verbose': return 'low'; default: return 'medium'; } } sanitizeHeaders(headers) { const sanitized = { ...headers }; const sensitiveHeaders = this.config.api?.sensitiveHeaders || ['authorization', 'cookie', 'x-api-key']; sensitiveHeaders.forEach((header) => { if (sanitized[header]) { sanitized[header] = '[REDACTED]'; } }); return sanitized; } isBodyLoggable(body, maxSize) { const bodyStr = JSON.stringify(body); return bodyStr.length <= maxSize; } getResponseBody(res) { return undefined; } }; exports.UniversalLoggerStandalone = UniversalLoggerStandalone; exports.UniversalLoggerStandalone = UniversalLoggerStandalone = __decorate([ (0, common_1.Injectable)(), __metadata("design:paramtypes", [mongoose_1.Model, Object, String, String, String]) ], UniversalLoggerStandalone);