feat(server): 资源监控——/proc 与 cgroup 采样、/system/resources 趋势,心跳带进程 CPU

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Cxwgnn2pbzQ7rXv3rq7fei
This commit is contained in:
rootandClaude Opus 5.5 committed 2026-10-06 17:58:08 +08:00
1 parent 744252b969
commit 2ce1841628
6 files changed
+712 -2

No files matched your search

+49 -1
View File
@@ -2,7 +2,7 @@ import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import type { DiskUsage } from "@umt/adapters";
import { SystemHealth, SystemHealthHistory } from "@umt/contracts";
import { SystemHealth, SystemHealthHistory, SystemResources } from "@umt/contracts";
import { sql } from "@umt/db";
import { afterAll, beforeAll, describe, expect, it } from "vitest";
import { inferTriggeredBy, triggeredByFromActor } from "../../src/modules/auth-settings/engine-business.ts";
@@ -16,6 +16,7 @@ import {
writeHeartbeat,
} from "../../src/modules/system/heartbeat.ts";
import { createSystemModule } from "../../src/modules/system/index.ts";
import { createResourceSampler } from "../../src/modules/system/resources.ts";
import type { ServerContext } from "../../src/modules/types.ts";
import { bearer, createHarness, type Harness, pgAvailable } from "../auth-settings/harness.ts";
import { DEV_KEY, json } from "../helpers.ts";
@@ -217,6 +218,16 @@ describe.skipIf(!hasPg)("W9-I 进程与健康(真实 PG)", () => {
engineSchema: h.engineSchema,
apiHeartbeat: false,
healthDeps: { dataDir: dir, diskUsage: usage, ioTimeoutMs: 200 },
// 资源采样:只给 meminfo 与 cgroup cpu.stat,其余读不到 → 降级为 null
resourceSampler: createResourceSampler({
autoStart: false,
read: (path) => {
if (path === "/proc/meminfo") return Promise.resolve("MemTotal: 1000 kB\nMemAvailable: 250 kB\n");
if (path === "/sys/fs/cgroup/cpu.stat")
return Promise.resolve(`usage_usec ${Math.round(performance.now() * 500)}\n`);
return Promise.resolve(null);
},
}),
}),
],
ctx: h.ctx,
@@ -295,6 +306,43 @@ describe.skipIf(!hasPg)("W9-I 进程与健康(真实 PG)", () => {
expect(d.sse.connections).toBe(0);
});
it("资源监控:GET /system/resources 给当前值与趋势,读不到的字段为 null", async () => {
const res = await admin("GET", "/system/resources?minutes=30");
expect(res.status).toBe(200);
const d = SystemResources.parse((await json(res)).data);
expect(d.memory).toMatchObject({ scope: "host", usedBytes: 750 * 1024, percent: 75 });
expect(d.cpu.scope).toBe("cgroup");
expect(d.cpu.percent).not.toBeNull();
expect(d.network).toMatchObject({ scope: "none", rxBps: null });
expect(d.diskIo.scope).toBe("none");
expect(d.samples.length).toBeGreaterThanOrEqual(1);
expect((await admin("GET", "/system/resources?minutes=1")).status).toBe(422);
});
it("心跳带上本进程 CPU 占用", async () => {
const w = startHeartbeatWriter({
db: h.db,
logger: h.ctx.logger,
kind: "worker:agent",
host: "cpu-host",
pid: 7,
intervalMs: 60_000,
lag: { take: () => ({ lastMs: 0, windowMaxMs: 0, lifetimeMaxMs: 0 }), stop() {} },
});
await w.beat();
await new Promise((r) => setTimeout(r, 20));
await w.beat();
const row = await h.db
.selectFrom("process_heartbeat")
.select("extra")
.where("instance_id", "=", w.instanceId)
.executeTakeFirstOrThrow();
expect(row.extra).toMatchObject({ cpuPercent: expect.any(Number), rssBytes: expect.any(Number) });
await w.stop();
await sql`delete from process_heartbeat where instance_id = ${w.instanceId}`.execute(h.db);
await sql`delete from process_heartbeat_sample where instance_id = ${w.instanceId}`.execute(h.db);
});
it("health 各段形状:DB / 磁盘阈值 / 挂载根哨兵 / 引擎 / 定时任务", async () => {
await sql`insert into library_root (name, local_path, sentinel_file) values
('电影', ${libOk}, '.umt-root'), ('剧集(掉线)', ${join(dir, "missing")}, null)`.execute(h.db);