appointment_system/backend/src/services/monitoringService.js
2025-12-11 22:50:18 +08:00

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