UNPKG

logceptor

Version:

NestJS interceptor to log HTTP requests and responses with full control, correlation IDs, file rotation, sensitive data masking, and production-ready features.

283 lines (282 loc) 13 kB
"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 __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) { function accept(f) { if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected"); return f; } var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value"; var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null; var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {}); var _, done = false; for (var i = decorators.length - 1; i >= 0; i--) { var context = {}; for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p]; for (var p in contextIn.access) context.access[p] = contextIn.access[p]; context.addInitializer = function (f) { if (done) throw new TypeError("Cannot add initializers after decoration has completed"); extraInitializers.push(accept(f || null)); }; var result = (0, decorators[i])(kind === "accessor" ? { get: descriptor.get, set: descriptor.set } : descriptor[key], context); if (kind === "accessor") { if (result === void 0) continue; if (result === null || typeof result !== "object") throw new TypeError("Object expected"); if (_ = accept(result.get)) descriptor.get = _; if (_ = accept(result.set)) descriptor.set = _; if (_ = accept(result.init)) initializers.unshift(_); } else if (_ = accept(result)) { if (kind === "field") initializers.unshift(_); else descriptor[key] = _; } } if (target) Object.defineProperty(target, contextIn.name, descriptor); done = true; }; var __runInitializers = (this && this.__runInitializers) || function (thisArg, initializers, value) { var useValue = arguments.length > 2; for (var i = 0; i < initializers.length; i++) { value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg); } return useValue ? value : void 0; }; 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 __setFunctionName = (this && this.__setFunctionName) || function (f, name, prefix) { if (typeof name === "symbol") name = name.description ? "[".concat(name.description, "]") : ""; return Object.defineProperty(f, "name", { configurable: true, value: prefix ? "".concat(prefix, " ", name) : name }); }; Object.defineProperty(exports, "__esModule", { value: true }); exports.LoggingInterceptor = void 0; const common_1 = require("@nestjs/common"); const rxjs_1 = require("rxjs"); const fs = __importStar(require("fs/promises")); const path = __importStar(require("path")); const uuid_1 = require("uuid"); let LoggingInterceptor = (() => { let _classDecorators = [(0, common_1.Injectable)()]; let _classDescriptor; let _classExtraInitializers = []; let _classThis; var LoggingInterceptor = _classThis = class { constructor(options = {}) { var _a, _b, _c, _d, _e, _f; this.logger = new common_1.Logger('logceptor'); this.level = (_a = options.level) !== null && _a !== void 0 ? _a : 'log'; this.format = (_b = options.format) !== null && _b !== void 0 ? _b : 'text'; const folder = (_c = options.folder) !== null && _c !== void 0 ? _c : process.cwd(); const filename = (_d = options.filename) !== null && _d !== void 0 ? _d : 'http_logs.txt'; this.logFilePath = path.join(folder, filename); this.maskFields = (_e = options.maskFields) !== null && _e !== void 0 ? _e : ['password', 'token']; const sizeMB = (_f = options.maxFileSizeMB) !== null && _f !== void 0 ? _f : 10; this.maxFileSizeBytes = sizeMB * 1024 * 1024; console.log(`🚀 LoggingInterceptor writing to: ${this.logFilePath}`); } intercept(context, next) { const req = context.switchToHttp().getRequest(); const res = context.switchToHttp().getResponse(); const { method, originalUrl, body, query, params, headers, ip, protocol, httpVersion, } = req; const correlationId = headers['x-correlation-id'] || (0, uuid_1.v4)(); req.correlationId = correlationId; res.setHeader('x-correlation-id', correlationId); const maskedBody = this.maskSensitive(body); const timestamp = new Date().toISOString(); const now = Date.now(); const requestLog = this.formatLog('request', { timestamp, correlationId, method, url: originalUrl, body: maskedBody, query, params, ip: headers['x-forwarded-for'] || ip, hostname: req.hostname, userAgent: headers['user-agent'], referer: headers['referer'] || '', protocol, httpVersion, }); this.log(`${method} ${originalUrl} [${correlationId}]`, this.level); this.appendToFile(requestLog); return next.handle().pipe((0, rxjs_1.tap)((data) => { const duration = Date.now() - now; const maskedRes = this.maskSensitive(this.extractPayload(data)); const responseLog = this.formatLog('response', { timestamp, correlationId, url: originalUrl, duration: `${duration}ms`, response: maskedRes, }); this.log(`${method} ${originalUrl} completed in ${duration}ms [${correlationId}]`, this.level); this.appendToFile(responseLog); })); } extractPayload(data) { return (typeof data === 'object' && data !== null) ? data : { data }; } formatLog(type, data) { if (this.format === 'json') { return this.safeStringify(Object.assign({ type }, data)) + '\n'; } if (type === 'request') { return ` *************** REQUEST *************** Timestamp : ${data.timestamp} Correlation ID: ${data.correlationId} Method : ${data.method} URL : ${data.url} Params : ${this.safeStringify(data.params)} Query : ${this.safeStringify(data.query)} Body : ${this.safeStringify(data.body)} IP : ${data.ip} Hostname : ${data.hostname} User-Agent : ${data.userAgent} Referer : ${data.referer} Protocol : ${data.protocol} HTTP Version : ${data.httpVersion} ****************************************\n`; } return ` *************** RESPONSE ************** Timestamp : ${data.timestamp} Correlation ID: ${data.correlationId} URL : ${data.url} Time Taken : ${data.duration} Response Body : ${this.safeStringify(data.response)} ****************************************\n`; } safeStringify(obj) { const seen = new WeakSet(); return JSON.stringify(obj, (key, value) => { if (typeof value === 'object' && value !== null) { if (seen.has(value)) return '[Circular]'; seen.add(value); } return value; }); } maskSensitive(obj) { if (!obj || typeof obj !== 'object') return obj; const seen = new WeakSet(); const deepMask = (value) => { if (value === null || typeof value !== 'object') return value; if (seen.has(value)) return '[Circular]'; seen.add(value); const masked = Array.isArray(value) ? [] : {}; for (const key in value) { if (this.maskFields.includes(key)) { masked[key] = '****'; } else if (typeof value[key] === 'object') { masked[key] = deepMask(value[key]); } else { masked[key] = value[key]; } } return masked; }; return deepMask(obj); } async appendToFile(log) { try { const dir = path.dirname(this.logFilePath); await fs.mkdir(dir, { recursive: true }); let rotate = false; try { const stat = await fs.stat(this.logFilePath); if (stat.size + Buffer.byteLength(log) > this.maxFileSizeBytes) { rotate = true; } } catch (_a) { // File does not exist yet } if (rotate) { const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); const rotatedName = `${this.logFilePath}.old-${timestamp}.log`; await fs.rename(this.logFilePath, rotatedName); this.logger.log(`Rotated log file: ${rotatedName}`); } await fs.appendFile(this.logFilePath, log); await this.cleanupOldLogs(dir); } catch (err) { this.logger.error('Failed to write log to file', err); } } async cleanupOldLogs(dir) { const files = await fs.readdir(dir); const now = Date.now(); const THIRTY_DAYS = 30 * 24 * 60 * 60 * 1000; for (const file of files) { const filePath = path.join(dir, file); const stat = await fs.stat(filePath); if (stat.isFile()) { const age = now - stat.mtimeMs; const ext = path.extname(file); if ((age > THIRTY_DAYS || stat.size > this.maxFileSizeBytes) && ['.log', '.txt'].includes(ext)) { await fs.unlink(filePath); this.logger.log(`Deleted old log: ${file}`); } } } } log(message, level) { switch (level) { case 'log': this.logger.log(message); break; case 'warn': this.logger.warn(message); break; case 'error': this.logger.error(message); break; case 'debug': this.logger.debug(message); break; } } }; __setFunctionName(_classThis, "LoggingInterceptor"); (() => { const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(null) : void 0; __esDecorate(null, _classDescriptor = { value: _classThis }, _classDecorators, { kind: "class", name: _classThis.name, metadata: _metadata }, null, _classExtraInitializers); LoggingInterceptor = _classThis = _classDescriptor.value; if (_metadata) Object.defineProperty(_classThis, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata }); __runInitializers(_classThis, _classExtraInitializers); })(); return LoggingInterceptor = _classThis; })(); exports.LoggingInterceptor = LoggingInterceptor;