| | #!/usr/bin/env node |
| |
|
| | |
| | |
| | |
| | |
| |
|
| | const fs = require('fs'); |
| |
|
| | class FailoverManager { |
| | constructor(reportPath = './api-monitor-report.json') { |
| | this.reportPath = reportPath; |
| | this.report = null; |
| | this.failoverChains = {}; |
| | } |
| |
|
| | |
| | loadReport() { |
| | try { |
| | const data = fs.readFileSync(this.reportPath, 'utf8'); |
| | this.report = JSON.parse(data); |
| | return true; |
| | } catch (error) { |
| | console.error('Failed to load report:', error.message); |
| | return false; |
| | } |
| | } |
| |
|
| | |
| | buildFailoverChains() { |
| | console.log('\nββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ'); |
| | console.log('β FAILOVER CHAIN BUILDER β'); |
| | console.log('ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ\n'); |
| |
|
| | const chains = { |
| | ethereumPrice: this.buildPriceChain('ethereum'), |
| | bitcoinPrice: this.buildPriceChain('bitcoin'), |
| | ethereumExplorer: this.buildExplorerChain('ethereum'), |
| | bscExplorer: this.buildExplorerChain('bsc'), |
| | tronExplorer: this.buildExplorerChain('tron'), |
| | rpcEthereum: this.buildRPCChain('ethereum'), |
| | rpcBSC: this.buildRPCChain('bsc'), |
| | newsFeeds: this.buildNewsChain(), |
| | sentiment: this.buildSentimentChain() |
| | }; |
| |
|
| | this.failoverChains = chains; |
| |
|
| | |
| | for (const [chainName, chain] of Object.entries(chains)) { |
| | this.displayChain(chainName, chain); |
| | } |
| |
|
| | return chains; |
| | } |
| |
|
| | |
| | buildPriceChain(coin) { |
| | const chain = []; |
| |
|
| | |
| | const marketResources = this.report?.categories?.marketData || []; |
| |
|
| | |
| | const sorted = marketResources |
| | .filter(r => ['ONLINE', 'DEGRADED'].includes(r.status)) |
| | .sort((a, b) => { |
| | |
| | if (a.tier !== b.tier) return a.tier - b.tier; |
| |
|
| | |
| | const statusPriority = { ONLINE: 1, DEGRADED: 2, SLOW: 3 }; |
| | return statusPriority[a.status] - statusPriority[b.status]; |
| | }); |
| |
|
| | for (const resource of sorted) { |
| | chain.push({ |
| | name: resource.name, |
| | url: resource.url, |
| | status: resource.status, |
| | tier: resource.tier, |
| | responseTime: resource.lastCheck?.responseTime |
| | }); |
| | } |
| |
|
| | return chain; |
| | } |
| |
|
| | |
| | buildExplorerChain(blockchain) { |
| | const chain = []; |
| | const explorerResources = this.report?.categories?.blockchainExplorers || []; |
| |
|
| | const filtered = explorerResources |
| | .filter(r => { |
| | const name = r.name.toLowerCase(); |
| | return (blockchain === 'ethereum' && name.includes('eth')) || |
| | (blockchain === 'bsc' && name.includes('bsc')) || |
| | (blockchain === 'tron' && name.includes('tron')); |
| | }) |
| | .filter(r => ['ONLINE', 'DEGRADED'].includes(r.status)) |
| | .sort((a, b) => a.tier - b.tier); |
| |
|
| | for (const resource of filtered) { |
| | chain.push({ |
| | name: resource.name, |
| | url: resource.url, |
| | status: resource.status, |
| | tier: resource.tier, |
| | responseTime: resource.lastCheck?.responseTime |
| | }); |
| | } |
| |
|
| | return chain; |
| | } |
| |
|
| | |
| | buildRPCChain(network) { |
| | const chain = []; |
| | const rpcResources = this.report?.categories?.rpcNodes || []; |
| |
|
| | const filtered = rpcResources |
| | .filter(r => { |
| | const name = r.name.toLowerCase(); |
| | return name.includes(network.toLowerCase()); |
| | }) |
| | .filter(r => ['ONLINE', 'DEGRADED'].includes(r.status)) |
| | .sort((a, b) => { |
| | if (a.tier !== b.tier) return a.tier - b.tier; |
| | return (a.lastCheck?.responseTime || 999999) - (b.lastCheck?.responseTime || 999999); |
| | }); |
| |
|
| | for (const resource of filtered) { |
| | chain.push({ |
| | name: resource.name, |
| | url: resource.url, |
| | status: resource.status, |
| | tier: resource.tier, |
| | responseTime: resource.lastCheck?.responseTime |
| | }); |
| | } |
| |
|
| | return chain; |
| | } |
| |
|
| | |
| | buildNewsChain() { |
| | const chain = []; |
| | const newsResources = this.report?.categories?.newsAndSentiment || []; |
| |
|
| | const filtered = newsResources |
| | .filter(r => ['ONLINE', 'DEGRADED'].includes(r.status)) |
| | .sort((a, b) => a.tier - b.tier); |
| |
|
| | for (const resource of filtered) { |
| | chain.push({ |
| | name: resource.name, |
| | url: resource.url, |
| | status: resource.status, |
| | tier: resource.tier, |
| | responseTime: resource.lastCheck?.responseTime |
| | }); |
| | } |
| |
|
| | return chain; |
| | } |
| |
|
| | |
| | buildSentimentChain() { |
| | const chain = []; |
| | const newsResources = this.report?.categories?.newsAndSentiment || []; |
| |
|
| | const filtered = newsResources |
| | .filter(r => r.name.toLowerCase().includes('fear') || |
| | r.name.toLowerCase().includes('greed') || |
| | r.name.toLowerCase().includes('sentiment')) |
| | .filter(r => ['ONLINE', 'DEGRADED'].includes(r.status)); |
| |
|
| | for (const resource of filtered) { |
| | chain.push({ |
| | name: resource.name, |
| | url: resource.url, |
| | status: resource.status, |
| | tier: resource.tier, |
| | responseTime: resource.lastCheck?.responseTime |
| | }); |
| | } |
| |
|
| | return chain; |
| | } |
| |
|
| | |
| | displayChain(chainName, chain) { |
| | console.log(`\nπ ${chainName.toUpperCase()} Failover Chain:`); |
| | console.log('β'.repeat(60)); |
| |
|
| | if (chain.length === 0) { |
| | console.log(' β οΈ No available resources'); |
| | return; |
| | } |
| |
|
| | chain.forEach((resource, index) => { |
| | const arrow = index === 0 ? 'π―' : ' β'; |
| | const priority = index === 0 ? '[PRIMARY]' : index === 1 ? '[BACKUP]' : `[BACKUP-${index}]`; |
| | const tierBadge = `[TIER-${resource.tier}]`; |
| | const rt = resource.responseTime ? `${resource.responseTime}ms` : 'N/A'; |
| |
|
| | console.log(` ${arrow} ${priority.padEnd(12)} ${resource.name.padEnd(25)} ${resource.status.padEnd(10)} ${rt.padStart(8)} ${tierBadge}`); |
| | }); |
| | } |
| |
|
| | |
| | exportFailoverConfig(filename = 'failover-config.json') { |
| | const config = { |
| | generatedAt: new Date().toISOString(), |
| | chains: this.failoverChains, |
| | usage: { |
| | description: 'Automatic failover configuration for API resources', |
| | example: ` |
| | // Example usage in your application: |
| | const failoverConfig = require('./failover-config.json'); |
| | |
| | async function fetchWithFailover(chainName, fetchFunction) { |
| | const chain = failoverConfig.chains[chainName]; |
| | |
| | for (const resource of chain) { |
| | try { |
| | const result = await fetchFunction(resource.url); |
| | return result; |
| | } catch (error) { |
| | console.log(\`Failed \${resource.name}, trying next...\`); |
| | continue; |
| | } |
| | } |
| | |
| | throw new Error('All resources in chain failed'); |
| | } |
| | |
| | // Use it: |
| | const data = await fetchWithFailover('ethereumPrice', async (url) => { |
| | const response = await fetch(url + '/api/v3/simple/price?ids=ethereum&vs_currencies=usd'); |
| | return response.json(); |
| | }); |
| | ` |
| | } |
| | }; |
| |
|
| | fs.writeFileSync(filename, JSON.stringify(config, null, 2)); |
| | console.log(`\nβ Failover configuration exported to ${filename}`); |
| | } |
| |
|
| | |
| | identifySinglePointsOfFailure() { |
| | console.log('\nββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ'); |
| | console.log('β SINGLE POINT OF FAILURE ANALYSIS β'); |
| | console.log('ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ\n'); |
| |
|
| | const spofs = []; |
| |
|
| | for (const [chainName, chain] of Object.entries(this.failoverChains)) { |
| | const onlineCount = chain.filter(r => r.status === 'ONLINE').length; |
| |
|
| | if (onlineCount === 0) { |
| | spofs.push({ |
| | chain: chainName, |
| | severity: 'CRITICAL', |
| | message: 'Zero available resources' |
| | }); |
| | } else if (onlineCount === 1) { |
| | spofs.push({ |
| | chain: chainName, |
| | severity: 'HIGH', |
| | message: 'Only one resource available (SPOF)' |
| | }); |
| | } else if (onlineCount === 2) { |
| | spofs.push({ |
| | chain: chainName, |
| | severity: 'MEDIUM', |
| | message: 'Only two resources available' |
| | }); |
| | } |
| | } |
| |
|
| | if (spofs.length === 0) { |
| | console.log(' β No single points of failure detected\n'); |
| | } else { |
| | for (const spof of spofs) { |
| | const icon = spof.severity === 'CRITICAL' ? 'π΄' : |
| | spof.severity === 'HIGH' ? 'π ' : 'π‘'; |
| | console.log(` ${icon} [${spof.severity}] ${spof.chain}: ${spof.message}`); |
| | } |
| | console.log(); |
| | } |
| |
|
| | return spofs; |
| | } |
| |
|
| | |
| | generateRedundancyReport() { |
| | console.log('\nββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ'); |
| | console.log('β REDUNDANCY ANALYSIS REPORT β'); |
| | console.log('ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ\n'); |
| |
|
| | const categories = this.report?.categories || {}; |
| |
|
| | for (const [category, resources] of Object.entries(categories)) { |
| | const total = resources.length; |
| | const online = resources.filter(r => r.status === 'ONLINE').length; |
| | const degraded = resources.filter(r => r.status === 'DEGRADED').length; |
| | const offline = resources.filter(r => r.status === 'OFFLINE').length; |
| |
|
| | let indicator = 'β'; |
| | if (online === 0) indicator = 'β'; |
| | else if (online === 1) indicator = 'β '; |
| | else if (online >= 3) indicator = 'ββ'; |
| |
|
| | console.log(` ${indicator} ${category.padEnd(25)} Online: ${online}/${total} Degraded: ${degraded} Offline: ${offline}`); |
| | } |
| | } |
| | } |
| |
|
| | |
| | |
| | |
| |
|
| | async function main() { |
| | const manager = new FailoverManager(); |
| |
|
| | if (!manager.loadReport()) { |
| | console.error('\nβ Please run the monitor first: node api-monitor.js'); |
| | process.exit(1); |
| | } |
| |
|
| | |
| | manager.buildFailoverChains(); |
| |
|
| | |
| | manager.exportFailoverConfig(); |
| |
|
| | |
| | manager.identifySinglePointsOfFailure(); |
| |
|
| | |
| | manager.generateRedundancyReport(); |
| |
|
| | console.log('\nβ Failover analysis complete\n'); |
| | } |
| |
|
| | if (require.main === module) { |
| | main().catch(console.error); |
| | } |
| |
|
| | module.exports = FailoverManager; |
| |
|