UNPKG

nestjs-universal-logger-v2

Version:

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

365 lines (364 loc) 13.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); }; var __param = (this && this.__param) || function (paramIndex, decorator) { return function (target, key) { decorator(target, key, paramIndex); } }; Object.defineProperty(exports, "__esModule", { value: true }); exports.UniversalLoggerService = void 0; const common_1 = require("@nestjs/common"); const mongoose_1 = require("@nestjs/mongoose"); const mongoose_2 = require("mongoose"); const winston_1 = require("winston"); const uuid_1 = require("uuid"); const universal_logger_config_1 = require("./universal-logger.config"); let UniversalLoggerService = class UniversalLoggerService { constructor(logModel, config) { this.logModel = logModel; this.config = config; const loggingConfig = config.getLoggingConfig(); this.serviceName = loggingConfig.serviceName; this.environment = loggingConfig.environment; this.version = loggingConfig.version; this.winstonLogger = (0, winston_1.createLogger)({ level: loggingConfig.level, 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(loggingConfig) }); } createTransports(loggingConfig) { const transports = []; if (loggingConfig.enableConsole) { transports.push(new transports.Console({ format: winston_1.format.combine(winston_1.format.colorize(), winston_1.format.simple()) })); } if (loggingConfig.enableFile) { const logDir = loggingConfig.logDirectory; transports.push(new transports.File({ filename: `${logDir}/error.log`, level: 'error', maxsize: loggingConfig.maxFileSize, maxFiles: loggingConfig.maxFiles }), new transports.File({ filename: `${logDir}/combined.log`, maxsize: loggingConfig.maxFileSize, maxFiles: loggingConfig.maxFiles })); } return transports; } async createLogEntry(level, message, metadata, context) { const logEntry = { id: (0, uuid_1.v4)(), timestamp: new Date(), level, service: this.serviceName, module: context, 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'; } } log(message, context, metadata) { this.winstonLogger.info(message, { context, ...metadata }); this.createLogEntry('info', message, metadata, context); } error(message, trace, context, metadata) { this.winstonLogger.error(message, { trace, context, ...metadata }); this.createLogEntry('error', message, { trace, ...metadata }, context); } warn(message, context, metadata) { this.winstonLogger.warn(message, { context, ...metadata }); this.createLogEntry('warn', message, metadata, context); } debug(message, context, metadata) { this.winstonLogger.debug(message, { context, ...metadata }); this.createLogEntry('debug', message, metadata, context); } verbose(message, context, metadata) { this.winstonLogger.verbose(message, { context, ...metadata }); this.createLogEntry('verbose', message, metadata, context); } logApiCall(req, res, duration, statusCode) { if (!this.config.isFeatureEnabled('api')) return; const apiConfig = this.config.getApiConfig(); 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)) { apiCall.body = req.body; } if (apiConfig.logHeaders) { apiCall.headers = this.sanitizeHeaders(req.headers); } if (statusCode >= 400 || apiConfig.logResponses) { apiCall.response.body = this.getResponseBody(res); } 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) { this.warn(`Slow API request: ${req.method} ${req.originalUrl} took ${duration}ms`, 'PERFORMANCE', { requestId, duration, threshold: apiConfig.slowRequestThreshold }); } } logPerformance(operation, duration, metadata) { if (!this.config.isFeatureEnabled('performance')) return; const level = duration > 1000 ? 'warn' : 'info'; this[level](`Performance: ${operation} took ${duration}ms`, 'PERFORMANCE', { operation, duration, ...metadata }); } logDatabaseQuery(query, duration, table, operation) { if (!this.config.isFeatureEnabled('performance')) return; const performanceData = { databaseQueries: [{ query, duration, table, operation }] }; this.log(`Database query executed in ${duration}ms`, 'DATABASE', { query, duration, table, operation, performance: performanceData }); } logExternalCall(url, method, duration, statusCode, response) { if (!this.config.isFeatureEnabled('performance')) return; const performanceData = { externalCalls: [{ url, method, duration, statusCode }] }; this.log(`External call: ${method} ${url}`, 'EXTERNAL', { url, method, duration, statusCode, response, performance: performanceData }); } logSecurity(event, metadata) { if (!this.config.isFeatureEnabled('security')) return; this.warn(`Security event: ${event}`, 'SECURITY', { event, ...metadata }); } logAuthEvent(event, userId, success = true, metadata) { if (!this.config.isFeatureEnabled('security')) return; const level = success ? 'info' : 'warn'; this[level](`Auth event: ${event}`, 'AUTH', { event, userId, success, ...metadata }); } logBusinessLogic(operation, data, context) { if (!this.config.isFeatureEnabled('business')) return; this.info(`Business logic: ${operation}`, context, { operation, data, tags: ['business-logic'] }); } logUserAction(action, userId, metadata) { if (!this.config.isFeatureEnabled('business')) return; this.info(`User action: ${action}`, 'USER_ACTION', { action, userId, ...metadata }); } logFeatureUsage(feature, userId, metadata) { if (!this.config.isFeatureEnabled('business')) return; this.info(`Feature usage: ${feature}`, 'FEATURE_USAGE', { feature, userId, ...metadata }); } logSystemMetrics() { if (!this.config.isFeatureEnabled('performance')) 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 } }; 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 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; } sanitizeHeaders(headers) { const sanitized = { ...headers }; const sensitiveHeaders = this.config.getApiConfig().sensitiveHeaders; 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.UniversalLoggerService = UniversalLoggerService; exports.UniversalLoggerService = UniversalLoggerService = __decorate([ (0, common_1.Injectable)(), __param(0, (0, mongoose_1.InjectModel)('LogEntry')), __metadata("design:paramtypes", [mongoose_2.Model, universal_logger_config_1.UniversalLoggerConfig]) ], UniversalLoggerService);