Files
SenMeshNet/electron/bridges/inspection/adapters/linuxServerAdapter.cjs
zhaolei 2f9f8da48e
Some checks failed
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
Initial commit: NetMesh smart ops platform with 3D topology, inspection reports, RCA and AI ops assistant
2026-10-15 13:53:04 +08:00

133 lines
4.8 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use strict";
/**
* Linux 服务器采集器
*
* 采集指标(告警相关):
* - cpu_usage CPU 使用率(%)
* - memory_usage 内存使用率(%)
* - disk_usage 磁盘使用率(%,只显示超过 80% 的挂载点)
*
* 实现说明:
* 直接使用 POSIX shell 命令,命令与 NetMesh System Manager 的概览一致。
* 传入 exec 函数来执行远程命令(由调度器注入,复用 SSH 连接池)。
*/
const BaseAdapter = require("./baseAdapter.cjs");
class LinuxServerAdapter extends BaseAdapter {
constructor(connectionInfo, options = {}) {
super(connectionInfo, options);
this.adapterName = "linux_server";
// 支持外部注入 exec 函数(用于复用 SSH 连接)
if (options.execFn) {
this.execute = options.execFn;
}
}
getSupportedMetrics() {
return [
"cpu_usage",
"memory_usage",
"disk_usage",
];
}
async collectMetrics() {
const metrics = {};
const raw = {};
const DEBUG = process.env.INSPECTION_DEBUG === "1";
// 1. CPU 使用率(采样 top)
try {
raw.cpu = await this.execute("top -bn1 2>/dev/null | head -n 5");
if (DEBUG) console.log("[LinuxAdapter] CPU stdout:", JSON.stringify(raw.cpu?.slice(0, 200)));
const cpuMatch = (raw.cpu || "").match(/Cpu\(s\):\s*([\d.]+)\s*us,\s*([\d.]+)\s*sy/i);
if (cpuMatch) {
metrics.cpu_usage = Math.round(parseFloat(cpuMatch[1]) + parseFloat(cpuMatch[2]));
} else {
// 尝试 mpstat 兜底(部分系统 top 不支持 batch 模式)
try {
raw.cpuMpstat = await this.execute("mpstat 1 1 2>/dev/null | tail -n 1");
if (DEBUG) console.log("[LinuxAdapter] mpstat stdout:", JSON.stringify(raw.cpuMpstat?.slice(0, 200)));
const idleMatch = (raw.cpuMpstat || "").match(/(?:%?idle|idle)\s*[:=]?\s*([\d.]+)/i);
if (idleMatch) metrics.cpu_usage = Math.round(100 - parseFloat(idleMatch[1]));
} catch (_) { /* ignore */ }
}
} catch (e) {
if (DEBUG) console.error("[LinuxAdapter] CPU 采集失败:", e.message);
throw e; // SSH 失败或其他错误必须向上传播
}
// 2. 内存使用率
try {
raw.mem = await this.execute("free -k 2>/dev/null | head -n 2 | tail -n 1");
if (DEBUG) console.log("[LinuxAdapter] MEM stdout:", JSON.stringify(raw.mem?.slice(0, 200)));
// 支持中英文输出: "Mem:" 或 "内存:"
const memMatch = (raw.mem || "").match(/(?:Mem:|内存[::])\s+(\d+)\s+(\d+)/);
if (memMatch) {
const total = parseInt(memMatch[1], 10);
const used = parseInt(memMatch[2], 10);
if (total > 0) {
metrics.memory_usage = Math.min(100, Math.round((used / total) * 100));
}
} else {
// 兜底:/proc/meminfo
try {
raw.memInfo = await this.execute("cat /proc/meminfo 2>/dev/null | head -n 3");
if (DEBUG) console.log("[LinuxAdapter] meminfo stdout:", JSON.stringify(raw.memInfo?.slice(0, 300)));
const memInfo = raw.memInfo || "";
const totalMatch = memInfo.match(/MemTotal:\s+(\d+)/);
const availMatch = memInfo.match(/MemAvailable:\s+(\d+)/);
if (totalMatch && availMatch) {
const total = parseInt(totalMatch[1], 10);
const avail = parseInt(availMatch[1], 10);
metrics.memory_usage = Math.min(100, Math.round(((total - avail) / total) * 100));
}
} catch (_) { /* ignore */ }
}
} catch (e) {
if (DEBUG) console.error("[LinuxAdapter] MEM 采集失败:", e.message);
throw e;
}
// 3. 磁盘使用率(显示所有挂载点)
try {
raw.disk = await this.execute("df -h 2>/dev/null | tail -n +2 | grep -v '/dev/loop'");
if (DEBUG) console.log("[LinuxAdapter] DISK stdout:", JSON.stringify(raw.disk?.slice(0, 300)));
const disks = this.parseDiskUsage(raw.disk);
if (disks.length > 0) metrics.disk_usage = disks;
} catch (e) {
if (DEBUG) console.error("[LinuxAdapter] DISK 采集失败:", e.message);
throw e;
}
if (DEBUG) console.log("[LinuxAdapter] 最终 metrics:", JSON.stringify(metrics));
return metrics;
}
/**
* 解析磁盘使用率(df -h 输出格式),返回所有挂载点
*/
parseDiskUsage(text) {
const disks = [];
const lines = (text || "").split("\n");
for (const line of lines) {
const parts = line.trim().split(/\s+/);
if (parts.length < 6) continue;
const usageStr = parts[4] || "";
const usageMatch = usageStr.match(/^(\d+)%/);
if (!usageMatch) continue;
const usagePercent = parseInt(usageMatch[1], 10);
disks.push({
filesystem: parts[0],
usage_percent: usagePercent,
mount: parts[parts.length - 1],
});
}
return disks;
}
}
module.exports = LinuxServerAdapter;