/** * Server Metrics Collector Service * Recolhe métricas de todos os servidores (Hetzner API + SSH) * @author Descomplicar® | @link descomplicar.pt | @copyright 2026 */ import db from '../db.js' import { collectAllMetrics as collectHetznerMetrics } from './hetzner.js' // Hetzner API Configuration const HETZNER_API_URL = 'https://api.hetzner.cloud/v1' const HETZNER_TOKEN = process.env.HETZNER_TOKEN || '' // SSH Configuration (from MCP ssh-unified) interface SSHServer { name: string monitorName: string host: string user: string pass: string } const SSH_SERVERS: SSHServer[] = [ { name: 'server', monitorName: 'CWP Server', host: process.env.SERVER_HOST || '176.9.3.158', user: process.env.SERVER_USER || 'root', pass: process.env.SERVER_PASS || '' }, { name: 'easy', monitorName: 'EasyPanel', host: process.env.EASY_HOST || '178.63.18.51', user: process.env.EASY_USER || 'root', pass: process.env.EASY_PASS || '' } ] // Hetzner server mapping to monitoring table const HETZNER_MAPPING: Record = { 'gateway': 'MCP Hub', 'meet': 'Meet', 'whatsapp.descomplicar.pt': 'WhatsApp', 'whatsms': 'WhatSMS' } interface ServerMetrics { cpu: number ram: number disk: number load: number containers?: number } /** * Parse SSH metrics output */ function parseSSHMetrics(output: string): ServerMetrics { const lines = output.split('\n') const metrics: ServerMetrics = { cpu: 0, ram: 0, disk: 0, load: 0 } for (const line of lines) { if (line.startsWith('CPU:')) metrics.cpu = parseFloat(line.split(':')[1]) || 0 if (line.startsWith('MEM:')) metrics.ram = parseFloat(line.split(':')[1]) || 0 if (line.startsWith('DISK:')) metrics.disk = parseFloat(line.split(':')[1]) || 0 if (line.startsWith('LOAD:')) metrics.load = parseFloat(line.split(':')[1]) || 0 if (line.startsWith('CONTAINERS:')) metrics.containers = parseInt(line.split(':')[1]) || 0 } return metrics } /** * Execute SSH command via subprocess (since we can't use MCP from here) * This is a placeholder - in production, use the MCP or a proper SSH library */ async function executeSSH(server: SSHServer, command: string): Promise { // For now, we'll use the database values or call an external endpoint // In production, you'd use ssh2 library or call the MCP endpoint const { exec } = await import('child_process') const { promisify } = await import('util') const execAsync = promisify(exec) try { // Use sshpass for password-based auth (not ideal but works for internal servers) const sshCommand = `sshpass -p '${server.pass}' ssh -o StrictHostKeyChecking=no -o ConnectTimeout=10 ${server.user}@${server.host} "${command}"` const { stdout } = await execAsync(sshCommand, { timeout: 30000 }) return stdout } catch (error) { console.error(`SSH error for ${server.name}:`, error) return '' } } /** * Collect metrics from SSH servers (CWP, EasyPanel) */ export async function collectSSHMetrics(): Promise<{ success: number; failed: number }> { let success = 0 let failed = 0 const metricsCommand = `echo "CPU:$(top -bn1 | grep 'Cpu(s)' | awk '{print $2}')"; echo "MEM:$(free -m | awk '/Mem:/ {printf "%.1f", $3/$2*100}')"; echo "DISK:$(df -h / | awk 'NR==2 {print $5}' | tr -d '%')"; echo "LOAD:$(cat /proc/loadavg | awk '{print $1}')"; echo "CONTAINERS:$(docker ps -q 2>/dev/null | wc -l || echo 0)"` for (const server of SSH_SERVERS) { try { const output = await executeSSH(server, metricsCommand) if (!output) { failed++ continue } const metrics = parseSSHMetrics(output) // Update monitoring table await db.query( `UPDATE tbl_eal_monitoring SET details = ?, status = 'up', last_check = NOW() WHERE category = 'server' AND name = ?`, [ JSON.stringify({ cpu: metrics.cpu, ram: metrics.ram, disk: metrics.disk, load: metrics.load }), server.monitorName ] ) // Update containers if EasyPanel if (server.name === 'easy' && metrics.containers !== undefined) { // Get current container stats const containerOutput = await executeSSH(server, 'docker ps -a --format "{{.Status}}" | grep -c "Up" || echo 0; docker ps -aq | wc -l') const [up, total] = containerOutput.trim().split('\n').map(n => parseInt(n) || 0) const down = total - up await db.query( `UPDATE tbl_eal_monitoring SET details = ?, status = ?, last_check = NOW() WHERE category = 'container' AND name = 'EasyPanel Containers'`, [ JSON.stringify({ total, up, down, restarting: 0 }), down > 0 ? 'warning' : 'ok' ] ) } success++ console.log(`✅ ${server.monitorName}: CPU=${metrics.cpu}%, RAM=${metrics.ram}%, Disk=${metrics.disk}%`) } catch (error) { console.error(`❌ Failed to collect metrics from ${server.name}:`, error) failed++ } } return { success, failed } } /** * Sync Hetzner metrics to monitoring table */ export async function syncHetznerToMonitoring(): Promise { // First collect fresh Hetzner metrics await collectHetznerMetrics() // Then sync to monitoring table let synced = 0 for (const [hetznerName, monitorName] of Object.entries(HETZNER_MAPPING)) { const namePattern = hetznerName.includes('.') ? hetznerName : `${hetznerName}%` const [result] = await db.query(` UPDATE tbl_eal_monitoring m JOIN v_eal_hetzner_latest h ON h.name LIKE ? SET m.details = JSON_OBJECT( 'cpu', ROUND(h.cpu_percent, 1), 'ram', 0, 'disk', 0, 'load', ROUND(h.cpu_percent / 25, 2), 'network_in', h.network_in_bps, 'network_out', h.network_out_bps, 'hetzner_id', h.hetzner_id ), m.status = 'up', m.last_check = NOW() WHERE m.category = 'server' AND m.name = ? `, [hetznerName.includes('.') ? hetznerName : hetznerName, monitorName]) if ((result as any).affectedRows > 0) synced++ } return synced } /** * Collect all server metrics (Hetzner + SSH) */ export async function collectAllServerMetrics(): Promise<{ hetzner: { success: number; failed: number } ssh: { success: number; failed: number } synced: number }> { console.log('[METRICS] Starting server metrics collection...') // Collect SSH metrics (CWP, EasyPanel) const ssh = await collectSSHMetrics() console.log(`[SSH] ${ssh.success} OK, ${ssh.failed} failed`) // Collect and sync Hetzner metrics const synced = await syncHetznerToMonitoring() console.log(`[HETZNER] ${synced} servers synced to monitoring`) return { hetzner: { success: synced, failed: 4 - synced }, ssh, synced } }