health-monitor.cjsā¢9.04 kB
#!/usr/bin/env node
// Health Monitor for EGW Writings MCP Server
// Monitors coldstart, heartbeat, and system health
const { SimpleEGWDatabase } = require('./database-utils.js');
class HealthMonitor {
constructor() {
this.isRunning = false;
this.monitorInterval = 30000; // 30 seconds
this.healthHistory = [];
}
async startMonitoring() {
if (this.isRunning) {
console.log('š„ Health monitoring already running');
return;
}
console.log('š„ Starting EGW Writings Health Monitor...');
this.isRunning = true;
// Initial health check
await this.performHealthCheck();
// Set up periodic monitoring
this.monitorTimer = setInterval(async () => {
await this.performHealthCheck();
}, this.monitorInterval);
console.log(`ā
Health monitor started (checking every ${this.monitorInterval / 1000}s)`);
// Handle graceful shutdown
process.on('SIGINT', () => this.stopMonitoring());
process.on('SIGTERM', () => this.stopMonitoring());
}
async performHealthCheck() {
try {
const health = await SimpleEGWDatabase.performHealthCheck();
const timestamp = new Date().toISOString();
// Analyze health status
const analysis = this.analyzeHealth(health);
console.log(`\nš„ Health Check - ${timestamp}`);
console.log('=' .repeat(60));
// Database status
console.log(`š Database: ${health.database.isWarm ? 'ā
WARM' : 'ā COLD'}`);
if (health.database.lastWarmup) {
const warmupAge = Date.now() - new Date(health.database.lastWarmup).getTime();
console.log(` Last warmup: ${Math.round(warmupAge / 1000)}s ago`);
}
// Heartbeat status
console.log(`š Heartbeat: ${health.heartbeat.isActive ? 'ā
ACTIVE' : 'ā INACTIVE'}`);
if (health.heartbeat.lastHeartbeat) {
const heartbeatAge = Date.now() - new Date(health.heartbeat.lastHeartbeat).getTime();
const status = heartbeatAge < 60000 ? 'ā
FRESH' : 'ā ļø STALE';
console.log(` Last heartbeat: ${Math.round(heartbeatAge / 1000)}s ago (${status})`);
}
// Persistent heartbeat status
if (health.heartbeat.persistent) {
if (health.heartbeat.persistent.isValid) {
console.log(` Persistent: ā
VALID (${Math.round(health.heartbeat.persistent.age / 1000)}s old)`);
} else {
console.log(` Persistent: ā INVALID/EXPIRED`);
}
}
// Connection pool status
if (health.connectionPool) {
console.log(`š Connections: ${health.connectionPool.size}/${health.connectionPool.size + health.connectionPool.waiting} total`);
if (health.connectionPool.waiting > 0) {
console.log(` ā ļø ${health.connectionPool.waiting} requests waiting`);
}
}
// Overall system status
console.log(`šÆ Overall: ${analysis.overall}`);
if (analysis.issues.length > 0) {
console.log('ā ļø Issues detected:');
analysis.issues.forEach(issue => console.log(` ⢠${issue}`));
}
if (analysis.recommendations.length > 0) {
console.log('š” Recommendations:');
analysis.recommendations.forEach(rec => console.log(` ⢠${rec}`));
}
console.log('=' .repeat(60));
// Store in history
this.healthHistory.push({
timestamp,
health,
analysis
});
// Keep only last 10 checks
if (this.healthHistory.length > 10) {
this.healthHistory.shift();
}
} catch (error) {
console.error('ā Health check failed:', error.message);
}
}
analyzeHealth(health) {
const analysis = {
overall: 'ā
HEALTHY',
issues: [],
recommendations: []
};
// Check database warm status
if (!health.database.isWarm) {
analysis.issues.push('Database is cold - warming may be needed');
analysis.recommendations.push('Run database warmup manually');
analysis.overall = 'ā ļø DEGRADED';
}
// Check heartbeat status
if (!health.heartbeat.isActive) {
analysis.issues.push('No active heartbeat detected');
analysis.recommendations.push('Start heartbeat process');
analysis.overall = 'ā UNHEALTHY';
} else if (health.heartbeat.lastHeartbeat) {
const heartbeatAge = Date.now() - new Date(health.heartbeat.lastHeartbeat).getTime();
if (heartbeatAge > 60000) { // More than 1 minute
analysis.issues.push('Heartbeat is stale (>60s)');
analysis.recommendations.push('Restart heartbeat process');
analysis.overall = 'ā ļø DEGRADED';
}
}
// Check persistent heartbeat
if (health.heartbeat.persistent && !health.heartbeat.persistent.isValid) {
analysis.issues.push('Persistent heartbeat is invalid');
analysis.recommendations.push('Clear persistent heartbeat file');
}
// Check connection pool
if (health.connectionPool && health.connectionPool.waiting > 3) {
analysis.issues.push('Connection pool backlog detected');
analysis.recommendations.push('Monitor database load');
analysis.overall = 'ā ļø DEGRADED';
}
return analysis;
}
async stopMonitoring() {
if (!this.isRunning) {
console.log('š„ Health monitor not running');
return;
}
console.log('\nš Stopping health monitor...');
if (this.monitorTimer) {
clearInterval(this.monitorTimer);
this.monitorTimer = null;
}
this.isRunning = false;
// Generate summary report
this.generateSummaryReport();
console.log('ā
Health monitor stopped');
process.exit(0);
}
generateSummaryReport() {
if (this.healthHistory.length === 0) {
console.log('š No health data collected');
return;
}
console.log('\nš Health Monitor Summary Report');
console.log('=' .repeat(50));
const healthyCount = this.healthHistory.filter(h => h.analysis.overall.includes('HEALTHY')).length;
const degradedCount = this.healthHistory.filter(h => h.analysis.overall.includes('DEGRADED')).length;
const unhealthyCount = this.healthHistory.filter(h => h.analysis.overall.includes('UNHEALTHY')).length;
console.log(`Total checks: ${this.healthHistory.length}`);
console.log(`ā
Healthy: ${healthyCount} (${(healthyCount / this.healthHistory.length * 100).toFixed(1)}%)`);
console.log(`ā ļø Degraded: ${degradedCount} (${(degradedCount / this.healthHistory.length * 100).toFixed(1)}%)`);
console.log(`ā Unhealthy: ${unhealthyCount} (${(unhealthyCount / this.healthHistory.length * 100).toFixed(1)}%)`);
// Most common issues
const allIssues = this.healthHistory.flatMap(h => h.analysis.issues);
const issueCounts = {};
allIssues.forEach(issue => {
issueCounts[issue] = (issueCounts[issue] || 0) + 1;
});
const sortedIssues = Object.entries(issueCounts)
.sort(([,a], [,b]) => b - a)
.slice(0, 5);
if (sortedIssues.length > 0) {
console.log('\nš Most Common Issues:');
sortedIssues.forEach(([issue, count]) => {
console.log(` ⢠${issue} (${count} times)`);
});
}
}
async showStatus() {
const health = await SimpleEGWDatabase.performHealthCheck();
const analysis = this.analyzeHealth(health);
console.log('\nš„ Current System Status:');
console.log(`Overall: ${analysis.overall}`);
if (analysis.issues.length > 0) {
console.log('\nIssues:');
analysis.issues.forEach(issue => console.log(` ⢠${issue}`));
}
if (analysis.recommendations.length > 0) {
console.log('\nRecommendations:');
analysis.recommendations.forEach(rec => console.log(` ⢠${rec}`));
}
}
}
// CLI interface
async function main() {
const command = process.argv[2];
const monitor = new HealthMonitor();
switch (command) {
case 'start':
await monitor.startMonitoring();
break;
case 'check':
await monitor.showStatus();
break;
case 'once':
await monitor.performHealthCheck();
break;
default:
console.log(`
EGW Writings Health Monitor
Usage: node health-monitor.cjs <command>
Commands:
start Start continuous health monitoring
check Show current system status
once Perform single health check
Examples:
node health-monitor.cjs start
node health-monitor.cjs check
node health-monitor.cjs once
The health monitor tracks:
⢠Database warm status and warmup times
⢠Heartbeat activity and persistence
⢠Connection pool utilization
⢠Overall system health and recommendations
⢠Historical trends and issue patterns
`);
}
}
if (require.main === module) {
main().catch(error => {
console.error('ā Health monitor error:', error.message);
process.exit(1);
});
}
module.exports = { HealthMonitor };