UNPKG

@erickzao/api-stats-cli

Version:

CLI oficial para configuração automática do API Stats Logger

293 lines (250 loc) 8.37 kB
const performanceNow = require('performance-now'); class MiddlewareManager { static express(options = {}) { const { logger, captureBody = false, captureHeaders = false, captureQuery = true, captureParams = true, skipPaths = ['/health', '/metrics'], skipMethods = ['OPTIONS'], maxBodySize = 1024 * 10 // 10KB } = options; if (!logger) { throw new Error('Logger instance é obrigatório para o middleware'); } return (req, res, next) => { const startTime = performanceNow(); const originalUrl = req.originalUrl || req.url; // Skip paths/methods específicos if (skipPaths.includes(req.path) || skipMethods.includes(req.method)) { return next(); } // Gerar ID único para a requisição const requestId = `req_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; req.requestId = requestId; // Capturar dados da requisição const requestData = { id: requestId, method: req.method, url: originalUrl, ip: req.ip || req.connection.remoteAddress, userAgent: req.get('User-Agent'), timestamp: new Date().toISOString() }; if (captureQuery && Object.keys(req.query).length > 0) { requestData.query = req.query; } if (captureParams && Object.keys(req.params).length > 0) { requestData.params = req.params; } if (captureHeaders) { requestData.headers = this._sanitizeHeaders(req.headers); } if (captureBody && req.body) { const bodyStr = JSON.stringify(req.body); if (bodyStr.length <= maxBodySize) { requestData.body = req.body; } else { requestData.body = '[BODY_TOO_LARGE]'; } } // Log da requisição iniciada logger.info('HTTP Request Started', { type: 'http_request', request: requestData }); // Interceptar resposta const originalSend = res.send; const originalJson = res.json; let responseBody = null; res.send = function(body) { if (captureBody && body && body.length <= maxBodySize) { responseBody = body; } return originalSend.call(this, body); }; res.json = function(obj) { if (captureBody) { const objStr = JSON.stringify(obj); if (objStr.length <= maxBodySize) { responseBody = obj; } } return originalJson.call(this, obj); }; // Quando a resposta terminar res.on('finish', () => { const duration = performanceNow() - startTime; const responseData = { statusCode: res.statusCode, duration: Math.round(duration * 100) / 100, // Arredondar para 2 casas decimais size: res.get('Content-Length') || 0 }; if (captureBody && responseBody) { responseData.body = responseBody; } if (captureHeaders && res.getHeaders) { responseData.headers = res.getHeaders(); } // Determinar nível do log baseado no status const level = res.statusCode >= 500 ? 'error' : res.statusCode >= 400 ? 'warn' : 'info'; logger.log({ level, message: `HTTP ${req.method} ${originalUrl} ${res.statusCode}`, metadata: { type: 'http_response', requestId, request: { method: req.method, url: originalUrl, ...requestData }, response: responseData, duration } }); }); // Capturar erros res.on('error', (error) => { const duration = performanceNow() - startTime; logger.error('HTTP Request Error', { type: 'http_error', requestId, error: error.message, stack: error.stack, request: requestData, duration }); }); next(); }; } static nest(options = {}) { const { logger, captureBody = false, captureHeaders = false, skipRoutes = ['/health', '/metrics'], maxBodySize = 1024 * 10 } = options; if (!logger) { throw new Error('Logger instance é obrigatório para o middleware NestJS'); } return (req, res, next) => { const startTime = performanceNow(); const requestId = `nest_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; // Skip rotas específicas if (skipRoutes.some(route => req.path.includes(route))) { return next(); } req.requestId = requestId; const requestData = { id: requestId, method: req.method, url: req.url, ip: req.ip, userAgent: req.get('User-Agent'), controller: req.route?.path || 'unknown', timestamp: new Date().toISOString() }; if (captureBody && req.body) { const bodyStr = JSON.stringify(req.body); if (bodyStr.length <= maxBodySize) { requestData.body = req.body; } } if (captureHeaders) { requestData.headers = this._sanitizeHeaders(req.headers); } logger.info('NestJS Request Started', { type: 'nestjs_request', request: requestData }); res.on('finish', () => { const duration = performanceNow() - startTime; const level = res.statusCode >= 500 ? 'error' : res.statusCode >= 400 ? 'warn' : 'info'; logger.log({ level, message: `NestJS ${req.method} ${req.url} ${res.statusCode}`, metadata: { type: 'nestjs_response', requestId, request: requestData, response: { statusCode: res.statusCode, duration: Math.round(duration * 100) / 100 }, duration } }); }); next(); }; } static _sanitizeHeaders(headers) { const sensitiveHeaders = ['authorization', 'cookie', 'x-api-key', 'x-auth-token']; const sanitized = { ...headers }; sensitiveHeaders.forEach(header => { if (sanitized[header]) { sanitized[header] = '[REDACTED]'; } }); return sanitized; } // Middleware para capturar métricas de banco de dados (para ORMs populares) static database(options = {}) { const { logger, captureQueries = false, maxQueryLength = 500 } = options; return { // Para Mongoose mongoose: () => { if (typeof require === 'function') { try { const mongoose = require('mongoose'); mongoose.set('debug', (collectionName, method, query, doc) => { const queryStr = JSON.stringify(query); const truncatedQuery = queryStr.length > maxQueryLength ? queryStr.substring(0, maxQueryLength) + '...' : queryStr; logger.info('Database Query', { type: 'database_query', database: 'mongodb', collection: collectionName, method, query: captureQueries ? truncatedQuery : '[QUERY_HIDDEN]', timestamp: new Date().toISOString() }); }); } catch (e) { // Mongoose não está instalado } } }, // Para Sequelize sequelize: (sequelizeInstance) => { if (sequelizeInstance && sequelizeInstance.addHook) { sequelizeInstance.addHook('beforeBulkCreate', (instances, options) => { logger.info('Database Bulk Create', { type: 'database_bulk_operation', operation: 'bulkCreate', model: options.model?.name, count: instances.length }); }); sequelizeInstance.addHook('beforeFind', (options) => { logger.info('Database Find', { type: 'database_query', operation: 'find', model: options.model?.name, where: captureQueries ? JSON.stringify(options.where) : '[WHERE_HIDDEN]' }); }); } } }; } } module.exports = MiddlewareManager;