feat: implement comprehensive startup system and fix authentication
Major improvements: - Created startup orchestration system with health monitoring and graceful shutdown - Fixed user registration and login with simplified authentication flow - Rebuilt authentication forms from scratch with direct API integration - Implemented comprehensive debugging and error handling - Added Redis fallback functionality for disabled environments - Fixed CORS configuration for cross-origin frontend requests - Simplified password validation to 6+ characters (removed complexity requirements) - Added toast notifications at app level for better UX feedback - Created comprehensive startup/shutdown scripts with OODA methodology - Fixed database validation and connection issues - Implemented TokenService memory fallback when Redis is disabled Technical details: - New SimpleLoginForm.tsx and SimpleRegisterForm.tsx components - Enhanced CORS middleware with additional allowed origins - Simplified auth validators and removed strict password requirements - Added extensive logging and diagnostic capabilities - Fixed authentication middleware token validation - Implemented graceful Redis error handling throughout the stack - Created modular startup system with configurable health checks 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
d41d1e8125
commit
e681c446b6
36 changed files with 7719 additions and 183 deletions
506
scripts/health-monitor.js
Normal file
506
scripts/health-monitor.js
Normal file
|
|
@ -0,0 +1,506 @@
|
|||
/**
|
||||
* Shattered Void MMO - Health Monitoring System
|
||||
*
|
||||
* This module provides comprehensive health monitoring for all game services,
|
||||
* including real-time status checks, performance metrics, and alerting.
|
||||
*/
|
||||
|
||||
const http = require('http');
|
||||
const { EventEmitter } = require('events');
|
||||
const os = require('os');
|
||||
|
||||
class HealthMonitor extends EventEmitter {
|
||||
constructor(options = {}) {
|
||||
super();
|
||||
|
||||
this.services = options.services || {};
|
||||
this.interval = options.interval || 30000; // 30 seconds
|
||||
this.onHealthChange = options.onHealthChange || null;
|
||||
this.timeout = options.timeout || 5000; // 5 seconds
|
||||
|
||||
this.healthStatus = {};
|
||||
this.metrics = {};
|
||||
this.alertThresholds = {
|
||||
responseTime: 5000, // 5 seconds
|
||||
memoryUsage: 80, // 80%
|
||||
cpuUsage: 90, // 90%
|
||||
errorRate: 10 // 10%
|
||||
};
|
||||
|
||||
this.monitoringInterval = null;
|
||||
this.isRunning = false;
|
||||
this.healthHistory = {};
|
||||
|
||||
// Initialize health status for all services
|
||||
this.initializeHealthStatus();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize health status tracking
|
||||
*/
|
||||
initializeHealthStatus() {
|
||||
Object.keys(this.services).forEach(serviceName => {
|
||||
this.healthStatus[serviceName] = {
|
||||
status: 'unknown',
|
||||
lastCheck: null,
|
||||
responseTime: null,
|
||||
consecutiveFailures: 0,
|
||||
uptime: 0,
|
||||
lastError: null
|
||||
};
|
||||
|
||||
this.healthHistory[serviceName] = [];
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Start health monitoring
|
||||
*/
|
||||
async start() {
|
||||
if (this.isRunning) {
|
||||
throw new Error('Health monitor is already running');
|
||||
}
|
||||
|
||||
this.isRunning = true;
|
||||
console.log(`🏥 Health monitoring started (interval: ${this.interval}ms)`);
|
||||
|
||||
// Initial health check
|
||||
await this.performHealthChecks();
|
||||
|
||||
// Start periodic monitoring
|
||||
this.monitoringInterval = setInterval(async () => {
|
||||
try {
|
||||
await this.performHealthChecks();
|
||||
} catch (error) {
|
||||
console.error('Health check error:', error);
|
||||
}
|
||||
}, this.interval);
|
||||
|
||||
// Start system metrics monitoring
|
||||
this.startSystemMetricsMonitoring();
|
||||
|
||||
this.emit('started');
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop health monitoring
|
||||
*/
|
||||
stop() {
|
||||
if (!this.isRunning) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.isRunning = false;
|
||||
|
||||
if (this.monitoringInterval) {
|
||||
clearInterval(this.monitoringInterval);
|
||||
this.monitoringInterval = null;
|
||||
}
|
||||
|
||||
console.log('🏥 Health monitoring stopped');
|
||||
this.emit('stopped');
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform health checks on all services
|
||||
*/
|
||||
async performHealthChecks() {
|
||||
const checkPromises = Object.entries(this.services).map(([serviceName, serviceInfo]) => {
|
||||
return this.checkServiceHealth(serviceName, serviceInfo);
|
||||
});
|
||||
|
||||
await Promise.allSettled(checkPromises);
|
||||
this.updateHealthSummary();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check health of a specific service
|
||||
*/
|
||||
async checkServiceHealth(serviceName, serviceInfo) {
|
||||
const startTime = Date.now();
|
||||
const previousStatus = this.healthStatus[serviceName].status;
|
||||
|
||||
try {
|
||||
let isHealthy = false;
|
||||
let responseTime = null;
|
||||
|
||||
// Different health check strategies based on service type
|
||||
switch (serviceName) {
|
||||
case 'backend':
|
||||
isHealthy = await this.checkHttpService(serviceInfo.port, '/health');
|
||||
responseTime = Date.now() - startTime;
|
||||
break;
|
||||
|
||||
case 'frontend':
|
||||
isHealthy = await this.checkHttpService(serviceInfo.port);
|
||||
responseTime = Date.now() - startTime;
|
||||
break;
|
||||
|
||||
case 'database':
|
||||
isHealthy = await this.checkDatabaseHealth();
|
||||
responseTime = Date.now() - startTime;
|
||||
break;
|
||||
|
||||
case 'redis':
|
||||
isHealthy = await this.checkRedisHealth();
|
||||
responseTime = Date.now() - startTime;
|
||||
break;
|
||||
|
||||
default:
|
||||
// For other services, assume healthy if they exist
|
||||
isHealthy = true;
|
||||
responseTime = Date.now() - startTime;
|
||||
}
|
||||
|
||||
// Update health status
|
||||
const newStatus = isHealthy ? 'healthy' : 'unhealthy';
|
||||
this.updateServiceStatus(serviceName, {
|
||||
status: newStatus,
|
||||
lastCheck: new Date(),
|
||||
responseTime,
|
||||
consecutiveFailures: isHealthy ? 0 : this.healthStatus[serviceName].consecutiveFailures + 1,
|
||||
lastError: null
|
||||
});
|
||||
|
||||
// Emit health change event if status changed
|
||||
if (previousStatus !== newStatus && this.onHealthChange) {
|
||||
this.onHealthChange(serviceName, newStatus);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
const responseTime = Date.now() - startTime;
|
||||
|
||||
this.updateServiceStatus(serviceName, {
|
||||
status: 'unhealthy',
|
||||
lastCheck: new Date(),
|
||||
responseTime,
|
||||
consecutiveFailures: this.healthStatus[serviceName].consecutiveFailures + 1,
|
||||
lastError: error.message
|
||||
});
|
||||
|
||||
// Emit health change event if status changed
|
||||
if (previousStatus !== 'unhealthy' && this.onHealthChange) {
|
||||
this.onHealthChange(serviceName, 'unhealthy');
|
||||
}
|
||||
|
||||
console.error(`Health check failed for ${serviceName}:`, error.message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check HTTP service health
|
||||
*/
|
||||
checkHttpService(port, path = '/') {
|
||||
return new Promise((resolve, reject) => {
|
||||
const options = {
|
||||
hostname: 'localhost',
|
||||
port: port,
|
||||
path: path,
|
||||
method: 'GET',
|
||||
timeout: this.timeout
|
||||
};
|
||||
|
||||
const req = http.request(options, (res) => {
|
||||
// Consider 2xx and 3xx status codes as healthy
|
||||
resolve(res.statusCode >= 200 && res.statusCode < 400);
|
||||
});
|
||||
|
||||
req.on('error', (error) => {
|
||||
reject(error);
|
||||
});
|
||||
|
||||
req.on('timeout', () => {
|
||||
req.destroy();
|
||||
reject(new Error('Request timeout'));
|
||||
});
|
||||
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check database health
|
||||
*/
|
||||
async checkDatabaseHealth() {
|
||||
try {
|
||||
// Try to get database connection from the app
|
||||
const db = require('../src/database/connection');
|
||||
|
||||
// Simple query to check database connectivity
|
||||
await db.raw('SELECT 1');
|
||||
return true;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check Redis health
|
||||
*/
|
||||
async checkRedisHealth() {
|
||||
try {
|
||||
// Skip if Redis is disabled
|
||||
if (process.env.DISABLE_REDIS === 'true') {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Try to get Redis client from the app
|
||||
const redisConfig = require('../src/config/redis');
|
||||
|
||||
if (!redisConfig.client) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Simple ping to check Redis connectivity
|
||||
await redisConfig.client.ping();
|
||||
return true;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update service status
|
||||
*/
|
||||
updateServiceStatus(serviceName, statusUpdate) {
|
||||
this.healthStatus[serviceName] = {
|
||||
...this.healthStatus[serviceName],
|
||||
...statusUpdate
|
||||
};
|
||||
|
||||
// Add to health history
|
||||
this.addToHealthHistory(serviceName, statusUpdate);
|
||||
|
||||
// Check for alerts
|
||||
this.checkForAlerts(serviceName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add health data to history
|
||||
*/
|
||||
addToHealthHistory(serviceName, statusData) {
|
||||
const historyEntry = {
|
||||
timestamp: Date.now(),
|
||||
status: statusData.status,
|
||||
responseTime: statusData.responseTime,
|
||||
error: statusData.lastError
|
||||
};
|
||||
|
||||
this.healthHistory[serviceName].push(historyEntry);
|
||||
|
||||
// Keep only last 100 entries
|
||||
if (this.healthHistory[serviceName].length > 100) {
|
||||
this.healthHistory[serviceName] = this.healthHistory[serviceName].slice(-100);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for health alerts
|
||||
*/
|
||||
checkForAlerts(serviceName) {
|
||||
const health = this.healthStatus[serviceName];
|
||||
const alerts = [];
|
||||
|
||||
// Check consecutive failures
|
||||
if (health.consecutiveFailures >= 3) {
|
||||
alerts.push({
|
||||
type: 'consecutive_failures',
|
||||
message: `Service ${serviceName} has failed ${health.consecutiveFailures} consecutive times`,
|
||||
severity: 'critical'
|
||||
});
|
||||
}
|
||||
|
||||
// Check response time
|
||||
if (health.responseTime && health.responseTime > this.alertThresholds.responseTime) {
|
||||
alerts.push({
|
||||
type: 'slow_response',
|
||||
message: `Service ${serviceName} response time: ${health.responseTime}ms (threshold: ${this.alertThresholds.responseTime}ms)`,
|
||||
severity: 'warning'
|
||||
});
|
||||
}
|
||||
|
||||
// Emit alerts
|
||||
alerts.forEach(alert => {
|
||||
this.emit('alert', serviceName, alert);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Start system metrics monitoring
|
||||
*/
|
||||
startSystemMetricsMonitoring() {
|
||||
const updateSystemMetrics = () => {
|
||||
const memUsage = process.memoryUsage();
|
||||
const cpuUsage = process.cpuUsage();
|
||||
const systemMem = {
|
||||
total: os.totalmem(),
|
||||
free: os.freemem()
|
||||
};
|
||||
|
||||
this.metrics.system = {
|
||||
timestamp: Date.now(),
|
||||
memory: {
|
||||
rss: memUsage.rss,
|
||||
heapTotal: memUsage.heapTotal,
|
||||
heapUsed: memUsage.heapUsed,
|
||||
external: memUsage.external,
|
||||
usage: Math.round((memUsage.heapUsed / memUsage.heapTotal) * 100)
|
||||
},
|
||||
cpu: {
|
||||
user: cpuUsage.user,
|
||||
system: cpuUsage.system
|
||||
},
|
||||
systemMemory: {
|
||||
total: systemMem.total,
|
||||
free: systemMem.free,
|
||||
used: systemMem.total - systemMem.free,
|
||||
usage: Math.round(((systemMem.total - systemMem.free) / systemMem.total) * 100)
|
||||
},
|
||||
uptime: process.uptime(),
|
||||
loadAverage: os.loadavg()
|
||||
};
|
||||
|
||||
// Check for system alerts
|
||||
this.checkSystemAlerts();
|
||||
};
|
||||
|
||||
// Update immediately
|
||||
updateSystemMetrics();
|
||||
|
||||
// Update every 10 seconds
|
||||
setInterval(updateSystemMetrics, 10000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for system-level alerts
|
||||
*/
|
||||
checkSystemAlerts() {
|
||||
const metrics = this.metrics.system;
|
||||
|
||||
if (!metrics) return;
|
||||
|
||||
// Memory usage alert
|
||||
if (metrics.memory.usage > this.alertThresholds.memoryUsage) {
|
||||
this.emit('alert', 'system', {
|
||||
type: 'high_memory_usage',
|
||||
message: `High memory usage: ${metrics.memory.usage}% (threshold: ${this.alertThresholds.memoryUsage}%)`,
|
||||
severity: 'warning'
|
||||
});
|
||||
}
|
||||
|
||||
// System memory alert
|
||||
if (metrics.systemMemory.usage > this.alertThresholds.memoryUsage) {
|
||||
this.emit('alert', 'system', {
|
||||
type: 'high_system_memory',
|
||||
message: `High system memory usage: ${metrics.systemMemory.usage}% (threshold: ${this.alertThresholds.memoryUsage}%)`,
|
||||
severity: 'critical'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update overall health summary
|
||||
*/
|
||||
updateHealthSummary() {
|
||||
const services = Object.keys(this.healthStatus);
|
||||
const healthyServices = services.filter(s => this.healthStatus[s].status === 'healthy');
|
||||
const unhealthyServices = services.filter(s => this.healthStatus[s].status === 'unhealthy');
|
||||
|
||||
this.metrics.summary = {
|
||||
timestamp: Date.now(),
|
||||
totalServices: services.length,
|
||||
healthyServices: healthyServices.length,
|
||||
unhealthyServices: unhealthyServices.length,
|
||||
overallHealth: unhealthyServices.length === 0 ? 'healthy' : 'degraded'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current health status
|
||||
*/
|
||||
getHealthStatus() {
|
||||
return {
|
||||
services: this.healthStatus,
|
||||
metrics: this.metrics,
|
||||
summary: this.metrics.summary,
|
||||
isRunning: this.isRunning
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get health history for a service
|
||||
*/
|
||||
getHealthHistory(serviceName) {
|
||||
return this.healthHistory[serviceName] || [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get service uptime
|
||||
*/
|
||||
getServiceUptime(serviceName) {
|
||||
const history = this.healthHistory[serviceName];
|
||||
if (!history || history.length === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
const oneDayAgo = now - (24 * 60 * 60 * 1000);
|
||||
|
||||
const recentHistory = history.filter(entry => entry.timestamp > oneDayAgo);
|
||||
|
||||
if (recentHistory.length === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const healthyCount = recentHistory.filter(entry => entry.status === 'healthy').length;
|
||||
return Math.round((healthyCount / recentHistory.length) * 100);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate health report
|
||||
*/
|
||||
generateHealthReport() {
|
||||
const services = Object.keys(this.healthStatus);
|
||||
const report = {
|
||||
timestamp: new Date().toISOString(),
|
||||
summary: this.metrics.summary,
|
||||
services: {},
|
||||
systemMetrics: this.metrics.system,
|
||||
alerts: []
|
||||
};
|
||||
|
||||
services.forEach(serviceName => {
|
||||
const health = this.healthStatus[serviceName];
|
||||
const uptime = this.getServiceUptime(serviceName);
|
||||
|
||||
report.services[serviceName] = {
|
||||
status: health.status,
|
||||
lastCheck: health.lastCheck,
|
||||
responseTime: health.responseTime,
|
||||
consecutiveFailures: health.consecutiveFailures,
|
||||
uptime: `${uptime}%`,
|
||||
lastError: health.lastError
|
||||
};
|
||||
});
|
||||
|
||||
return report;
|
||||
}
|
||||
|
||||
/**
|
||||
* Export health data for monitoring systems
|
||||
*/
|
||||
exportMetrics() {
|
||||
return {
|
||||
timestamp: Date.now(),
|
||||
services: this.healthStatus,
|
||||
system: this.metrics.system,
|
||||
summary: this.metrics.summary,
|
||||
uptime: Object.keys(this.healthStatus).reduce((acc, serviceName) => {
|
||||
acc[serviceName] = this.getServiceUptime(serviceName);
|
||||
return acc;
|
||||
}, {})
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = HealthMonitor;
|
||||
Loading…
Add table
Add a link
Reference in a new issue