From 94db202de966e5987e1564d9b78668c9e57006e0 Mon Sep 17 00:00:00 2001 From: Emanuel Almeida Date: Tue, 28 Apr 2026 16:08:20 +0100 Subject: [PATCH] fix(monitoring): SSH ao EasyPanel em vez de API inexistente MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - server-metrics: substituir CWP (só aceita ed25519) por Easy server (aceita password auth na porta 22) - monitoring-collector: remover chamadas a monitor.getSystemStats e monitor.getDockerTaskStats (endpoint não existe nesta versão EasyPanel); métricas CPU/RAM via SSH e containers via docker service ls sobre SSH Co-Authored-By: Claude Sonnet 4.6 --- api/services/monitoring-collector.ts | 138 ++++++++++++--------------- api/services/server-metrics.ts | 18 ++-- 2 files changed, 69 insertions(+), 87 deletions(-) diff --git a/api/services/monitoring-collector.ts b/api/services/monitoring-collector.ts index 9db4138..1a90ec2 100644 --- a/api/services/monitoring-collector.ts +++ b/api/services/monitoring-collector.ts @@ -19,14 +19,6 @@ interface CheckResult { error?: string } -/** - * EasyPanel API config. - * Accessible from Docker Swarm via service name 'easypanel'. - * Token read from EASYPANEL_API_TOKEN env var. - */ -const EASYPANEL_API_URL = process.env.EASYPANEL_API_URL || 'http://easypanel:3000/api/trpc' -const EASYPANEL_API_TOKEN = process.env.EASYPANEL_API_TOKEN || '' - /** * Services to monitor via HTTP health check. * Each entry maps to a record in tbl_eal_monitoring (category='service'). @@ -163,86 +155,76 @@ export async function checkStaleness(): Promise { } /** - * Call EasyPanel tRPC API endpoint. - * Returns parsed JSON or null on failure. - */ -async function callEasyPanelAPI(endpoint: string): Promise { - if (!EASYPANEL_API_TOKEN) return null - - try { - const controller = new AbortController() - const timeout = setTimeout(() => controller.abort(), 10000) - - const response = await fetch(`${EASYPANEL_API_URL}/${endpoint}`, { - headers: { 'Authorization': `Bearer ${EASYPANEL_API_TOKEN}` }, - signal: controller.signal, - }) - - clearTimeout(timeout) - if (!response.ok) return null - - const data: any = await response.json() - return data?.result?.data?.json ?? null - } catch { - return null - } -} - -/** - * Collect EasyPanel server metrics (CPU, RAM, disk) via API. - * Replaces SSH-based collection for the Easy server. + * Collect EasyPanel server metrics + container stats via SSH. + * A API tRPC do EasyPanel não expõe endpoint monitor.* nesta versão. + * SSH com password ao Easy server (5.9.90.70) funciona a partir do container. */ export async function collectEasyPanelMetrics(): Promise { - const stats = await callEasyPanelAPI('monitor.getSystemStats') - if (!stats) return false - - const cpu = Math.round(stats.cpuInfo?.usedPercentage ?? 0) - const ram = Math.round((stats.memInfo?.usedMemPercentage ?? 0) * 10) / 10 - const disk = parseFloat(stats.diskInfo?.usedPercentage ?? '0') - const load = stats.cpuInfo?.loadavg?.[0] ?? 0 - - await upsertMonitoring('server', 'EasyPanel', 'up', { - cpu, ram, disk, load, - uptime_hours: Math.round((stats.uptime ?? 0) / 3600), - mem_total_mb: Math.round(stats.memInfo?.totalMemMb ?? 0), - mem_used_mb: Math.round(stats.memInfo?.usedMemMb ?? 0), - disk_total_gb: stats.diskInfo?.totalGb, - disk_free_gb: stats.diskInfo?.freeGb, - }) - - console.log(`[EASYPANEL] Server: CPU=${cpu}%, RAM=${ram}%, Disk=${disk}%`) - return true + const { collectSSHMetrics } = await import('./server-metrics.js') + const result = await collectSSHMetrics() + return result.success > 0 } /** - * Collect Docker container/task stats via EasyPanel API. - * Updates the 'container' category in monitoring DB. + * Collect Docker Swarm service status via SSH to EasyPanel server. + * Usa `docker service ls` para obter replicas actual vs desired. */ export async function collectEasyPanelContainers(): Promise { - const tasks = await callEasyPanelAPI('monitor.getDockerTaskStats') - if (!tasks) return false + const easyHost = process.env.EASY_HOST || '5.9.90.70' + const easyUser = process.env.EASY_USER || 'root' + const easyPass = process.env.EASY_PASS || '' - let total = 0, up = 0, down = 0 - const unhealthy: string[] = [] + if (!easyPass) return false - for (const [name, info] of Object.entries(tasks) as [string, { actual: number; desired: number }][]) { - total++ - if (info.actual >= info.desired) { - up++ - } else { - down++ - unhealthy.push(name.replace('descomplicar_', '')) + try { + const { Client } = await import('ssh2') + const output = await new Promise((resolve, reject) => { + const conn = new Client() + let data = '' + const timer = setTimeout(() => { conn.end(); reject(new Error('timeout')) }, 20000) + + conn.on('ready', () => { + conn.exec("docker service ls --format '{{.Name}} {{.Replicas}}'", (err, stream) => { + if (err) { clearTimeout(timer); conn.end(); reject(err); return } + stream.on('data', (chunk: Buffer) => { data += chunk.toString() }) + stream.on('close', () => { clearTimeout(timer); conn.end(); resolve(data) }) + stream.stderr.on('data', () => {}) + }) + }) + conn.on('error', (err) => { clearTimeout(timer); reject(err) }) + conn.connect({ host: easyHost, port: 22, username: easyUser, password: easyPass, readyTimeout: 15000 }) + }) + + let total = 0, up = 0, down = 0 + const unhealthy: string[] = [] + + for (const line of output.trim().split('\n')) { + if (!line.trim()) continue + const parts = line.trim().split(/\s+/) + const name = parts[0] || '' + const replicas = parts[1] || '0/0' + const [actual, desired] = replicas.split('/').map(Number) + total++ + if (actual >= desired && desired > 0) { + up++ + } else { + down++ + unhealthy.push(name.replace('descomplicar_', '')) + } } + + const status = down > 0 ? 'warning' : 'ok' + await upsertMonitoring('container', 'EasyPanel Containers', status, { + total, up, down, restarting: 0, + ...(unhealthy.length > 0 ? { unhealthy } : {}), + }) + + console.log(`[EASYPANEL] Containers: ${up}/${total} running${down > 0 ? `, ${down} down: ${unhealthy.join(', ')}` : ''}`) + return true + } catch (err: unknown) { + console.error('[EASYPANEL] Container collection failed:', err instanceof Error ? err.message : err) + return false } - - const status = down > 0 ? 'warning' : 'ok' - await upsertMonitoring('container', 'EasyPanel Containers', status, { - total, up, down, restarting: 0, - ...(unhealthy.length > 0 ? { unhealthy } : {}), - }) - - console.log(`[EASYPANEL] Containers: ${up}/${total} running${down > 0 ? `, ${down} down: ${unhealthy.join(', ')}` : ''}`) - return true } /** @@ -264,7 +246,7 @@ export async function collectMonitoringData(): Promise { const gotStats = await collectEasyPanelMetrics() const gotContainers = await collectEasyPanelContainers() if (!gotStats && !gotContainers) { - console.warn('[COLLECTOR] EasyPanel API unavailable (check EASYPANEL_API_TOKEN)') + console.warn('[COLLECTOR] EasyPanel metrics unavailable (check EASY_HOST/EASY_USER/EASY_PASS)') } } catch (err: unknown) { console.error('[COLLECTOR] EasyPanel collection failed:', err instanceof Error ? err.message : err) diff --git a/api/services/server-metrics.ts b/api/services/server-metrics.ts index 8a3c120..4c4cb0f 100755 --- a/api/services/server-metrics.ts +++ b/api/services/server-metrics.ts @@ -15,17 +15,17 @@ interface SSHServer { pass: string } -// EasyPanel metrics: collected via API in monitoring-collector.ts -// Gateway metrics: not needed (just Nginx proxy, covered by HTTP health check) -// Only CWP Server remains on SSH (password auth) +// CWP Server: só aceita autenticação por chave ed25519 (não por password) — não acessível a partir do container +// EasyPanel Server: aceita password auth na porta 22 — usado para métricas CPU/RAM/disk +// Gateway: apenas proxy Nginx, coberto pelo health check HTTP const SSH_SERVERS: SSHServer[] = [ { - name: 'server', - monitorName: 'CWP Server', - host: process.env.SERVER_HOST || '5.9.90.105', - port: parseInt(process.env.SERVER_PORT || '9443'), - user: process.env.SERVER_USER || 'root', - pass: process.env.SERVER_PASS || '' + name: 'easy', + monitorName: 'EasyPanel', + host: process.env.EASY_HOST || '5.9.90.70', + port: 22, + user: process.env.EASY_USER || 'root', + pass: process.env.EASY_PASS || '' } ]