UNPKG

api-stats-logger

Version:

SDK completo de logging e monitoramento de APIs com nova estrutura de logs organizada, auto-instrumentação, dashboard em tempo real e CLI para configuração automática. Suporta logs estruturados por contexto (HTTP, business, security, system) com campos op

643 lines (551 loc) 19 kB
const performanceNow = require('performance-now'); const os = require('os'); 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 IDs únicos para rastreamento const requestId = `req_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; const traceId = req.headers['x-trace-id'] || `trace_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; req.requestId = requestId; req.traceId = traceId; // Capturar dados da requisição no novo formato const requestData = { method: req.method, url: originalUrl, path: req.path, ip: req.ip || req.connection.remoteAddress, timestamp: new Date().toISOString() }; // Adicionar dados opcionais apenas se existirem if (captureQuery && req.query && Object.keys(req.query).length > 0) { requestData.query = req.query; } if (captureParams && req.params && Object.keys(req.params).length > 0) { requestData.params = req.params; } if (captureHeaders && req.headers) { 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]'; } } // Capturar User-Agent separadamente if (req.get('User-Agent')) { requestData.userAgent = req.get('User-Agent'); } // 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 endTime = performanceNow(); const duration = endTime - startTime; // Preparar dados da resposta const responseData = { statusCode: res.statusCode, statusText: this._getStatusText(res.statusCode), duration: Math.round(duration * 100) / 100, size: parseInt(res.get('Content-Length') || '0', 10), timestamp: new Date().toISOString() }; if (captureBody && responseBody) { responseData.body = responseBody; } if (captureHeaders && res.getHeaders) { responseData.headers = res.getHeaders(); } // Determinar categoria de performance const performanceCategory = duration < 100 ? 'fast' : duration < 500 ? 'medium' : duration < 2000 ? 'slow' : 'critical'; // Determinar nível do log baseado no status const level = res.statusCode >= 500 ? 'error' : res.statusCode >= 400 ? 'warn' : 'info'; // Capturar informações do sistema const systemInfo = this._getSystemInfo(); // Criar log estruturado no novo formato const structuredLog = { level, message: `HTTP ${req.method} ${originalUrl} ${res.statusCode}`, category: 'http_request', context: { requestId, traceId, userId: req.user?.id || req.userId, sessionId: req.sessionID }, http: { request: requestData, response: responseData, performance: { duration: Math.round(duration * 100) / 100, processingTime: Math.round(duration * 100) / 100, // Para middleware simples, é igual à duration category: performanceCategory } }, system: systemInfo, sdk: { name: 'api-stats-logger', version: '1.0.0', framework: 'express' } }; // Adicionar contexto de segurança se disponível if (req.user) { structuredLog.security = { authenticated: true, userId: req.user.id, roles: req.user.roles, permissions: req.user.permissions }; } // Adicionar contexto de negócio se disponível if (req.businessContext) { structuredLog.business = { operation: req.businessContext.operation, feature: req.businessContext.feature, version: req.businessContext.version, tags: req.businessContext.tags, customData: req.businessContext.customData }; } // Adicionar informações de erro se status >= 400 if (res.statusCode >= 400) { structuredLog.error = { type: res.statusCode >= 500 ? 'server_error' : 'client_error', message: responseData.statusText, code: res.statusCode.toString(), resolved: false }; } // Adicionar métricas structuredLog.metrics = { bytes: { request: req.get('content-length') ? parseInt(req.get('content-length'), 10) : 0, response: responseData.size, total: (req.get('content-length') ? parseInt(req.get('content-length'), 10) : 0) + responseData.size }, timing: { total: Math.round(duration * 100) / 100 } }; logger.log(structuredLog); }); // Capturar erros res.on('error', (error) => { const duration = performanceNow() - startTime; const errorLog = { level: 'error', message: `HTTP Request Error: ${error.message}`, category: 'error', context: { requestId, traceId, userId: req.user?.id || req.userId }, http: { request: requestData, performance: { duration: Math.round(duration * 100) / 100, category: 'critical' } }, error: { type: 'http_error', message: error.message, stack: error.stack, resolved: false }, system: this._getSystemInfo(), sdk: { name: 'api-stats-logger', version: '1.0.0', framework: 'express' } }; logger.log(errorLog); }); 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)}`; const traceId = req.headers['x-trace-id'] || `trace_${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; req.traceId = traceId; const requestData = { method: req.method, url: req.url, path: req.path, ip: req.ip, userAgent: req.get('User-Agent'), 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); } res.on('finish', () => { const duration = performanceNow() - startTime; const level = res.statusCode >= 500 ? 'error' : res.statusCode >= 400 ? 'warn' : 'info'; const structuredLog = { level, message: `NestJS ${req.method} ${req.url} ${res.statusCode}`, category: 'http_request', context: { requestId, traceId, userId: req.user?.id }, http: { request: requestData, response: { statusCode: res.statusCode, statusText: this._getStatusText(res.statusCode), duration: Math.round(duration * 100) / 100, timestamp: new Date().toISOString() }, performance: { duration: Math.round(duration * 100) / 100, category: duration < 100 ? 'fast' : duration < 500 ? 'medium' : duration < 2000 ? 'slow' : 'critical' } }, system: this._getSystemInfo(), sdk: { name: 'api-stats-logger', version: '1.0.0', framework: 'nestjs' } }; logger.log(structuredLog); }); next(); }; } // Método utilitário para obter informações básicas do sistema (versão segura) static _getSystemInfo() { return { platform: process.platform, nodeVersion: process.version, timestamp: new Date().toISOString() }; } // Método utilitário para obter texto do status HTTP static _getStatusText(statusCode) { const statusTexts = { 200: 'OK', 201: 'Created', 204: 'No Content', 400: 'Bad Request', 401: 'Unauthorized', 403: 'Forbidden', 404: 'Not Found', 500: 'Internal Server Error', 502: 'Bad Gateway', 503: 'Service Unavailable' }; return statusTexts[statusCode] || 'Unknown Status'; } // Método utilitário para sanitizar headers static _sanitizeHeaders(headers) { const sanitized = { ...headers }; const sensitiveHeaders = ['authorization', 'cookie', 'x-api-key', 'x-auth-token']; 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]' }); }); } } }; } // Middleware para Fastify static fastify(options = {}) { const { logger, captureBody = false, captureHeaders = false, captureQuery = true, captureParams = true, skipPaths = ['/health', '/metrics'], skipMethods = ['OPTIONS'], maxBodySize = 1024 * 10 } = options; if (!logger) { throw new Error('Logger instance é obrigatório para o middleware Fastify'); } return async (request, reply) => { const startTime = performanceNow(); // Skip paths/methods específicos if (skipPaths.includes(request.url) || skipMethods.includes(request.method)) { return; } // Gerar ID único para a requisição const requestId = `fastify_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; request.requestId = requestId; // Capturar dados da requisição const requestData = { id: requestId, method: request.method, url: request.url, ip: request.ip, userAgent: request.headers['user-agent'], timestamp: new Date().toISOString() }; if (captureQuery && request.query && Object.keys(request.query).length > 0) { requestData.query = request.query; } if (captureParams && request.params && Object.keys(request.params).length > 0) { requestData.params = request.params; } if (captureHeaders) { requestData.headers = this._sanitizeHeaders(request.headers); } if (captureBody && request.body) { const bodyStr = JSON.stringify(request.body); if (bodyStr.length <= maxBodySize) { requestData.body = request.body; } else { requestData.body = '[BODY_TOO_LARGE]'; } } logger.info('Fastify Request Started', { type: 'fastify_request', request: requestData }); // Hook para capturar a resposta reply.addHook('onSend', async (request, reply, payload) => { const duration = performanceNow() - startTime; const responseData = { statusCode: reply.statusCode, duration: Math.round(duration * 100) / 100, size: payload ? payload.length : 0 }; if (captureBody && payload && payload.length <= maxBodySize) { responseData.body = payload; } if (captureHeaders) { responseData.headers = reply.getHeaders(); } const level = reply.statusCode >= 500 ? 'error' : reply.statusCode >= 400 ? 'warn' : 'info'; logger.log({ level, message: `Fastify ${request.method} ${request.url} ${reply.statusCode}`, metadata: { type: 'fastify_response', requestId, request: requestData, response: responseData, duration } }); return payload; }); }; } // Middleware para Koa static koa(options = {}) { const { logger, captureBody = false, captureHeaders = false, captureQuery = true, skipPaths = ['/health', '/metrics'], skipMethods = ['OPTIONS'], maxBodySize = 1024 * 10 } = options; if (!logger) { throw new Error('Logger instance é obrigatório para o middleware Koa'); } return async (ctx, next) => { const startTime = performanceNow(); // Skip paths/methods específicos if (skipPaths.includes(ctx.path) || skipMethods.includes(ctx.method)) { return await next(); } // Gerar ID único para a requisição const requestId = `koa_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; ctx.state.requestId = requestId; // Capturar dados da requisição const requestData = { id: requestId, method: ctx.method, url: ctx.url, path: ctx.path, ip: ctx.ip, userAgent: ctx.get('User-Agent'), timestamp: new Date().toISOString() }; if (captureQuery && ctx.query && Object.keys(ctx.query).length > 0) { requestData.query = ctx.query; } if (captureHeaders) { requestData.headers = this._sanitizeHeaders(ctx.headers); } if (captureBody && ctx.request.body) { const bodyStr = JSON.stringify(ctx.request.body); if (bodyStr.length <= maxBodySize) { requestData.body = ctx.request.body; } else { requestData.body = '[BODY_TOO_LARGE]'; } } logger.info('Koa Request Started', { type: 'koa_request', request: requestData }); try { await next(); const duration = performanceNow() - startTime; const responseData = { statusCode: ctx.status, duration: Math.round(duration * 100) / 100, size: ctx.length || 0 }; if (captureBody && ctx.body && JSON.stringify(ctx.body).length <= maxBodySize) { responseData.body = ctx.body; } if (captureHeaders) { responseData.headers = ctx.response.headers; } const level = ctx.status >= 500 ? 'error' : ctx.status >= 400 ? 'warn' : 'info'; logger.log({ level, message: `Koa ${ctx.method} ${ctx.url} ${ctx.status}`, metadata: { type: 'koa_response', requestId, request: requestData, response: responseData, duration } }); } catch (error) { const duration = performanceNow() - startTime; logger.error('Koa Request Error', { type: 'koa_error', requestId, error: error.message, stack: error.stack, request: requestData, duration }); throw error; } }; } } module.exports = MiddlewareManager;