@mraicodedev/bsc-token-detector
Version:
🚀 Real-time BSC token detector with advanced filtering, health monitoring, and comprehensive event system. Detect new tokens on Binance Smart Chain as they are created!
741 lines (643 loc) • 19.2 kB
JavaScript
/**
* Health Monitoring System for BSC Token Detector
* Provides comprehensive health checks and status reporting for all components
*/
import { EventEmitter } from 'events';
import { Logger } from './Logger.js';
/**
* Health status levels
*/
export const HealthStatus = {
HEALTHY: 'healthy',
DEGRADED: 'degraded',
UNHEALTHY: 'unhealthy',
CRITICAL: 'critical',
UNKNOWN: 'unknown'
};
/**
* Health check result
*/
export class HealthCheckResult {
constructor(name, status, message = '', details = {}, duration = 0) {
this.name = name;
this.status = status;
this.message = message;
this.details = details;
this.duration = duration;
this.timestamp = new Date().toISOString();
}
/**
* Check if health check passed
*/
isHealthy() {
return this.status === HealthStatus.HEALTHY;
}
/**
* Check if health check indicates degraded performance
*/
isDegraded() {
return this.status === HealthStatus.DEGRADED;
}
/**
* Check if health check failed
*/
isUnhealthy() {
return [HealthStatus.UNHEALTHY, HealthStatus.CRITICAL].includes(this.status);
}
/**
* Convert to JSON
*/
toJSON() {
return {
name: this.name,
status: this.status,
message: this.message,
details: this.details,
duration: this.duration,
timestamp: this.timestamp
};
}
}
/**
* Individual health check implementation
*/
export class HealthCheck {
constructor(name, checkFunction, config = {}) {
this.name = name;
this.checkFunction = checkFunction;
this.config = {
timeout: 5000,
interval: 30000,
retries: 1,
enabled: true,
critical: false,
...config
};
this.lastResult = null;
this.history = [];
this.maxHistorySize = 100;
// Statistics
this.stats = {
totalChecks: 0,
successfulChecks: 0,
failedChecks: 0,
averageDuration: 0,
totalDuration: 0,
lastCheck: null,
consecutiveFailures: 0,
consecutiveSuccesses: 0
};
}
/**
* Execute health check with timeout and retry logic
*/
async execute() {
if (!this.config.enabled) {
return new HealthCheckResult(
this.name,
HealthStatus.UNKNOWN,
'Health check disabled'
);
}
const startTime = Date.now();
let lastError;
for (let attempt = 1; attempt <= this.config.retries + 1; attempt++) {
try {
// Execute with timeout
const result = await Promise.race([
this.checkFunction(),
this.createTimeoutPromise()
]);
const duration = Date.now() - startTime;
const healthResult = this.processResult(result, duration);
this.updateStats(true, duration);
this.addToHistory(healthResult);
return healthResult;
} catch (error) {
lastError = error;
if (attempt <= this.config.retries) {
// Wait before retry
await this.sleep(1000 * attempt);
}
}
}
// All attempts failed
const duration = Date.now() - startTime;
const healthResult = new HealthCheckResult(
this.name,
HealthStatus.UNHEALTHY,
`Health check failed: ${lastError.message}`,
{ error: lastError.message, attempts: this.config.retries + 1 },
duration
);
this.updateStats(false, duration);
this.addToHistory(healthResult);
return healthResult;
}
/**
* Process health check result
*/
processResult(result, duration) {
// If result is already a HealthCheckResult, return it
if (result instanceof HealthCheckResult) {
result.duration = duration;
return result;
}
// If result is boolean
if (typeof result === 'boolean') {
return new HealthCheckResult(
this.name,
result ? HealthStatus.HEALTHY : HealthStatus.UNHEALTHY,
result ? 'Health check passed' : 'Health check failed',
{},
duration
);
}
// If result is object with status
if (result && typeof result === 'object') {
return new HealthCheckResult(
this.name,
result.status || HealthStatus.HEALTHY,
result.message || 'Health check completed',
result.details || {},
duration
);
}
// Default to healthy
return new HealthCheckResult(
this.name,
HealthStatus.HEALTHY,
'Health check passed',
{ result },
duration
);
}
/**
* Create timeout promise
*/
createTimeoutPromise() {
return new Promise((_, reject) => {
setTimeout(() => {
reject(new Error(`Health check timeout after ${this.config.timeout}ms`));
}, this.config.timeout);
});
}
/**
* Update statistics
*/
updateStats(success, duration) {
this.stats.totalChecks++;
this.stats.totalDuration += duration;
this.stats.averageDuration = this.stats.totalDuration / this.stats.totalChecks;
this.stats.lastCheck = new Date().toISOString();
if (success) {
this.stats.successfulChecks++;
this.stats.consecutiveSuccesses++;
this.stats.consecutiveFailures = 0;
} else {
this.stats.failedChecks++;
this.stats.consecutiveFailures++;
this.stats.consecutiveSuccesses = 0;
}
}
/**
* Add result to history
*/
addToHistory(result) {
this.lastResult = result;
this.history.push(result);
// Maintain history size
if (this.history.length > this.maxHistorySize) {
this.history.shift();
}
}
/**
* Get health check statistics
*/
getStats() {
return {
...this.stats,
successRate: this.stats.totalChecks > 0 ?
(this.stats.successfulChecks / this.stats.totalChecks) * 100 : 0,
config: this.config,
lastResult: this.lastResult?.toJSON()
};
}
/**
* Get recent history
*/
getHistory(limit = 10) {
return this.history.slice(-limit).map(result => result.toJSON());
}
/**
* Sleep utility
*/
sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
}
/**
* Main health monitoring system
*/
export class HealthMonitor extends EventEmitter {
constructor(config = {}) {
super();
this.config = {
checkInterval: 30000, // 30 seconds
enableAutoCheck: true,
enableMetrics: true,
maxHistorySize: 1000,
alertThresholds: {
consecutiveFailures: 3,
failureRate: 50, // percentage
responseTime: 5000 // milliseconds
},
...config
};
this.logger = new Logger().child('HealthMonitor');
this.healthChecks = new Map();
this.isRunning = false;
this.checkTimer = null;
// Overall system health
this.systemHealth = {
status: HealthStatus.UNKNOWN,
message: 'System not initialized',
lastCheck: null,
components: {},
metrics: {
totalComponents: 0,
healthyComponents: 0,
degradedComponents: 0,
unhealthyComponents: 0,
criticalComponents: 0
}
};
// Metrics collection
this.metrics = {
totalChecks: 0,
totalDuration: 0,
averageResponseTime: 0,
checksPerMinute: 0,
lastMinuteChecks: [],
alerts: []
};
}
/**
* Register a health check
*/
registerHealthCheck(name, checkFunction, config = {}) {
const healthCheck = new HealthCheck(name, checkFunction, config);
this.healthChecks.set(name, healthCheck);
this.logger.info(`Registered health check: ${name}`, {
timeout: healthCheck.config.timeout,
interval: healthCheck.config.interval,
critical: healthCheck.config.critical
});
return healthCheck;
}
/**
* Unregister a health check
*/
unregisterHealthCheck(name) {
if (this.healthChecks.delete(name)) {
this.logger.info(`Unregistered health check: ${name}`);
return true;
}
return false;
}
/**
* Start health monitoring
*/
start() {
if (this.isRunning) {
this.logger.warn('Health monitor is already running');
return;
}
this.isRunning = true;
this.logger.info('Starting health monitor', {
checkInterval: this.config.checkInterval,
registeredChecks: this.healthChecks.size
});
// Perform initial health check
this.performHealthChecks().catch(error => {
this.logger.error('Initial health check failed:', error);
});
// Start periodic checks if enabled
if (this.config.enableAutoCheck) {
this.startPeriodicChecks();
}
this.emit('started');
}
/**
* Stop health monitoring
*/
stop() {
if (!this.isRunning) {
return;
}
this.isRunning = false;
if (this.checkTimer) {
clearInterval(this.checkTimer);
this.checkTimer = null;
}
this.logger.info('Health monitor stopped');
this.emit('stopped');
}
/**
* Start periodic health checks
*/
startPeriodicChecks() {
this.checkTimer = setInterval(async () => {
try {
await this.performHealthChecks();
} catch (error) {
this.logger.error('Periodic health check failed:', error);
}
}, this.config.checkInterval);
}
/**
* Perform all health checks
*/
async performHealthChecks() {
if (this.healthChecks.size === 0) {
this.updateSystemHealth(HealthStatus.UNKNOWN, 'No health checks registered');
return this.systemHealth;
}
const startTime = Date.now();
const results = new Map();
// Execute all health checks concurrently
const checkPromises = Array.from(this.healthChecks.entries()).map(async ([name, healthCheck]) => {
try {
const result = await healthCheck.execute();
results.set(name, result);
return { name, result };
} catch (error) {
const errorResult = new HealthCheckResult(
name,
HealthStatus.CRITICAL,
`Health check execution failed: ${error.message}`,
{ error: error.message }
);
results.set(name, errorResult);
return { name, result: errorResult };
}
});
await Promise.allSettled(checkPromises);
// Update metrics
const totalDuration = Date.now() - startTime;
this.updateMetrics(results.size, totalDuration);
// Analyze results and update system health
this.analyzeHealthResults(results);
// Check for alerts
this.checkAlerts(results);
// Emit health check completed event
this.emit('healthCheckCompleted', {
systemHealth: this.systemHealth,
results: Array.from(results.values()).map(r => r.toJSON()),
duration: totalDuration
});
return this.systemHealth;
}
/**
* Perform health check for specific component
*/
async performHealthCheck(name) {
const healthCheck = this.healthChecks.get(name);
if (!healthCheck) {
throw new Error(`Health check '${name}' not found`);
}
const result = await healthCheck.execute();
// Update system health if this was a critical component
if (healthCheck.config.critical) {
await this.performHealthChecks();
}
return result;
}
/**
* Analyze health check results and update system health
*/
analyzeHealthResults(results) {
const metrics = {
totalComponents: results.size,
healthyComponents: 0,
degradedComponents: 0,
unhealthyComponents: 0,
criticalComponents: 0
};
const componentResults = {};
let overallStatus = HealthStatus.HEALTHY;
let statusMessage = 'All components healthy';
// Analyze each result
for (const [name, result] of results) {
componentResults[name] = result.toJSON();
switch (result.status) {
case HealthStatus.HEALTHY:
metrics.healthyComponents++;
break;
case HealthStatus.DEGRADED:
metrics.degradedComponents++;
if (overallStatus === HealthStatus.HEALTHY) {
overallStatus = HealthStatus.DEGRADED;
statusMessage = 'Some components degraded';
}
break;
case HealthStatus.UNHEALTHY:
metrics.unhealthyComponents++;
if ([HealthStatus.HEALTHY, HealthStatus.DEGRADED].includes(overallStatus)) {
overallStatus = HealthStatus.UNHEALTHY;
statusMessage = 'Some components unhealthy';
}
break;
case HealthStatus.CRITICAL:
metrics.criticalComponents++;
overallStatus = HealthStatus.CRITICAL;
statusMessage = 'Critical components failing';
break;
}
}
// Check for critical component failures
const criticalFailures = Array.from(this.healthChecks.entries())
.filter(([name, check]) => check.config.critical && results.get(name)?.isUnhealthy())
.map(([name]) => name);
if (criticalFailures.length > 0) {
overallStatus = HealthStatus.CRITICAL;
statusMessage = `Critical components failing: ${criticalFailures.join(', ')}`;
}
this.updateSystemHealth(overallStatus, statusMessage, componentResults, metrics);
}
/**
* Update system health status
*/
updateSystemHealth(status, message, components = {}, metrics = null) {
const previousStatus = this.systemHealth.status;
this.systemHealth = {
status,
message,
lastCheck: new Date().toISOString(),
components,
metrics: metrics || this.systemHealth.metrics
};
// Emit status change event if status changed
if (previousStatus !== status) {
this.logger.info(`System health status changed: ${previousStatus} -> ${status}`, {
message,
components: Object.keys(components).length
});
this.emit('statusChanged', {
previousStatus,
currentStatus: status,
message,
systemHealth: this.systemHealth
});
}
}
/**
* Update metrics
*/
updateMetrics(checkCount, duration) {
if (!this.config.enableMetrics) {
return;
}
this.metrics.totalChecks += checkCount;
this.metrics.totalDuration += duration;
this.metrics.averageResponseTime = this.metrics.totalDuration / this.metrics.totalChecks;
// Track checks per minute
const now = Date.now();
this.metrics.lastMinuteChecks.push(now);
// Remove checks older than 1 minute
this.metrics.lastMinuteChecks = this.metrics.lastMinuteChecks.filter(
timestamp => now - timestamp < 60000
);
this.metrics.checksPerMinute = this.metrics.lastMinuteChecks.length;
}
/**
* Check for alert conditions
*/
checkAlerts(results) {
const alerts = [];
for (const [name, result] of results) {
const healthCheck = this.healthChecks.get(name);
const stats = healthCheck.getStats();
// Check consecutive failures
if (stats.consecutiveFailures >= this.config.alertThresholds.consecutiveFailures) {
alerts.push({
type: 'consecutive_failures',
component: name,
message: `${name} has ${stats.consecutiveFailures} consecutive failures`,
severity: 'high',
timestamp: new Date().toISOString()
});
}
// Check failure rate
if (stats.successRate < (100 - this.config.alertThresholds.failureRate)) {
alerts.push({
type: 'high_failure_rate',
component: name,
message: `${name} has ${stats.successRate.toFixed(1)}% success rate`,
severity: 'medium',
timestamp: new Date().toISOString()
});
}
// Check response time
if (result.duration > this.config.alertThresholds.responseTime) {
alerts.push({
type: 'slow_response',
component: name,
message: `${name} response time ${result.duration}ms exceeds threshold`,
severity: 'low',
timestamp: new Date().toISOString()
});
}
}
// Emit alerts
for (const alert of alerts) {
this.emit('alert', alert);
this.logger.warn(`Health alert: ${alert.message}`, alert);
}
// Store alerts in metrics
this.metrics.alerts.push(...alerts);
// Maintain alert history size
if (this.metrics.alerts.length > this.config.maxHistorySize) {
this.metrics.alerts = this.metrics.alerts.slice(-this.config.maxHistorySize);
}
}
/**
* Get system health status
*/
getSystemHealth() {
return this.systemHealth;
}
/**
* Get health check statistics
*/
getHealthCheckStats(name) {
const healthCheck = this.healthChecks.get(name);
return healthCheck ? healthCheck.getStats() : null;
}
/**
* Get all health check statistics
*/
getAllHealthCheckStats() {
const stats = {};
for (const [name, healthCheck] of this.healthChecks) {
stats[name] = healthCheck.getStats();
}
return stats;
}
/**
* Get monitoring metrics
*/
getMetrics() {
return {
...this.metrics,
systemHealth: this.systemHealth,
registeredChecks: this.healthChecks.size,
isRunning: this.isRunning
};
}
/**
* Get recent alerts
*/
getRecentAlerts(limit = 50) {
return this.metrics.alerts.slice(-limit);
}
/**
* Clear alert history
*/
clearAlerts() {
this.metrics.alerts = [];
this.logger.info('Alert history cleared');
}
/**
* Enable/disable health check
*/
setHealthCheckEnabled(name, enabled) {
const healthCheck = this.healthChecks.get(name);
if (healthCheck) {
healthCheck.config.enabled = enabled;
this.logger.info(`Health check '${name}' ${enabled ? 'enabled' : 'disabled'}`);
return true;
}
return false;
}
/**
* Update health check configuration
*/
updateHealthCheckConfig(name, config) {
const healthCheck = this.healthChecks.get(name);
if (healthCheck) {
healthCheck.config = { ...healthCheck.config, ...config };
this.logger.info(`Health check '${name}' configuration updated`, config);
return true;
}
return false;
}
/**
* Cleanup resources
*/
destroy() {
this.stop();
this.healthChecks.clear();
this.removeAllListeners();
}
}