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
JavaScript
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;