@erickzao/api-stats-cli
Version:
CLI oficial para configuração automática do API Stats Logger
293 lines (250 loc) • 8.37 kB
JavaScript
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;