feat: server metrics API — local os + remote SSH
All checks were successful
Deploy API Server / build-and-deploy (push) Successful in 38s
All checks were successful
Deploy API Server / build-and-deploy (push) Successful in 38s
This commit is contained in:
parent
c31725433d
commit
f30a446bd5
@ -15,6 +15,7 @@ import { AuthModule } from './modules/auth/auth.module';
|
|||||||
import { AdminAuthModule } from './modules/admin-auth/admin-auth.module';
|
import { AdminAuthModule } from './modules/admin-auth/admin-auth.module';
|
||||||
import { AdminDashboardModule } from './modules/admin-dashboard/admin-dashboard.module';
|
import { AdminDashboardModule } from './modules/admin-dashboard/admin-dashboard.module';
|
||||||
import { AdminUsersModule } from './modules/admin-users/admin-users.module';
|
import { AdminUsersModule } from './modules/admin-users/admin-users.module';
|
||||||
|
import { AdminServersModule } from './modules/admin-servers/admin-servers.module';
|
||||||
import { AdminConversationModule } from './modules/admin-conversation/admin-conversation.module';
|
import { AdminConversationModule } from './modules/admin-conversation/admin-conversation.module';
|
||||||
import { AdminAiChatModule } from './modules/admin-ai-chat/admin-ai-chat.module';
|
import { AdminAiChatModule } from './modules/admin-ai-chat/admin-ai-chat.module';
|
||||||
import { AdminAuditLogModule } from './modules/admin-audit-log/admin-audit-log.module';
|
import { AdminAuditLogModule } from './modules/admin-audit-log/admin-audit-log.module';
|
||||||
@ -89,6 +90,7 @@ import appleConfig from './config/apple.config';
|
|||||||
AdminAuthModule,
|
AdminAuthModule,
|
||||||
AdminDashboardModule,
|
AdminDashboardModule,
|
||||||
AdminUsersModule,
|
AdminUsersModule,
|
||||||
|
AdminServersModule,
|
||||||
AdminConversationModule,
|
AdminConversationModule,
|
||||||
AdminAiChatModule,
|
AdminAiChatModule,
|
||||||
AdminAuditLogModule,
|
AdminAuditLogModule,
|
||||||
|
|||||||
22
src/modules/admin-servers/admin-servers.controller.ts
Normal file
22
src/modules/admin-servers/admin-servers.controller.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { Controller, Get, UseGuards } from '@nestjs/common';
|
||||||
|
import { ApiTags, ApiBearerAuth, ApiOperation } from '@nestjs/swagger';
|
||||||
|
import { AdminServersService } from './admin-servers.service';
|
||||||
|
import { AdminAuthGuard } from '../../common/guards/admin-auth.guard';
|
||||||
|
import { AdminRolesGuard } from '../../common/guards/admin-roles.guard';
|
||||||
|
import { AdminRoles } from '../../common/decorators/admin-roles.decorator';
|
||||||
|
import type { AdminRole } from '../../common/types/admin-role.enum';
|
||||||
|
|
||||||
|
@ApiTags('admin-servers')
|
||||||
|
@Controller('admin-api/servers')
|
||||||
|
@UseGuards(AdminAuthGuard, AdminRolesGuard)
|
||||||
|
@ApiBearerAuth()
|
||||||
|
export class AdminServersController {
|
||||||
|
constructor(private readonly serversService: AdminServersService) {}
|
||||||
|
|
||||||
|
@Get('metrics')
|
||||||
|
@AdminRoles('SUPER_ADMIN' as AdminRole)
|
||||||
|
@ApiOperation({ summary: '服务器实时指标(仅超级管理员)' })
|
||||||
|
async getMetrics() {
|
||||||
|
return this.serversService.getAllMetrics();
|
||||||
|
}
|
||||||
|
}
|
||||||
11
src/modules/admin-servers/admin-servers.module.ts
Normal file
11
src/modules/admin-servers/admin-servers.module.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { AdminServersController } from './admin-servers.controller';
|
||||||
|
import { AdminServersService } from './admin-servers.service';
|
||||||
|
import { AdminAuthGuard } from '../../common/guards/admin-auth.guard';
|
||||||
|
import { AdminRolesGuard } from '../../common/guards/admin-roles.guard';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
controllers: [AdminServersController],
|
||||||
|
providers: [AdminServersService, AdminAuthGuard, AdminRolesGuard],
|
||||||
|
})
|
||||||
|
export class AdminServersModule {}
|
||||||
142
src/modules/admin-servers/admin-servers.service.ts
Normal file
142
src/modules/admin-servers/admin-servers.service.ts
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
|
import * as os from 'os';
|
||||||
|
import { exec } from 'child_process';
|
||||||
|
import { promisify } from 'util';
|
||||||
|
|
||||||
|
const execAsync = promisify(exec);
|
||||||
|
|
||||||
|
interface ServerMetrics {
|
||||||
|
hostname: string;
|
||||||
|
cpu: { model: string; cores: number; usagePercent: number; loadAvg: number[] };
|
||||||
|
memory: { total: string; used: string; free: string; percent: number };
|
||||||
|
disk: { total: string; used: string; free: string; percent: number };
|
||||||
|
uptime: string;
|
||||||
|
processes: { pid: number; cpu: string; mem: string; command: string }[];
|
||||||
|
network: { ip: string };
|
||||||
|
}
|
||||||
|
|
||||||
|
const REMOTE_SSH = 'ssh -o StrictHostKeyChecking=no -o ConnectTimeout=5 -i /home/ubuntu/.ssh/zhixi.pem ubuntu@10.2.0.7';
|
||||||
|
const SSH_KEY_PATH = process.env.SSH_KEY_PATH || '/home/ubuntu/.ssh/zhixi.pem';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AdminServersService {
|
||||||
|
private readonly logger = new Logger(AdminServersService.name);
|
||||||
|
|
||||||
|
async getLocalMetrics(): Promise<ServerMetrics> {
|
||||||
|
const cpus = os.cpus();
|
||||||
|
const totalMem = os.totalmem();
|
||||||
|
const freeMem = os.freemem();
|
||||||
|
const usedMem = totalMem - freeMem;
|
||||||
|
|
||||||
|
// CPU usage (approximate via load avg vs cores)
|
||||||
|
const loadAvg = os.loadavg();
|
||||||
|
const cpuUsage = Math.round((loadAvg[0] / cpus.length) * 100);
|
||||||
|
|
||||||
|
// Disk
|
||||||
|
let disk = { total: '-', used: '-', free: '-', percent: 0 };
|
||||||
|
try {
|
||||||
|
const { stdout } = await execAsync("df -h / | tail -1 | awk '{print $2,$3,$4,$5}'");
|
||||||
|
const [total, used, free, pct] = stdout.trim().split(/\s+/);
|
||||||
|
disk = { total, used, free, percent: parseInt(pct) || 0 };
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
// Top processes
|
||||||
|
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').map(line => {
|
||||||
|
const [pid, cpu, mem, ...cmd] = line.trim().split(/\s+/);
|
||||||
|
return { pid: parseInt(pid), cpu: cpu + '%', mem: mem + '%', command: cmd.join(' ').slice(0, 60) };
|
||||||
|
});
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
// Network IPs
|
||||||
|
const nets = os.networkInterfaces();
|
||||||
|
const ip = Object.values(nets).flat().find(n => n?.family === 'IPv4' && !n.internal)?.address || 'unknown';
|
||||||
|
|
||||||
|
// Uptime
|
||||||
|
const uptimeSeconds = os.uptime();
|
||||||
|
const d = Math.floor(uptimeSeconds / 86400);
|
||||||
|
const h = Math.floor((uptimeSeconds % 86400) / 3600);
|
||||||
|
const m = Math.floor((uptimeSeconds % 3600) / 60);
|
||||||
|
const uptime = `${d}d ${h}h ${m}m`;
|
||||||
|
|
||||||
|
return {
|
||||||
|
hostname: os.hostname(),
|
||||||
|
cpu: { model: cpus[0]?.model || '', cores: cpus.length, usagePercent: cpuUsage, loadAvg },
|
||||||
|
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, processes,
|
||||||
|
network: { ip },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async getRemoteMetrics(): Promise<ServerMetrics | null> {
|
||||||
|
try {
|
||||||
|
const sshKey = SSH_KEY_PATH;
|
||||||
|
const cmd = `${REMOTE_SSH} 'echo "HOST=$(hostname)"; echo "IP=$(hostname -I | awk '"'"'{print \$1}'"'"')"; echo "UPTIME=$(uptime -p)"; top -bn1 | head -1; free -h | grep Mem; df -h / | tail -1; ps aux --sort=-%mem --no-headers | head -6 | awk '"'"'{print \$2,\$3,\$4,\$11}'"'"'`;
|
||||||
|
const { stdout } = await execAsync(cmd.replace('ssh -o', `ssh -i ${sshKey} -o`), { timeout: 8000 });
|
||||||
|
const lines = stdout.trim().split('\n');
|
||||||
|
|
||||||
|
const hostname = lines.find(l => l.startsWith('HOST='))?.split('=')[1] || 'remote';
|
||||||
|
const ip = lines.find(l => l.startsWith('IP='))?.split('=')[1] || '10.2.0.7';
|
||||||
|
const uptimeStr = lines.find(l => l.startsWith('UPTIME='))?.split('=')[1]?.replace('up ', '') || '';
|
||||||
|
|
||||||
|
// top output: "load average: 0.08, 0.03, 0.01"
|
||||||
|
const topLine = lines.find(l => l.includes('load average')) || '';
|
||||||
|
const loadMatch = topLine.match(/load average: ([\d.]+), ([\d.]+), ([\d.]+)/);
|
||||||
|
const loadAvg = loadMatch ? [parseFloat(loadMatch[1]), parseFloat(loadMatch[2]), parseFloat(loadMatch[3])] : [0, 0, 0];
|
||||||
|
|
||||||
|
// memory
|
||||||
|
const memLine = lines.find(l => /Mem:/.test(l)) || '';
|
||||||
|
const memParts = memLine.replace('Mem:', '').trim().split(/\s+/);
|
||||||
|
|
||||||
|
// disk
|
||||||
|
const diskLine = lines.find(l => /\/$/.test(l) || l.includes('/ ')) || '';
|
||||||
|
const diskParts = diskLine.trim().split(/\s+/);
|
||||||
|
|
||||||
|
// processes
|
||||||
|
const procLines = lines.filter(l => /^\d+\s/.test(l));
|
||||||
|
|
||||||
|
const cpuUsage = Math.round((loadAvg[0] / 4) * 100); // assume 4 cores
|
||||||
|
|
||||||
|
return {
|
||||||
|
hostname,
|
||||||
|
cpu: { model: 'Intel Xeon (Lighthouse)', cores: 4, usagePercent: cpuUsage, loadAvg },
|
||||||
|
memory: {
|
||||||
|
total: memParts[1] || '-',
|
||||||
|
used: memParts[2] || '-',
|
||||||
|
free: memParts[3] || '-',
|
||||||
|
percent: loadAvg[0] > 0 ? Math.round(cpuUsage) : 0,
|
||||||
|
},
|
||||||
|
disk: {
|
||||||
|
total: diskParts[1] || '-',
|
||||||
|
used: diskParts[2] || '-',
|
||||||
|
free: diskParts[3] || '-',
|
||||||
|
percent: parseInt(diskParts[4]) || 0,
|
||||||
|
},
|
||||||
|
uptime: uptimeStr,
|
||||||
|
processes: procLines.map(line => {
|
||||||
|
const [pid, cpu, mem, ...cmd] = line.trim().split(/\s+/);
|
||||||
|
return { pid: parseInt(pid), cpu: cpu + '%', mem: mem + '%', command: (cmd || []).join(' ').slice(0, 60) };
|
||||||
|
}),
|
||||||
|
network: { ip },
|
||||||
|
};
|
||||||
|
} catch (err: any) {
|
||||||
|
this.logger.warn('Remote metrics failed: ' + err.message);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAllMetrics() {
|
||||||
|
const [local, remote] = await Promise.all([
|
||||||
|
this.getLocalMetrics(),
|
||||||
|
this.getRemoteMetrics(),
|
||||||
|
]);
|
||||||
|
return {
|
||||||
|
servers: [
|
||||||
|
{ name: '蜂驰云 8核32G', role: '生产核心', ...local },
|
||||||
|
...(remote ? [{ name: '轻量云 4核4G', role: '工具/辅助', ...remote }] : []),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user