fix: Remaining TypeScript strict mode errors in routes
This commit is contained in:
219
api/services/server-metrics.ts
Normal file
219
api/services/server-metrics.ts
Normal file
@@ -0,0 +1,219 @@
|
||||
/**
|
||||
* 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<string, string> = {
|
||||
'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<string> {
|
||||
// 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<number> {
|
||||
// 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<any[]>(`
|
||||
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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user