← back to Watches
monitoring/health-monitor.js
298 lines
/**
* Health Monitoring System
* Continuously monitors application health and sends alerts
*/
import cron from 'node-cron';
import fs from 'fs/promises';
import path from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const CONFIG = {
checkInterval: '*/5 * * * *', // Every 5 minutes
healthEndpoint: 'http://localhost:7600/api/health',
alertThresholds: {
memoryUsagePercent: 85,
cpuUsagePercent: 80,
responseTimeMs: 3000,
errorRate: 5 // errors per minute
},
metricsFile: path.join(__dirname, '../logs/metrics.json'),
slackWebhook: process.env.SLACK_WEBHOOK_URL,
emailAlert: process.env.EMAIL_ALERTS_TO
};
class HealthMonitor {
constructor() {
this.metrics = {
uptime: 0,
lastCheck: null,
status: 'unknown',
alerts: [],
history: []
};
this.errorCount = 0;
this.lastErrorReset = Date.now();
}
async start() {
console.log('🔍 Starting Health Monitor...');
// Run initial check
await this.checkHealth();
// Schedule periodic checks
cron.schedule(CONFIG.checkInterval, async () => {
await this.checkHealth();
});
// Reset error counter every minute
cron.schedule('* * * * *', () => {
this.errorCount = 0;
this.lastErrorReset = Date.now();
});
console.log(`✓ Health Monitor active (checking every 5 minutes)`);
}
async checkHealth() {
const startTime = Date.now();
try {
const response = await fetch(CONFIG.healthEndpoint, {
timeout: 5000
});
if (!response.ok) {
throw new Error(`Health check failed with status ${response.status}`);
}
const data = await response.json();
const responseTime = Date.now() - startTime;
// Update metrics
this.metrics.status = 'healthy';
this.metrics.lastCheck = new Date().toISOString();
this.metrics.uptime = data.uptime;
// Check thresholds
await this.checkThresholds(data, responseTime);
// Store metrics
await this.storeMetrics(data, responseTime);
console.log(`✓ Health check passed (${responseTime}ms) - Memory: ${Math.round(data.memory.heapUsed / 1024 / 1024)}MB`);
} catch (error) {
this.errorCount++;
this.metrics.status = 'unhealthy';
console.error(`✗ Health check failed:`, error.message);
await this.sendAlert({
severity: 'critical',
title: 'Service Health Check Failed',
message: error.message,
timestamp: new Date().toISOString()
});
// Check if service needs restart
if (this.errorCount >= 3) {
await this.attemptAutoRecover();
}
}
}
async checkThresholds(data, responseTime) {
const alerts = [];
// Check response time
if (responseTime > CONFIG.alertThresholds.responseTimeMs) {
alerts.push({
severity: 'warning',
metric: 'response_time',
message: `High response time: ${responseTime}ms`,
value: responseTime,
threshold: CONFIG.alertThresholds.responseTimeMs
});
}
// Check memory usage
const memoryUsagePercent = (data.memory.heapUsed / data.memory.heapTotal) * 100;
if (memoryUsagePercent > CONFIG.alertThresholds.memoryUsagePercent) {
alerts.push({
severity: 'warning',
metric: 'memory_usage',
message: `High memory usage: ${memoryUsagePercent.toFixed(2)}%`,
value: memoryUsagePercent,
threshold: CONFIG.alertThresholds.memoryUsagePercent
});
}
// Check error rate
if (this.errorCount >= CONFIG.alertThresholds.errorRate) {
alerts.push({
severity: 'critical',
metric: 'error_rate',
message: `High error rate: ${this.errorCount} errors/min`,
value: this.errorCount,
threshold: CONFIG.alertThresholds.errorRate
});
}
// Send alerts if any
for (const alert of alerts) {
await this.sendAlert(alert);
}
}
async storeMetrics(data, responseTime) {
const metric = {
timestamp: new Date().toISOString(),
uptime: data.uptime,
memory: {
used: Math.round(data.memory.heapUsed / 1024 / 1024),
total: Math.round(data.memory.heapTotal / 1024 / 1024),
percent: ((data.memory.heapUsed / data.memory.heapTotal) * 100).toFixed(2)
},
cache: data.cache,
database: data.database,
websocket: data.websocket,
responseTime
};
// Keep last 288 entries (24 hours at 5-minute intervals)
this.metrics.history.push(metric);
if (this.metrics.history.length > 288) {
this.metrics.history.shift();
}
// Save to file
try {
await fs.writeFile(
CONFIG.metricsFile,
JSON.stringify(this.metrics, null, 2)
);
} catch (error) {
console.error('Failed to save metrics:', error);
}
}
async sendAlert(alert) {
console.log(`🚨 ALERT [${alert.severity}]: ${alert.message || alert.title}`);
this.metrics.alerts.push({
...alert,
timestamp: alert.timestamp || new Date().toISOString()
});
// Keep only last 100 alerts
if (this.metrics.alerts.length > 100) {
this.metrics.alerts.shift();
}
// Send to Slack
if (CONFIG.slackWebhook) {
await this.sendSlackAlert(alert);
}
// Could add email alerts here
// if (CONFIG.emailAlert) {
// await this.sendEmailAlert(alert);
// }
}
async sendSlackAlert(alert) {
try {
const color = alert.severity === 'critical' ? 'danger' : 'warning';
const emoji = alert.severity === 'critical' ? '🔴' : '⚠️';
const payload = {
text: `${emoji} Omega Watches Alert`,
attachments: [{
color,
title: alert.title || alert.metric,
text: alert.message,
fields: [
{
title: 'Severity',
value: alert.severity,
short: true
},
{
title: 'Timestamp',
value: alert.timestamp,
short: true
}
],
footer: 'Omega Watch Price History Monitor'
}]
};
await fetch(CONFIG.slackWebhook, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
} catch (error) {
console.error('Failed to send Slack alert:', error);
}
}
async attemptAutoRecover() {
console.log('🔄 Attempting auto-recovery...');
try {
// Try to restart PM2 process
const { exec } = await import('child_process');
const { promisify } = await import('util');
const execAsync = promisify(exec);
await execAsync('pm2 restart omega-watches');
console.log('✓ Service restarted successfully');
await this.sendAlert({
severity: 'warning',
title: 'Auto-Recovery Executed',
message: 'Service was automatically restarted due to repeated failures'
});
this.errorCount = 0;
} catch (error) {
console.error('✗ Auto-recovery failed:', error);
await this.sendAlert({
severity: 'critical',
title: 'Auto-Recovery Failed',
message: `Manual intervention required: ${error.message}`
});
}
}
async getMetrics() {
return this.metrics;
}
async getStatus() {
return {
status: this.metrics.status,
lastCheck: this.metrics.lastCheck,
uptime: this.metrics.uptime,
recentAlerts: this.metrics.alerts.slice(-10)
};
}
}
// Export singleton instance
const monitor = new HealthMonitor();
// Start monitoring if run directly
if (import.meta.url === `file://${process.argv[1]}`) {
monitor.start();
}
export default monitor;