diff --git a/src/modules/admin-servers/admin-servers.controller.ts b/src/modules/admin-servers/admin-servers.controller.ts index 466c02d..5272316 100644 --- a/src/modules/admin-servers/admin-servers.controller.ts +++ b/src/modules/admin-servers/admin-servers.controller.ts @@ -16,7 +16,7 @@ export class AdminServersController { @Get('metrics') @AdminRoles('SUPER_ADMIN' as AdminRole) @ApiOperation({ summary: '服务器实时指标(仅超级管理员)' }) - async getMetrics() { + async getMetrics(): Promise<{ servers: import("./admin-servers.service").ServerInfo[] }> { return this.serversService.getAllMetrics(); } } diff --git a/src/modules/admin-servers/admin-servers.service.ts b/src/modules/admin-servers/admin-servers.service.ts index 265dd5d..5320bc4 100644 --- a/src/modules/admin-servers/admin-servers.service.ts +++ b/src/modules/admin-servers/admin-servers.service.ts @@ -5,104 +5,151 @@ import { promisify } from 'util'; const execAsync = promisify(exec); -interface ServerMetrics { - hostname: string; +export interface DiskInfo { mount: string; total: string; used: string; free: string; percent: number } +export interface ProcessInfo { pid: number; cpu: string; mem: string; name: string; command: string } +export interface ServerInfo { + name: string; role: string; hostname: string; cpu: { model: string; cores: number; usagePercent: number }; memory: { total: string; used: string; free: string; percent: number }; - disk: { total: string; used: string; free: string; percent: number }; + disks: DiskInfo[]; uptime: string; - processes: { pid: number; cpu: string; mem: string; command: string }[]; - network: { ip: string }; + processes: ProcessInfo[]; + network: { publicIp: string; privateIp: string; domains: string[] }; } const SSH_KEY_PATH = process.env.SSH_KEY_PATH || '/home/ubuntu/.ssh/wangdl.pem'; const REMOTE_HOST = '10.2.0.7'; +// Map raw process commands to friendly names +const PROCESS_ALIASES: Record = { + 'mysqld': 'MySQL 8.0', + 'redis-server': 'Redis 7', + 'qdrant': 'Qdrant', + 'nginx': 'Nginx', + 'gitea': 'Gitea', + 'node dist/src/main.js': 'NestJS API', + 'node dist/main.js': 'NestJS API', + 'act_runner': 'Gitea Runner', + 'zhixi-worker': 'RAG Worker', + 'python3': 'Python Worker', + 'docker': 'Docker Daemon', + 'containerd': 'containerd', + 'hermes': 'Hermes Agent', + 'certbot': 'Certbot', +}; + +function friendlyName(cmd: string): string { + for (const [pattern, name] of Object.entries(PROCESS_ALIASES)) { + if (cmd.includes(pattern)) return name; + } + // Trim common paths + return cmd.replace(/^\/usr\/bin\//, '').replace(/^\/opt\/.*?\//, '').replace(/^\/usr\/lib\//, '').slice(0, 30); +} + +async function getDisks(mounts: string[]): Promise { + const results: DiskInfo[] = []; + for (const m of mounts) { + try { + const { stdout } = await execAsync(`df -h ${m} | tail -1 | awk '{print $2,$3,$4,$5}'`); + const parts = stdout.trim().split(/\s+/); + results.push({ mount: m, total: parts[0] || '-', used: parts[1] || '-', free: parts[2] || '-', percent: parseInt(parts[3]) || 0 }); + } catch { results.push({ mount: m, total: '-', used: '-', free: '-', percent: 0 }) } + } + return results; +} + +async function getProcesses(): Promise { + try { + const { stdout } = await execAsync("ps aux --sort=-%mem --no-headers | head -10 | awk '{print $2,$3,$4,$11}'"); + return stdout.trim().split('\n').filter(Boolean).map(line => { + const [pid, cpu, mem, ...cmd] = line.trim().split(/\s+/); + const command = (cmd || []).join(' '); + return { pid: parseInt(pid), cpu: cpu + '%', mem: mem + '%', name: friendlyName(command), command }; + }); + } catch { return [] } +} + @Injectable() export class AdminServersService { private readonly logger = new Logger(AdminServersService.name); - async getLocalMetrics(): Promise { + async getLocalMetrics(): Promise { const cpus = os.cpus(); const totalMem = os.totalmem(); const freeMem = os.freemem(); const usedMem = totalMem - freeMem; - const loadAvg = os.loadavg(); - const cpuUsage = Math.min(100, Math.round((loadAvg[0] / cpus.length) * 100)); + const cpuUsage = Math.min(100, Math.round((os.loadavg()[0] / cpus.length) * 100)); - let disk = { total: '-', used: '-', free: '-', percent: 0 }; - try { - const { stdout } = await execAsync("df -h / | tail -1 | awk '{print $2,$3,$4,$5}'"); - const parts = stdout.trim().split(/\s+/); - disk = { total: parts[0] || '-', used: parts[1] || '-', free: parts[2] || '-', percent: parseInt(parts[3]) || 0 }; - } catch {} - - let processes: ServerMetrics['processes'] = []; - try { - const { stdout } = await execAsync("ps aux --sort=-%mem --no-headers | head -8 | awk '{print $2,$3,$4,$11}'"); - processes = stdout.trim().split('\n').filter(Boolean).map(line => { - const [pid, cpu, mem, ...cmd] = line.trim().split(/\s+/); - return { pid: parseInt(pid), cpu: cpu + '%', mem: mem + '%', command: (cmd || []).join(' ').slice(0, 50) }; - }); - } catch {} + const [disks, processes] = await Promise.all([ + getDisks(['/', '/data']), + getProcesses(), + ]); const nets = os.networkInterfaces(); - const ip = Object.values(nets).flat().find(n => n?.family === 'IPv4' && !n.internal)?.address || 'unknown'; + const privateIp = Object.values(nets).flat().find(n => n?.family === 'IPv4' && !n.internal)?.address || '172.21.0.4'; const d = Math.floor(os.uptime() / 86400); const h = Math.floor((os.uptime() % 86400) / 3600); const m = Math.floor((os.uptime() % 3600) / 60); return { - hostname: os.hostname(), + name: '蜂驰云 8核32G', role: '生产核心', hostname: os.hostname(), cpu: { model: cpus[0]?.model || '', cores: cpus.length, usagePercent: cpuUsage }, memory: { total: (totalMem / 1e9).toFixed(1) + 'G', used: (usedMem / 1e9).toFixed(1) + 'G', free: (freeMem / 1e9).toFixed(1) + 'G', percent: Math.round((usedMem / totalMem) * 100) }, - disk, uptime: `${d}d ${h}h ${m}m`, processes, - network: { ip }, + disks, uptime: `${d}d ${h}h ${m}m`, processes, + network: { publicIp: '120.53.227.155', privateIp, domains: ['api.longde.cloud', 'admin.longde.cloud'] }, }; } - async getRemoteMetrics(): Promise { + async getRemoteMetrics(): Promise { try { const base = `ssh -o StrictHostKeyChecking=no -o ConnectTimeout=5 -i ${SSH_KEY_PATH} ubuntu@${REMOTE_HOST}`; - const cmds = [ - `${base} hostname`, - `${base} "hostname -I | awk '{print \\$1}'"`, - `${base} "cat /proc/loadavg | awk '{print \\$1}'"`, - `${base} "cat /proc/cpuinfo | grep processor | wc -l"`, - `${base} "free -m | grep Mem | awk '{print \\$2,\\$3,\\$4}'"`, - `${base} "df -h / | tail -1 | awk '{print \\$2,\\$3,\\$4,\\$5}'"`, - `${base} "uptime -p | sed 's/up //'"`, - `${base} "ps aux --sort=-%mem --no-headers | head -6 | awk '{print \\$2,\\$3,\\$4,\\$11}'"`, + + const execSSH = (cmd: string) => execAsync(`${base} "${cmd.replace(/"/g, '\\"')}"`, { timeout: 5000 }).then(r => r.stdout.trim()).catch(() => ''); + + const [hostname, privateIp, load, cores, memRaw, diskRoot, diskData, uptimeStr] = await Promise.all([ + execSSH('hostname'), + execSSH("hostname -I | awk '{print $1}'"), + execSSH("cat /proc/loadavg | awk '{print $1}'"), + execSSH('cat /proc/cpuinfo | grep processor | wc -l'), + execSSH("free -m | grep Mem | awk '{print $2,$3,$4}'"), + execSSH("df -h / | tail -1 | awk '{print $2,$3,$4,$5}'"), + execSSH("df -h /data 2>/dev/null | tail -1 | awk '{print $2,$3,$4,$5}'"), + execSSH("uptime -p | sed 's/up //'"), + ]); + + // Get remote process list + let processes: ProcessInfo[] = []; + try { + const procRaw = await execAsync(`${base} "ps aux --sort=-%mem --no-headers | head -10 | awk '{print \\$2,\\$3,\\$4,\\$11}'"`, { timeout: 5000 }); + processes = procRaw.stdout.trim().split('\n').filter(Boolean).map(line => { + const [pid, cpu, mem, ...cmd] = line.trim().split(/\s+/); + const command = (cmd || []).join(' '); + return { pid: parseInt(pid), cpu: cpu + '%', mem: mem + '%', name: friendlyName(command), command }; + }); + } catch {} + + const load1 = parseFloat(load) || 0; + const cpuCores = parseInt(cores) || 4; + const cpuUsage = Math.min(100, Math.round((load1 / cpuCores) * 100)); + + const memParts = memRaw.split(/\s+/); + const memPer = memParts[0] && memParts[1] ? Math.round((parseInt(memParts[1]) / parseInt(memParts[0])) * 100) : 0; + + const rootParts = diskRoot.split(/\s+/); + const dataParts = diskData.split(/\s+/); + const disks: DiskInfo[] = [ + { mount: '/', total: rootParts[0] || '-', used: rootParts[1] || '-', free: rootParts[2] || '-', percent: parseInt(rootParts[3]) || 0 }, ]; - - const results = await Promise.all(cmds.map(c => execAsync(c, { timeout: 5000 }).then(r => r.stdout.trim()).catch(() => ''))); - - const hostname = results[0] || 'remote'; - const ip = results[1] || '10.2.0.7'; - const load1 = parseFloat(results[2]) || 0; - const cores = parseInt(results[3]) || 4; - const cpuUsage = Math.min(100, Math.round((load1 / cores) * 100)); - - const memParts = results[4].split(/\s+/); - const memTotal = memParts[0] ? (parseInt(memParts[0]) / 1024).toFixed(1) + 'G' : '-'; - const memUsed = memParts[1] ? (parseInt(memParts[1]) / 1024).toFixed(1) + 'G' : '-'; - const memFree = memParts[2] ? (parseInt(memParts[2]) / 1024).toFixed(1) + 'G' : '-'; - const memPercent = memParts[0] && memParts[1] ? Math.round((parseInt(memParts[1]) / parseInt(memParts[0])) * 100) : 0; - - const diskParts = results[5].split(/\s+/); - const diskPercent = parseInt(diskParts[3]) || 0; - - const processes = results[7].split('\n').filter(Boolean).map(line => { - const [pid, cpu, mem, ...cmd] = line.trim().split(/\s+/); - return { pid: parseInt(pid), cpu: cpu + '%', mem: mem + '%', command: (cmd || []).join(' ').slice(0, 50) }; - }); + if (dataParts.length >= 3) { + disks.push({ mount: '/data', total: dataParts[0], used: dataParts[1], free: dataParts[2], percent: parseInt(dataParts[3]) || 0 }); + } return { - hostname, cpu: { model: 'Intel Xeon (Lighthouse)', cores, usagePercent: cpuUsage }, - memory: { total: memTotal, used: memUsed, free: memFree, percent: memPercent }, - disk: { total: diskParts[0] || '-', used: diskParts[1] || '-', free: diskParts[2] || '-', percent: diskPercent }, - uptime: results[6] || '-', processes, - network: { ip }, + name: '轻量云 4核4G', role: '工具/辅助', hostname: hostname || 'remote', + cpu: { model: 'Intel Xeon (Lighthouse)', cores: cpuCores, usagePercent: cpuUsage }, + memory: { total: memParts[0] ? (parseInt(memParts[0]) / 1024).toFixed(1) + 'G' : '-', used: memParts[1] ? (parseInt(memParts[1]) / 1024).toFixed(1) + 'G' : '-', free: memParts[2] ? (parseInt(memParts[2]) / 1024).toFixed(1) + 'G' : '-', percent: memPer }, + disks, uptime: uptimeStr || '-', processes, + network: { publicIp: '81.70.187.179', privateIp: privateIp || '10.2.0.7', domains: ['longde.cloud', 'git.longde.cloud'] }, }; } catch (err: any) { this.logger.warn('Remote metrics failed: ' + err.message); @@ -112,10 +159,8 @@ export class AdminServersService { async getAllMetrics() { const [local, remote] = await Promise.all([this.getLocalMetrics(), this.getRemoteMetrics()]); - const servers = [ - { name: '蜂驰云 8核32G', role: '生产核心', ...local }, - ]; - if (remote) servers.push({ name: '轻量云 4核4G', role: '工具/辅助', ...remote }); + const servers = [local]; + if (remote) servers.push(remote); return { servers }; } }