|
|
#!/usr/bin/env node |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const fs = require('fs'); |
|
|
const https = require('https'); |
|
|
const http = require('http'); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const CONFIG = { |
|
|
REGISTRY_FILE: './all_apis_merged_2025.json', |
|
|
CHECK_INTERVAL: 5 * 60 * 1000, |
|
|
TIMEOUT: 10000, |
|
|
MAX_RETRIES: 3, |
|
|
RETRY_DELAY: 2000, |
|
|
|
|
|
|
|
|
THRESHOLDS: { |
|
|
ONLINE: { responseTime: 2000, successRate: 0.95 }, |
|
|
DEGRADED: { responseTime: 5000, successRate: 0.80 }, |
|
|
SLOW: { responseTime: 10000, successRate: 0.70 }, |
|
|
UNSTABLE: { responseTime: Infinity, successRate: 0.50 } |
|
|
} |
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const API_REGISTRY = { |
|
|
blockchainExplorers: { |
|
|
etherscan: [ |
|
|
{ name: 'Etherscan-1', url: 'https://api.etherscan.io/api', keyName: 'etherscan', keyIndex: 0, testEndpoint: '?module=stats&action=ethprice&apikey={{KEY}}', tier: 1 }, |
|
|
{ name: 'Etherscan-2', url: 'https://api.etherscan.io/api', keyName: 'etherscan', keyIndex: 1, testEndpoint: '?module=stats&action=ethprice&apikey={{KEY}}', tier: 1 } |
|
|
], |
|
|
bscscan: [ |
|
|
{ name: 'BscScan', url: 'https://api.bscscan.com/api', keyName: 'bscscan', keyIndex: 0, testEndpoint: '?module=stats&action=bnbprice&apikey={{KEY}}', tier: 1 } |
|
|
], |
|
|
tronscan: [ |
|
|
{ name: 'TronScan', url: 'https://apilist.tronscanapi.com/api', keyName: 'tronscan', keyIndex: 0, testEndpoint: '/system/status', tier: 2 } |
|
|
] |
|
|
}, |
|
|
|
|
|
marketData: { |
|
|
coingecko: [ |
|
|
{ name: 'CoinGecko', url: 'https://api.coingecko.com/api/v3', testEndpoint: '/ping', requiresKey: false, tier: 1 }, |
|
|
{ name: 'CoinGecko-Price', url: 'https://api.coingecko.com/api/v3', testEndpoint: '/simple/price?ids=bitcoin&vs_currencies=usd', requiresKey: false, tier: 1 } |
|
|
], |
|
|
coinmarketcap: [ |
|
|
{ name: 'CoinMarketCap-1', url: 'https://pro-api.coinmarketcap.com/v1', keyName: 'coinmarketcap', keyIndex: 0, testEndpoint: '/key/info', headerKey: 'X-CMC_PRO_API_KEY', tier: 1 }, |
|
|
{ name: 'CoinMarketCap-2', url: 'https://pro-api.coinmarketcap.com/v1', keyName: 'coinmarketcap', keyIndex: 1, testEndpoint: '/key/info', headerKey: 'X-CMC_PRO_API_KEY', tier: 1 } |
|
|
], |
|
|
cryptocompare: [ |
|
|
{ name: 'CryptoCompare', url: 'https://min-api.cryptocompare.com/data', keyName: 'cryptocompare', keyIndex: 0, testEndpoint: '/price?fsym=BTC&tsyms=USD&api_key={{KEY}}', tier: 2 } |
|
|
], |
|
|
coinpaprika: [ |
|
|
{ name: 'CoinPaprika', url: 'https://api.coinpaprika.com/v1', testEndpoint: '/ping', requiresKey: false, tier: 2 } |
|
|
], |
|
|
coincap: [ |
|
|
{ name: 'CoinCap', url: 'https://api.coincap.io/v2', testEndpoint: '/assets/bitcoin', requiresKey: false, tier: 2 } |
|
|
] |
|
|
}, |
|
|
|
|
|
newsAndSentiment: { |
|
|
cryptopanic: [ |
|
|
{ name: 'CryptoPanic', url: 'https://cryptopanic.com/api/v1', testEndpoint: '/posts/?public=true', requiresKey: false, tier: 2 } |
|
|
], |
|
|
newsapi: [ |
|
|
{ name: 'NewsAPI', url: 'https://newsapi.org/v2', keyName: 'newsapi', keyIndex: 0, testEndpoint: '/top-headlines?category=business&apiKey={{KEY}}', tier: 2 } |
|
|
], |
|
|
alternativeme: [ |
|
|
{ name: 'Fear-Greed-Index', url: 'https://api.alternative.me', testEndpoint: '/fng/?limit=1', requiresKey: false, tier: 2 } |
|
|
], |
|
|
reddit: [ |
|
|
{ name: 'Reddit-Crypto', url: 'https://www.reddit.com/r/cryptocurrency', testEndpoint: '/hot.json?limit=1', requiresKey: false, tier: 3 } |
|
|
] |
|
|
}, |
|
|
|
|
|
rpcNodes: { |
|
|
ethereum: [ |
|
|
{ name: 'Ankr-ETH', url: 'https://rpc.ankr.com/eth', testEndpoint: '', method: 'POST', rpcTest: true, requiresKey: false, tier: 1 }, |
|
|
{ name: 'PublicNode-ETH', url: 'https://ethereum.publicnode.com', testEndpoint: '', method: 'POST', rpcTest: true, requiresKey: false, tier: 2 }, |
|
|
{ name: 'Cloudflare-ETH', url: 'https://cloudflare-eth.com', testEndpoint: '', method: 'POST', rpcTest: true, requiresKey: false, tier: 2 }, |
|
|
{ name: 'LlamaNodes-ETH', url: 'https://eth.llamarpc.com', testEndpoint: '', method: 'POST', rpcTest: true, requiresKey: false, tier: 3 } |
|
|
], |
|
|
bsc: [ |
|
|
{ name: 'BSC-Official', url: 'https://bsc-dataseed.binance.org', testEndpoint: '', method: 'POST', rpcTest: true, requiresKey: false, tier: 2 }, |
|
|
{ name: 'Ankr-BSC', url: 'https://rpc.ankr.com/bsc', testEndpoint: '', method: 'POST', rpcTest: true, requiresKey: false, tier: 2 }, |
|
|
{ name: 'PublicNode-BSC', url: 'https://bsc-rpc.publicnode.com', testEndpoint: '', method: 'POST', rpcTest: true, requiresKey: false, tier: 3 } |
|
|
], |
|
|
polygon: [ |
|
|
{ name: 'Polygon-Official', url: 'https://polygon-rpc.com', testEndpoint: '', method: 'POST', rpcTest: true, requiresKey: false, tier: 2 }, |
|
|
{ name: 'Ankr-Polygon', url: 'https://rpc.ankr.com/polygon', testEndpoint: '', method: 'POST', rpcTest: true, requiresKey: false, tier: 2 } |
|
|
], |
|
|
tron: [ |
|
|
{ name: 'TronGrid', url: 'https://api.trongrid.io', testEndpoint: '/wallet/getnowblock', method: 'POST', requiresKey: false, tier: 2 }, |
|
|
{ name: 'TronStack', url: 'https://api.tronstack.io', testEndpoint: '/wallet/getnowblock', method: 'POST', requiresKey: false, tier: 3 } |
|
|
] |
|
|
}, |
|
|
|
|
|
onChainAnalytics: [ |
|
|
{ name: 'TheGraph', url: 'https://api.thegraph.com', testEndpoint: '/index-node/graphql', requiresKey: false, tier: 2 }, |
|
|
{ name: 'Blockchair', url: 'https://api.blockchair.com', testEndpoint: '/stats', requiresKey: false, tier: 3 } |
|
|
], |
|
|
|
|
|
whaleTracking: [ |
|
|
{ name: 'WhaleAlert-Status', url: 'https://api.whale-alert.io/v1', testEndpoint: '/status', requiresKey: false, tier: 1 } |
|
|
], |
|
|
|
|
|
corsProxies: [ |
|
|
{ name: 'AllOrigins', url: 'https://api.allorigins.win', testEndpoint: '/get?url=https://api.coingecko.com/api/v3/ping', requiresKey: false, tier: 3 }, |
|
|
{ name: 'CORS.SH', url: 'https://proxy.cors.sh', testEndpoint: '/https://api.coingecko.com/api/v3/ping', requiresKey: false, tier: 3 }, |
|
|
{ name: 'Corsfix', url: 'https://proxy.corsfix.com', testEndpoint: '/?url=https://api.coingecko.com/api/v3/ping', requiresKey: false, tier: 3 }, |
|
|
{ name: 'ThingProxy', url: 'https://thingproxy.freeboard.io', testEndpoint: '/fetch/https://api.coingecko.com/api/v3/ping', requiresKey: false, tier: 3 } |
|
|
] |
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class CryptoAPIMonitor { |
|
|
constructor() { |
|
|
this.apiKeys = {}; |
|
|
this.resourceStatus = {}; |
|
|
this.metrics = { |
|
|
totalChecks: 0, |
|
|
successfulChecks: 0, |
|
|
failedChecks: 0, |
|
|
totalResponseTime: 0 |
|
|
}; |
|
|
this.history = {}; |
|
|
this.alerts = []; |
|
|
} |
|
|
|
|
|
|
|
|
loadRegistry() { |
|
|
try { |
|
|
const data = fs.readFileSync(CONFIG.REGISTRY_FILE, 'utf8'); |
|
|
const registry = JSON.parse(data); |
|
|
|
|
|
this.apiKeys = registry.discovered_keys || {}; |
|
|
console.log('β Registry loaded successfully'); |
|
|
console.log(` Found ${Object.keys(this.apiKeys).length} API key categories`); |
|
|
|
|
|
return true; |
|
|
} catch (error) { |
|
|
console.error('β Failed to load registry:', error.message); |
|
|
return false; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
getApiKey(keyName, keyIndex = 0) { |
|
|
if (!keyName || !this.apiKeys[keyName]) return null; |
|
|
const keys = this.apiKeys[keyName]; |
|
|
return Array.isArray(keys) ? keys[keyIndex] : keys; |
|
|
} |
|
|
|
|
|
|
|
|
maskKey(key) { |
|
|
if (!key || key.length < 8) return '****'; |
|
|
return key.substring(0, 4) + '****' + key.substring(key.length - 4); |
|
|
} |
|
|
|
|
|
|
|
|
makeRequest(url, options = {}) { |
|
|
return new Promise((resolve, reject) => { |
|
|
const startTime = Date.now(); |
|
|
const protocol = url.startsWith('https') ? https : http; |
|
|
|
|
|
const req = protocol.request(url, { |
|
|
method: options.method || 'GET', |
|
|
headers: options.headers || {}, |
|
|
timeout: CONFIG.TIMEOUT |
|
|
}, (res) => { |
|
|
let data = ''; |
|
|
|
|
|
res.on('data', chunk => data += chunk); |
|
|
res.on('end', () => { |
|
|
const responseTime = Date.now() - startTime; |
|
|
resolve({ |
|
|
statusCode: res.statusCode, |
|
|
data: data, |
|
|
responseTime: responseTime, |
|
|
success: res.statusCode >= 200 && res.statusCode < 300 |
|
|
}); |
|
|
}); |
|
|
}); |
|
|
|
|
|
req.on('error', (error) => { |
|
|
reject({ |
|
|
error: error.message, |
|
|
responseTime: Date.now() - startTime, |
|
|
success: false |
|
|
}); |
|
|
}); |
|
|
|
|
|
req.on('timeout', () => { |
|
|
req.destroy(); |
|
|
reject({ |
|
|
error: 'Request timeout', |
|
|
responseTime: CONFIG.TIMEOUT, |
|
|
success: false |
|
|
}); |
|
|
}); |
|
|
|
|
|
if (options.body) { |
|
|
req.write(options.body); |
|
|
} |
|
|
|
|
|
req.end(); |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
async checkEndpoint(resource) { |
|
|
const startTime = Date.now(); |
|
|
|
|
|
try { |
|
|
|
|
|
let url = resource.url + (resource.testEndpoint || ''); |
|
|
|
|
|
|
|
|
if (resource.keyName) { |
|
|
const apiKey = this.getApiKey(resource.keyName, resource.keyIndex || 0); |
|
|
if (apiKey) { |
|
|
url = url.replace('{{KEY}}', apiKey); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
const headers = { |
|
|
'User-Agent': 'CryptoAPIMonitor/1.0' |
|
|
}; |
|
|
|
|
|
|
|
|
if (resource.headerKey && resource.keyName) { |
|
|
const apiKey = this.getApiKey(resource.keyName, resource.keyIndex || 0); |
|
|
if (apiKey) { |
|
|
headers[resource.headerKey] = apiKey; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
let options = { method: resource.method || 'GET', headers }; |
|
|
|
|
|
if (resource.rpcTest) { |
|
|
options.method = 'POST'; |
|
|
options.headers['Content-Type'] = 'application/json'; |
|
|
options.body = JSON.stringify({ |
|
|
jsonrpc: '2.0', |
|
|
method: 'eth_blockNumber', |
|
|
params: [], |
|
|
id: 1 |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
const result = await this.makeRequest(url, options); |
|
|
|
|
|
return { |
|
|
name: resource.name, |
|
|
url: resource.url, |
|
|
success: result.success, |
|
|
statusCode: result.statusCode, |
|
|
responseTime: result.responseTime, |
|
|
timestamp: new Date().toISOString(), |
|
|
tier: resource.tier || 4 |
|
|
}; |
|
|
|
|
|
} catch (error) { |
|
|
return { |
|
|
name: resource.name, |
|
|
url: resource.url, |
|
|
success: false, |
|
|
error: error.error || error.message, |
|
|
responseTime: error.responseTime || Date.now() - startTime, |
|
|
timestamp: new Date().toISOString(), |
|
|
tier: resource.tier || 4 |
|
|
}; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
classifyStatus(resource) { |
|
|
if (!this.history[resource.name]) { |
|
|
return 'UNKNOWN'; |
|
|
} |
|
|
|
|
|
const hist = this.history[resource.name]; |
|
|
const recentChecks = hist.slice(-10); |
|
|
|
|
|
if (recentChecks.length === 0) return 'UNKNOWN'; |
|
|
|
|
|
const successCount = recentChecks.filter(c => c.success).length; |
|
|
const successRate = successCount / recentChecks.length; |
|
|
const avgResponseTime = recentChecks |
|
|
.filter(c => c.success) |
|
|
.reduce((sum, c) => sum + c.responseTime, 0) / (successCount || 1); |
|
|
|
|
|
if (successRate >= CONFIG.THRESHOLDS.ONLINE.successRate && |
|
|
avgResponseTime < CONFIG.THRESHOLDS.ONLINE.responseTime) { |
|
|
return 'ONLINE'; |
|
|
} else if (successRate >= CONFIG.THRESHOLDS.DEGRADED.successRate && |
|
|
avgResponseTime < CONFIG.THRESHOLDS.DEGRADED.responseTime) { |
|
|
return 'DEGRADED'; |
|
|
} else if (successRate >= CONFIG.THRESHOLDS.SLOW.successRate && |
|
|
avgResponseTime < CONFIG.THRESHOLDS.SLOW.responseTime) { |
|
|
return 'SLOW'; |
|
|
} else if (successRate >= CONFIG.THRESHOLDS.UNSTABLE.successRate) { |
|
|
return 'UNSTABLE'; |
|
|
} else { |
|
|
return 'OFFLINE'; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
updateHistory(resource, result) { |
|
|
if (!this.history[resource.name]) { |
|
|
this.history[resource.name] = []; |
|
|
} |
|
|
|
|
|
this.history[resource.name].push(result); |
|
|
|
|
|
|
|
|
if (this.history[resource.name].length > 100) { |
|
|
this.history[resource.name] = this.history[resource.name].slice(-100); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
async checkCategory(categoryName, resources) { |
|
|
console.log(`\n Checking ${categoryName}...`); |
|
|
|
|
|
const results = []; |
|
|
|
|
|
if (Array.isArray(resources)) { |
|
|
for (const resource of resources) { |
|
|
const result = await this.checkEndpoint(resource); |
|
|
this.updateHistory(resource, result); |
|
|
results.push(result); |
|
|
|
|
|
|
|
|
await new Promise(resolve => setTimeout(resolve, 200)); |
|
|
} |
|
|
} else { |
|
|
|
|
|
for (const [subCategory, subResources] of Object.entries(resources)) { |
|
|
for (const resource of subResources) { |
|
|
const result = await this.checkEndpoint(resource); |
|
|
this.updateHistory(resource, result); |
|
|
results.push(result); |
|
|
|
|
|
await new Promise(resolve => setTimeout(resolve, 200)); |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
return results; |
|
|
} |
|
|
|
|
|
|
|
|
async runMonitoringCycle() { |
|
|
console.log('\nββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ'); |
|
|
console.log('β CRYPTOCURRENCY API RESOURCE MONITOR - Health Check β'); |
|
|
console.log('ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ'); |
|
|
console.log(` Timestamp: ${new Date().toISOString()}`); |
|
|
|
|
|
const cycleResults = {}; |
|
|
|
|
|
for (const [category, resources] of Object.entries(API_REGISTRY)) { |
|
|
const results = await this.checkCategory(category, resources); |
|
|
cycleResults[category] = results; |
|
|
} |
|
|
|
|
|
this.generateReport(cycleResults); |
|
|
this.checkAlertConditions(cycleResults); |
|
|
|
|
|
return cycleResults; |
|
|
} |
|
|
|
|
|
|
|
|
generateReport(cycleResults) { |
|
|
console.log('\nββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ'); |
|
|
console.log('β RESOURCE STATUS REPORT β'); |
|
|
console.log('ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ\n'); |
|
|
|
|
|
let totalResources = 0; |
|
|
let onlineCount = 0; |
|
|
let degradedCount = 0; |
|
|
let offlineCount = 0; |
|
|
|
|
|
for (const [category, results] of Object.entries(cycleResults)) { |
|
|
console.log(`\nπ ${category.toUpperCase()}`); |
|
|
console.log('β'.repeat(60)); |
|
|
|
|
|
for (const result of results) { |
|
|
totalResources++; |
|
|
const status = this.classifyStatus(result); |
|
|
|
|
|
let statusSymbol = 'β'; |
|
|
let statusColor = ''; |
|
|
|
|
|
switch (status) { |
|
|
case 'ONLINE': |
|
|
statusSymbol = 'β'; |
|
|
onlineCount++; |
|
|
break; |
|
|
case 'DEGRADED': |
|
|
case 'SLOW': |
|
|
statusSymbol = 'β'; |
|
|
degradedCount++; |
|
|
break; |
|
|
case 'OFFLINE': |
|
|
case 'UNSTABLE': |
|
|
statusSymbol = 'β'; |
|
|
offlineCount++; |
|
|
break; |
|
|
} |
|
|
|
|
|
const rt = result.responseTime ? `${result.responseTime}ms` : 'N/A'; |
|
|
const tierBadge = result.tier === 1 ? '[TIER-1]' : result.tier === 2 ? '[TIER-2]' : ''; |
|
|
|
|
|
console.log(` ${statusSymbol} ${result.name.padEnd(25)} ${status.padEnd(10)} ${rt.padStart(8)} ${tierBadge}`); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
console.log('\nββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ'); |
|
|
console.log('β SUMMARY β'); |
|
|
console.log('ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ'); |
|
|
console.log(` Total Resources: ${totalResources}`); |
|
|
console.log(` Online: ${onlineCount} (${((onlineCount/totalResources)*100).toFixed(1)}%)`); |
|
|
console.log(` Degraded: ${degradedCount} (${((degradedCount/totalResources)*100).toFixed(1)}%)`); |
|
|
console.log(` Offline: ${offlineCount} (${((offlineCount/totalResources)*100).toFixed(1)}%)`); |
|
|
console.log(` Overall Health: ${((onlineCount/totalResources)*100).toFixed(1)}%`); |
|
|
} |
|
|
|
|
|
|
|
|
checkAlertConditions(cycleResults) { |
|
|
const newAlerts = []; |
|
|
|
|
|
|
|
|
for (const [category, results] of Object.entries(cycleResults)) { |
|
|
for (const result of results) { |
|
|
if (result.tier === 1 && !result.success) { |
|
|
newAlerts.push({ |
|
|
severity: 'CRITICAL', |
|
|
message: `TIER-1 API offline: ${result.name}`, |
|
|
timestamp: new Date().toISOString() |
|
|
}); |
|
|
} |
|
|
|
|
|
if (result.responseTime > 5000) { |
|
|
newAlerts.push({ |
|
|
severity: 'WARNING', |
|
|
message: `Elevated response time: ${result.name} (${result.responseTime}ms)`, |
|
|
timestamp: new Date().toISOString() |
|
|
}); |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
if (newAlerts.length > 0) { |
|
|
console.log('\nββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ'); |
|
|
console.log('β β οΈ ALERTS β'); |
|
|
console.log('ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ'); |
|
|
|
|
|
for (const alert of newAlerts) { |
|
|
console.log(` [${alert.severity}] ${alert.message}`); |
|
|
} |
|
|
|
|
|
this.alerts.push(...newAlerts); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
exportReport(filename = 'api-monitor-report.json') { |
|
|
const report = { |
|
|
timestamp: new Date().toISOString(), |
|
|
summary: { |
|
|
totalResources: 0, |
|
|
onlineResources: 0, |
|
|
degradedResources: 0, |
|
|
offlineResources: 0 |
|
|
}, |
|
|
categories: {}, |
|
|
alerts: this.alerts.slice(-50), |
|
|
history: this.history |
|
|
}; |
|
|
|
|
|
|
|
|
for (const [category, resources] of Object.entries(API_REGISTRY)) { |
|
|
report.categories[category] = []; |
|
|
|
|
|
const flatResources = this.flattenResources(resources); |
|
|
|
|
|
for (const resource of flatResources) { |
|
|
const status = this.classifyStatus(resource); |
|
|
const lastCheck = this.history[resource.name] ? |
|
|
this.history[resource.name].slice(-1)[0] : null; |
|
|
|
|
|
report.summary.totalResources++; |
|
|
|
|
|
if (status === 'ONLINE') report.summary.onlineResources++; |
|
|
else if (status === 'DEGRADED' || status === 'SLOW') report.summary.degradedResources++; |
|
|
else if (status === 'OFFLINE' || status === 'UNSTABLE') report.summary.offlineResources++; |
|
|
|
|
|
report.categories[category].push({ |
|
|
name: resource.name, |
|
|
url: resource.url, |
|
|
status: status, |
|
|
tier: resource.tier, |
|
|
lastCheck: lastCheck |
|
|
}); |
|
|
} |
|
|
} |
|
|
|
|
|
fs.writeFileSync(filename, JSON.stringify(report, null, 2)); |
|
|
console.log(`\nβ Report exported to ${filename}`); |
|
|
|
|
|
return report; |
|
|
} |
|
|
|
|
|
|
|
|
flattenResources(resources) { |
|
|
if (Array.isArray(resources)) { |
|
|
return resources; |
|
|
} |
|
|
|
|
|
const flattened = []; |
|
|
for (const subResources of Object.values(resources)) { |
|
|
flattened.push(...subResources); |
|
|
} |
|
|
return flattened; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async function main() { |
|
|
const monitor = new CryptoAPIMonitor(); |
|
|
|
|
|
|
|
|
if (!monitor.loadRegistry()) { |
|
|
console.error('Failed to initialize monitor'); |
|
|
process.exit(1); |
|
|
} |
|
|
|
|
|
|
|
|
console.log('\nπ Starting initial health check...'); |
|
|
await monitor.runMonitoringCycle(); |
|
|
|
|
|
|
|
|
monitor.exportReport(); |
|
|
|
|
|
|
|
|
if (process.argv.includes('--continuous')) { |
|
|
console.log(`\nβΎοΈ Continuous monitoring enabled (interval: ${CONFIG.CHECK_INTERVAL/1000}s)`); |
|
|
|
|
|
setInterval(async () => { |
|
|
await monitor.runMonitoringCycle(); |
|
|
monitor.exportReport(); |
|
|
}, CONFIG.CHECK_INTERVAL); |
|
|
} else { |
|
|
console.log('\nβ Monitoring cycle complete'); |
|
|
console.log(' Use --continuous flag for continuous monitoring'); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
if (require.main === module) { |
|
|
main().catch(console.error); |
|
|
} |
|
|
|
|
|
module.exports = CryptoAPIMonitor; |
|
|
|