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:
1 parent
744252b969
commit
2ce1841628
6 files changed
+712
-2
No files matched your search
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user