/** * txoko-metrics — Backend de métricas del nodo Bitcoin * * Servidor HTTP Node.js que escucha en 127.0.0.1:4082. * Sirve métricas del sistema y estado de Bitcoin Core al dashboard. * * CONFIGURACIÓN: * Editar las variables de la sección "CONFIGURACIÓN LOCAL" con * los datos de tu nodo antes de arrancar. * * ARRANCAR: * node system-metrics.js * * O como servicio systemd: * sudo systemctl start txoko-metrics */ // ── CONFIGURACIÓN LOCAL ────────────────────────────────────────────── // Copia este archivo como system-metrics.local.js y edita estos valores. // El archivo .local.js está en .gitignore — nunca se sube al repo. const RPC_HOST = '127.0.0.1'; const RPC_PORT = 8332; const RPC_USER = 'TU_RPC_USER'; // Ver bitcoin.conf → rpcuser const RPC_PASSWORD = 'TU_RPC_PASSWORD'; // Ver bitcoin.conf → rpcpassword // Alternativa: autenticación por cookie (más seguro) // const RPC_COOKIE = '/data/bitcoin/.cookie'; const METRICS_PORT = 4082; const METRICS_HOST = '127.0.0.1'; // Solo localhost — no exponer al exterior // ── FIN CONFIGURACIÓN ──────────────────────────────────────────────── const http = require('http'); const { execSync } = require('child_process'); const fs = require('fs'); const os = require('os'); // Cliente RPC Bitcoin Core async function rpcCall(method, params = []) { const body = JSON.stringify({ jsonrpc: '1.0', id: 'txoko', method, params }); const auth = Buffer.from(`${RPC_USER}:${RPC_PASSWORD}`).toString('base64'); return new Promise((resolve, reject) => { const req = http.request({ host: RPC_HOST, port: RPC_PORT, method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Basic ${auth}` } }, res => { let data = ''; res.on('data', chunk => data += chunk); res.on('end', () => { try { const parsed = JSON.parse(data); if (parsed.error) reject(parsed.error); else resolve(parsed.result); } catch (e) { reject(e); } }); }); req.on('error', reject); req.write(body); req.end(); }); } // Endpoints const routes = { '/system/info': async () => { const cpus = os.cpus(); const totalMem = os.totalmem(); const freeMem = os.freemem(); const usedMem = totalMem - freeMem; // CPU usage (snapshot simple) const cpuUsage = cpus.reduce((sum, cpu) => { const total = Object.values(cpu.times).reduce((a, b) => a + b, 0); const idle = cpu.times.idle; return sum + ((total - idle) / total) * 100; }, 0) / cpus.length; return { cpu: { usage: cpuUsage.toFixed(1), cores: cpus.length, model: cpus[0]?.model }, memory: { total: totalMem, free: freeMem, used: usedMem, usedPercent: ((usedMem / totalMem) * 100).toFixed(1) }, uptime: os.uptime(), platform: os.platform(), hostname: os.hostname() }; }, '/system/bitcoin': async () => { const [info, netinfo, mempoolinfo] = await Promise.all([ rpcCall('getblockchaininfo'), rpcCall('getnetworkinfo'), rpcCall('getmempoolinfo') ]); return { blockchain: info, network: netinfo, mempool: mempoolinfo }; }, '/system/logs/bitcoin': async () => { try { const lines = execSync('journalctl -u bitcoind -n 80 --no-pager -o short-iso 2>/dev/null || tail -80 /data/bitcoin/debug.log 2>/dev/null || echo ""') .toString().trim().split('\n').filter(Boolean); return { logs: lines.map(line => ({ line, level: line.includes('error') || line.includes('ERROR') ? 'error' : line.includes('warn') || line.includes('WARN') ? 'warn' : 'info' }))}; } catch { return { logs: [] }; } }, '/system/logs/fulcrum': async () => { try { const lines = execSync('journalctl -u fulcrum -n 80 --no-pager -o short-iso 2>/dev/null || echo ""') .toString().trim().split('\n').filter(Boolean); return { logs: lines.map(line => ({ line, level: line.includes('Error') || line.includes('error') ? 'error' : line.includes('Warn') || line.includes('warn') ? 'warn' : 'info' }))}; } catch { return { logs: [] }; } }, '/system/health': async () => ({ status: 'ok', ts: Date.now() }) }; // Servidor HTTP const server = http.createServer(async (req, res) => { res.setHeader('Access-Control-Allow-Origin', '*'); res.setHeader('Content-Type', 'application/json'); // Extraer ruta sin query string const path = req.url.split('?')[0]; const handler = routes[path]; if (!handler) { res.writeHead(404); return res.end(JSON.stringify({ error: 'Not found' })); } try { const data = await handler(); res.writeHead(200); res.end(JSON.stringify(data)); } catch (err) { res.writeHead(500); res.end(JSON.stringify({ error: err.message || 'Internal error' })); } }); server.listen(METRICS_PORT, METRICS_HOST, () => { console.log(`txoko-metrics escuchando en ${METRICS_HOST}:${METRICS_PORT}`); });