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, };