feat: friendly process names + data disk + public IPs + domains
All checks were successful
Deploy API Server / build-and-deploy (push) Successful in 36s
All checks were successful
Deploy API Server / build-and-deploy (push) Successful in 36s
This commit is contained in:
parent
13a7718a3c
commit
1776bed47e
@ -16,7 +16,7 @@ export class AdminServersController {
|
|||||||
@Get('metrics')
|
@Get('metrics')
|
||||||
@AdminRoles('SUPER_ADMIN' as AdminRole)
|
@AdminRoles('SUPER_ADMIN' as AdminRole)
|
||||||
@ApiOperation({ summary: '服务器实时指标(仅超级管理员)' })
|
@ApiOperation({ summary: '服务器实时指标(仅超级管理员)' })
|
||||||
async getMetrics() {
|
async getMetrics(): Promise<{ servers: import("./admin-servers.service").ServerInfo[] }> {
|
||||||
return this.serversService.getAllMetrics();
|
return this.serversService.getAllMetrics();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,104 +5,151 @@ import { promisify } from 'util';
|
|||||||
|
|
||||||
const execAsync = promisify(exec);
|
const execAsync = promisify(exec);
|
||||||
|
|
||||||
interface ServerMetrics {
|
export interface DiskInfo { mount: string; total: string; used: string; free: string; percent: number }
|
||||||
hostname: string;
|
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 };
|
cpu: { model: string; cores: number; usagePercent: number };
|
||||||
memory: { total: string; used: string; free: string; percent: number };
|
memory: { total: string; used: string; free: string; percent: number };
|
||||||
disk: { total: string; used: string; free: string; percent: number };
|
disks: DiskInfo[];
|
||||||
uptime: string;
|
uptime: string;
|
||||||
processes: { pid: number; cpu: string; mem: string; command: string }[];
|
processes: ProcessInfo[];
|
||||||
network: { ip: string };
|
network: { publicIp: string; privateIp: string; domains: string[] };
|
||||||
}
|
}
|
||||||
|
|
||||||
const SSH_KEY_PATH = process.env.SSH_KEY_PATH || '/home/ubuntu/.ssh/wangdl.pem';
|
const SSH_KEY_PATH = process.env.SSH_KEY_PATH || '/home/ubuntu/.ssh/wangdl.pem';
|
||||||
const REMOTE_HOST = '10.2.0.7';
|
const REMOTE_HOST = '10.2.0.7';
|
||||||
|
|
||||||
|
// Map raw process commands to friendly names
|
||||||
|
const PROCESS_ALIASES: Record<string, string> = {
|
||||||
|
'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<DiskInfo[]> {
|
||||||
|
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<ProcessInfo[]> {
|
||||||
|
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()
|
@Injectable()
|
||||||
export class AdminServersService {
|
export class AdminServersService {
|
||||||
private readonly logger = new Logger(AdminServersService.name);
|
private readonly logger = new Logger(AdminServersService.name);
|
||||||
|
|
||||||
async getLocalMetrics(): Promise<ServerMetrics> {
|
async getLocalMetrics(): Promise<ServerInfo> {
|
||||||
const cpus = os.cpus();
|
const cpus = os.cpus();
|
||||||
const totalMem = os.totalmem();
|
const totalMem = os.totalmem();
|
||||||
const freeMem = os.freemem();
|
const freeMem = os.freemem();
|
||||||
const usedMem = totalMem - freeMem;
|
const usedMem = totalMem - freeMem;
|
||||||
const loadAvg = os.loadavg();
|
const cpuUsage = Math.min(100, Math.round((os.loadavg()[0] / cpus.length) * 100));
|
||||||
const cpuUsage = Math.min(100, Math.round((loadAvg[0] / cpus.length) * 100));
|
|
||||||
|
|
||||||
let disk = { total: '-', used: '-', free: '-', percent: 0 };
|
const [disks, processes] = await Promise.all([
|
||||||
try {
|
getDisks(['/', '/data']),
|
||||||
const { stdout } = await execAsync("df -h / | tail -1 | awk '{print $2,$3,$4,$5}'");
|
getProcesses(),
|
||||||
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 nets = os.networkInterfaces();
|
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 d = Math.floor(os.uptime() / 86400);
|
||||||
const h = Math.floor((os.uptime() % 86400) / 3600);
|
const h = Math.floor((os.uptime() % 86400) / 3600);
|
||||||
const m = Math.floor((os.uptime() % 3600) / 60);
|
const m = Math.floor((os.uptime() % 3600) / 60);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
hostname: os.hostname(),
|
name: '蜂驰云 8核32G', role: '生产核心', hostname: os.hostname(),
|
||||||
cpu: { model: cpus[0]?.model || '', cores: cpus.length, usagePercent: cpuUsage },
|
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) },
|
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,
|
disks, uptime: `${d}d ${h}h ${m}m`, processes,
|
||||||
network: { ip },
|
network: { publicIp: '120.53.227.155', privateIp, domains: ['api.longde.cloud', 'admin.longde.cloud'] },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async getRemoteMetrics(): Promise<ServerMetrics | null> {
|
async getRemoteMetrics(): Promise<ServerInfo | null> {
|
||||||
try {
|
try {
|
||||||
const base = `ssh -o StrictHostKeyChecking=no -o ConnectTimeout=5 -i ${SSH_KEY_PATH} ubuntu@${REMOTE_HOST}`;
|
const base = `ssh -o StrictHostKeyChecking=no -o ConnectTimeout=5 -i ${SSH_KEY_PATH} ubuntu@${REMOTE_HOST}`;
|
||||||
const cmds = [
|
|
||||||
`${base} hostname`,
|
const execSSH = (cmd: string) => execAsync(`${base} "${cmd.replace(/"/g, '\\"')}"`, { timeout: 5000 }).then(r => r.stdout.trim()).catch(() => '');
|
||||||
`${base} "hostname -I | awk '{print \\$1}'"`,
|
|
||||||
`${base} "cat /proc/loadavg | awk '{print \\$1}'"`,
|
const [hostname, privateIp, load, cores, memRaw, diskRoot, diskData, uptimeStr] = await Promise.all([
|
||||||
`${base} "cat /proc/cpuinfo | grep processor | wc -l"`,
|
execSSH('hostname'),
|
||||||
`${base} "free -m | grep Mem | awk '{print \\$2,\\$3,\\$4}'"`,
|
execSSH("hostname -I | awk '{print $1}'"),
|
||||||
`${base} "df -h / | tail -1 | awk '{print \\$2,\\$3,\\$4,\\$5}'"`,
|
execSSH("cat /proc/loadavg | awk '{print $1}'"),
|
||||||
`${base} "uptime -p | sed 's/up //'"`,
|
execSSH('cat /proc/cpuinfo | grep processor | wc -l'),
|
||||||
`${base} "ps aux --sort=-%mem --no-headers | head -6 | awk '{print \\$2,\\$3,\\$4,\\$11}'"`,
|
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 },
|
||||||
];
|
];
|
||||||
|
if (dataParts.length >= 3) {
|
||||||
const results = await Promise.all(cmds.map(c => execAsync(c, { timeout: 5000 }).then(r => r.stdout.trim()).catch(() => '')));
|
disks.push({ mount: '/data', total: dataParts[0], used: dataParts[1], free: dataParts[2], percent: parseInt(dataParts[3]) || 0 });
|
||||||
|
}
|
||||||
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) };
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
hostname, cpu: { model: 'Intel Xeon (Lighthouse)', cores, usagePercent: cpuUsage },
|
name: '轻量云 4核4G', role: '工具/辅助', hostname: hostname || 'remote',
|
||||||
memory: { total: memTotal, used: memUsed, free: memFree, percent: memPercent },
|
cpu: { model: 'Intel Xeon (Lighthouse)', cores: cpuCores, usagePercent: cpuUsage },
|
||||||
disk: { total: diskParts[0] || '-', used: diskParts[1] || '-', free: diskParts[2] || '-', percent: diskPercent },
|
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 },
|
||||||
uptime: results[6] || '-', processes,
|
disks, uptime: uptimeStr || '-', processes,
|
||||||
network: { ip },
|
network: { publicIp: '81.70.187.179', privateIp: privateIp || '10.2.0.7', domains: ['longde.cloud', 'git.longde.cloud'] },
|
||||||
};
|
};
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
this.logger.warn('Remote metrics failed: ' + err.message);
|
this.logger.warn('Remote metrics failed: ' + err.message);
|
||||||
@ -112,10 +159,8 @@ export class AdminServersService {
|
|||||||
|
|
||||||
async getAllMetrics() {
|
async getAllMetrics() {
|
||||||
const [local, remote] = await Promise.all([this.getLocalMetrics(), this.getRemoteMetrics()]);
|
const [local, remote] = await Promise.all([this.getLocalMetrics(), this.getRemoteMetrics()]);
|
||||||
const servers = [
|
const servers = [local];
|
||||||
{ name: '蜂驰云 8核32G', role: '生产核心', ...local },
|
if (remote) servers.push(remote);
|
||||||
];
|
|
||||||
if (remote) servers.push({ name: '轻量云 4核4G', role: '工具/辅助', ...remote });
|
|
||||||
return { servers };
|
return { servers };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user