317 lines
8.0 KiB
JavaScript
317 lines
8.0 KiB
JavaScript
const { sequelize } = require('../config/database');
|
|
const logger = require('../config/logger');
|
|
|
|
// Metrics storage
|
|
const metrics = {
|
|
requests: {
|
|
total: 0,
|
|
success: 0,
|
|
error: 0,
|
|
byEndpoint: {},
|
|
byStatusCode: {},
|
|
},
|
|
responseTime: {
|
|
total: 0,
|
|
count: 0,
|
|
min: Infinity,
|
|
max: 0,
|
|
},
|
|
errors: [],
|
|
startTime: Date.now(),
|
|
};
|
|
|
|
// Alert thresholds
|
|
const alertThresholds = {
|
|
errorRatePercent: 5, // Alert if error rate exceeds 5%
|
|
responseTimeMs: 500, // Alert if avg response time exceeds 500ms
|
|
dbPoolUsagePercent: 80, // Alert if DB pool usage exceeds 80%
|
|
memoryUsagePercent: 90, // Alert if memory usage exceeds 90%
|
|
};
|
|
|
|
/**
|
|
* Record a request metric
|
|
*/
|
|
function recordRequest(endpoint, statusCode, responseTimeMs) {
|
|
metrics.requests.total++;
|
|
|
|
if (statusCode >= 200 && statusCode < 400) {
|
|
metrics.requests.success++;
|
|
} else {
|
|
metrics.requests.error++;
|
|
}
|
|
|
|
// Track by endpoint
|
|
if (!metrics.requests.byEndpoint[endpoint]) {
|
|
metrics.requests.byEndpoint[endpoint] = { total: 0, success: 0, error: 0 };
|
|
}
|
|
metrics.requests.byEndpoint[endpoint].total++;
|
|
if (statusCode >= 200 && statusCode < 400) {
|
|
metrics.requests.byEndpoint[endpoint].success++;
|
|
} else {
|
|
metrics.requests.byEndpoint[endpoint].error++;
|
|
}
|
|
|
|
// Track by status code
|
|
if (!metrics.requests.byStatusCode[statusCode]) {
|
|
metrics.requests.byStatusCode[statusCode] = 0;
|
|
}
|
|
metrics.requests.byStatusCode[statusCode]++;
|
|
|
|
// Track response time
|
|
metrics.responseTime.total += responseTimeMs;
|
|
metrics.responseTime.count++;
|
|
metrics.responseTime.min = Math.min(metrics.responseTime.min, responseTimeMs);
|
|
metrics.responseTime.max = Math.max(metrics.responseTime.max, responseTimeMs);
|
|
}
|
|
|
|
/**
|
|
* Record an error for alerting
|
|
*/
|
|
function recordError(error, context = {}) {
|
|
const errorRecord = {
|
|
timestamp: new Date().toISOString(),
|
|
message: error.message,
|
|
stack: error.stack,
|
|
context,
|
|
};
|
|
|
|
metrics.errors.push(errorRecord);
|
|
|
|
// Keep only last 100 errors
|
|
if (metrics.errors.length > 100) {
|
|
metrics.errors.shift();
|
|
}
|
|
|
|
// Check if we need to trigger an alert
|
|
checkAlerts();
|
|
}
|
|
|
|
/**
|
|
* Check database connection health
|
|
*/
|
|
async function checkDatabaseHealth() {
|
|
try {
|
|
await sequelize.authenticate();
|
|
const pool = sequelize.connectionManager.pool;
|
|
|
|
return {
|
|
status: 'healthy',
|
|
pool: {
|
|
size: pool ? pool.size : 0,
|
|
available: pool ? pool.available : 0,
|
|
pending: pool ? pool.pending : 0,
|
|
max: sequelize.options.pool?.max || 10,
|
|
min: sequelize.options.pool?.min || 0,
|
|
},
|
|
};
|
|
} catch (error) {
|
|
logger.error('Database health check failed:', error);
|
|
return {
|
|
status: 'unhealthy',
|
|
error: error.message,
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check Redis connection health
|
|
*/
|
|
async function checkRedisHealth() {
|
|
try {
|
|
// Try to get Redis client - may not be initialized in test environment
|
|
const { getRedisClient } = require('../config/redis');
|
|
const client = getRedisClient();
|
|
|
|
const pingResult = await client.ping();
|
|
const info = await client.info('clients');
|
|
|
|
// Parse connected clients from info
|
|
const connectedClientsMatch = info.match(/connected_clients:(\d+)/);
|
|
const connectedClients = connectedClientsMatch ? parseInt(connectedClientsMatch[1], 10) : 0;
|
|
|
|
return {
|
|
status: pingResult === 'PONG' ? 'healthy' : 'unhealthy',
|
|
ping: pingResult,
|
|
connectedClients,
|
|
};
|
|
} catch (error) {
|
|
// Redis might not be initialized in test environment
|
|
if (error.message.includes('not initialized')) {
|
|
return {
|
|
status: 'not_initialized',
|
|
message: 'Redis client not initialized',
|
|
};
|
|
}
|
|
logger.error('Redis health check failed:', error);
|
|
return {
|
|
status: 'unhealthy',
|
|
error: error.message,
|
|
};
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Get memory usage statistics
|
|
*/
|
|
function getMemoryUsage() {
|
|
const memUsage = process.memoryUsage();
|
|
return {
|
|
heapUsed: memUsage.heapUsed,
|
|
heapTotal: memUsage.heapTotal,
|
|
external: memUsage.external,
|
|
rss: memUsage.rss,
|
|
heapUsedMB: Math.round(memUsage.heapUsed / 1024 / 1024 * 100) / 100,
|
|
heapTotalMB: Math.round(memUsage.heapTotal / 1024 / 1024 * 100) / 100,
|
|
rssMB: Math.round(memUsage.rss / 1024 / 1024 * 100) / 100,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Get all metrics
|
|
*/
|
|
function getMetrics() {
|
|
const avgResponseTime =
|
|
metrics.responseTime.count > 0
|
|
? Math.round(metrics.responseTime.total / metrics.responseTime.count)
|
|
: 0;
|
|
|
|
const errorRate =
|
|
metrics.requests.total > 0
|
|
? Math.round((metrics.requests.error / metrics.requests.total) * 10000) / 100
|
|
: 0;
|
|
|
|
return {
|
|
uptime: Math.round((Date.now() - metrics.startTime) / 1000),
|
|
requests: {
|
|
total: metrics.requests.total,
|
|
success: metrics.requests.success,
|
|
error: metrics.requests.error,
|
|
errorRate: `${errorRate}%`,
|
|
byStatusCode: metrics.requests.byStatusCode,
|
|
},
|
|
responseTime: {
|
|
average: avgResponseTime,
|
|
min: metrics.responseTime.min === Infinity ? 0 : metrics.responseTime.min,
|
|
max: metrics.responseTime.max,
|
|
},
|
|
memory: getMemoryUsage(),
|
|
recentErrors: metrics.errors.slice(-10),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Check alerts and log warnings
|
|
*/
|
|
function checkAlerts() {
|
|
const currentMetrics = getMetrics();
|
|
|
|
// Check error rate
|
|
const errorRate = parseFloat(currentMetrics.requests.errorRate);
|
|
if (errorRate > alertThresholds.errorRatePercent && metrics.requests.total > 100) {
|
|
logger.warn(`ALERT: High error rate detected: ${errorRate}%`, {
|
|
threshold: alertThresholds.errorRatePercent,
|
|
current: errorRate,
|
|
});
|
|
}
|
|
|
|
// Check response time
|
|
if (currentMetrics.responseTime.average > alertThresholds.responseTimeMs) {
|
|
logger.warn(`ALERT: High average response time: ${currentMetrics.responseTime.average}ms`, {
|
|
threshold: alertThresholds.responseTimeMs,
|
|
current: currentMetrics.responseTime.average,
|
|
});
|
|
}
|
|
|
|
// Check memory usage
|
|
const memUsage = currentMetrics.memory;
|
|
const heapUsagePercent = (memUsage.heapUsed / memUsage.heapTotal) * 100;
|
|
if (heapUsagePercent > alertThresholds.memoryUsagePercent) {
|
|
logger.warn(`ALERT: High memory usage: ${heapUsagePercent.toFixed(2)}%`, {
|
|
threshold: alertThresholds.memoryUsagePercent,
|
|
current: heapUsagePercent,
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check database pool usage and alert if needed
|
|
*/
|
|
async function checkDatabasePoolAlert() {
|
|
const dbHealth = await checkDatabaseHealth();
|
|
if (dbHealth.status === 'healthy' && dbHealth.pool) {
|
|
const usagePercent = ((dbHealth.pool.size - dbHealth.pool.available) / dbHealth.pool.max) * 100;
|
|
if (usagePercent > alertThresholds.dbPoolUsagePercent) {
|
|
logger.warn(`ALERT: High database pool usage: ${usagePercent.toFixed(2)}%`, {
|
|
threshold: alertThresholds.dbPoolUsagePercent,
|
|
current: usagePercent,
|
|
pool: dbHealth.pool,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get comprehensive health status
|
|
*/
|
|
async function getHealthStatus() {
|
|
const [dbHealth, redisHealth] = await Promise.all([
|
|
checkDatabaseHealth(),
|
|
checkRedisHealth(),
|
|
]);
|
|
|
|
const overallStatus =
|
|
dbHealth.status === 'healthy' &&
|
|
(redisHealth.status === 'healthy' || redisHealth.status === 'not_initialized')
|
|
? 'healthy'
|
|
: 'degraded';
|
|
|
|
return {
|
|
status: overallStatus,
|
|
timestamp: new Date().toISOString(),
|
|
uptime: process.uptime(),
|
|
version: process.env.npm_package_version || '1.0.0',
|
|
nodeVersion: process.version,
|
|
services: {
|
|
database: dbHealth,
|
|
redis: redisHealth,
|
|
},
|
|
memory: getMemoryUsage(),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Reset metrics (useful for testing)
|
|
*/
|
|
function resetMetrics() {
|
|
metrics.requests = {
|
|
total: 0,
|
|
success: 0,
|
|
error: 0,
|
|
byEndpoint: {},
|
|
byStatusCode: {},
|
|
};
|
|
metrics.responseTime = {
|
|
total: 0,
|
|
count: 0,
|
|
min: Infinity,
|
|
max: 0,
|
|
};
|
|
metrics.errors = [];
|
|
metrics.startTime = Date.now();
|
|
}
|
|
|
|
module.exports = {
|
|
recordRequest,
|
|
recordError,
|
|
checkDatabaseHealth,
|
|
checkRedisHealth,
|
|
getMemoryUsage,
|
|
getMetrics,
|
|
getHealthStatus,
|
|
checkAlerts,
|
|
checkDatabasePoolAlert,
|
|
resetMetrics,
|
|
alertThresholds,
|
|
};
|