Files
UHD_Media_Tool/apps/server/test/logs/logs-volume.test.ts
T
rootandClaude Opus 5.5 2bd09c89e9 fix(logs): 日志查询先按原始列筛选取前 n 条再算标题
带筛选(级别、关键词、类型)时原查询要对全部引擎运行算标题子查询与 search 拼接,
3 万条运行上超时。改为每个来源两段:第一段只用原始列与一次性算好的业务引用筛选并
order + limit,第二段只对取出的行算标题 / 对象标签;关键词先在业务对象小表里查出
命中 id 集合;查询内关掉 JIT。新增 3 万条运行的耗时测试与改写前口径对照测试。

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Cxwgnn2pbzQ7rXv3rq7fei
2026-10-06 19:34:07 +08:00

106 lines
4.2 KiB
TypeScript

import { LogPage } from "@umt/contracts";
import { sql } from "@umt/db";
import { afterAll, beforeAll, describe, expect, it } from "vitest";
import { createLogStreamPoller, createLogsModule } from "../../src/modules/logs/index.ts";
import { logQueries } from "../../src/modules/logs/query.ts";
import { bearer, createHarness, type Harness, pgAvailable } from "../auth-settings/harness.ts";
import { DEV_KEY, json } from "../helpers.ts";
import { seedLogVolume, VOLUME_RUNS } from "./volume-seed.ts";
// 日志接口在真实数据量上的耗时(约 30,000 条引擎运行 + 定时任务运行 + 审计)。
// 改写前这几种查询在部署库上超时(> 60s);每个来源先按原始列筛选取前 n 条再算标题后,应在 1 秒内(阈值放宽到 1.5s 防抖动)。
const hasPg = await pgAvailable();
const LIMIT_MS = 1500;
describe.skipIf(!hasPg)("日志查询耗时(约 3 万条引擎运行)", () => {
let h: Harness;
let admin: ReturnType<typeof bearer>;
beforeAll(async () => {
h = await createHarness({ engine: true });
const { createApp } = await import("../../src/http/app.ts");
const { createAuthSettingsModule } = await import("../../src/modules/auth-settings/index.ts");
h.app = createApp({
config: h.ctx.config,
logger: h.ctx.logger,
bus: h.bus,
db: h.db,
role: "all",
version: "0.0.0-test",
modules: [
createAuthSettingsModule({ setupTokenOnStart: false, engineSchema: h.engineSchema }),
createLogsModule({ engineSchema: h.engineSchema }),
],
ctx: h.ctx,
}) as Harness["app"];
admin = bearer(h.app, DEV_KEY);
await seedLogVolume(h.db, h.engineSchema);
const n = await sql<{
n: number;
}>`select count(*)::int as n from ${sql.raw(`"${h.engineSchema}".workflow_runs`)}`.execute(h.db);
expect(n.rows[0]!.n).toBe(VOLUME_RUNS);
// 预热连接
expect((await admin("GET", "/logs?limit=1")).status).toBe(200);
}, 120_000);
afterAll(async () => {
await h?.close();
});
const since24h = () => encodeURIComponent(new Date(Date.now() - 24 * 3600_000).toISOString());
const cases: [string, () => string, (items: LogPage["items"]) => void][] = [
[
"level=error&since=24h",
() => `?level=error&from=${since24h()}&limit=50`,
(items) => {
expect(items.length).toBeGreaterThan(0);
expect(items.every((e) => e.level === "error")).toBe(true);
},
],
[
"system=1&q=feeds",
() => "?system=1&q=feeds&limit=50",
(items) => {
// 业务标题(RSS 源「my feeds 汇总」)命中的任务
expect(items.some((e) => e.source === "engine_run" && e.title === "my feeds 汇总")).toBe(true);
},
],
[
"type=task&level=error",
() => "?type=task&level=error&limit=50",
(items) => {
expect(items).toHaveLength(50);
expect(items.every((e) => e.type === "task" && e.level === "error")).toBe(true);
},
],
["type=task", () => "?type=task&limit=50", (items) => expect(items).toHaveLength(50)],
["默认", () => "?limit=50", (items) => expect(items).toHaveLength(50)],
// 关键词一条都不命中:引擎来源要扫完全部运行,是最坏情况
["system=1&q=<不命中>", () => "?system=1&q=zz-nomatch&limit=50", (items) => expect(items).toEqual([])],
];
for (const [name, query, check] of cases) {
it(`${name} < ${LIMIT_MS}ms`, async () => {
const t = performance.now();
const res = await admin("GET", `/logs${query()}`);
const ms = performance.now() - t;
expect(res.status).toBe(200);
const items = LogPage.parse((await json(res)).data).items;
console.log(`[logs-volume] ${name}: ${ms.toFixed(0)}ms(${items.length} 条)`);
check(items);
expect(ms).toBeLessThan(LIMIT_MS);
});
}
it(`流的一轮轮询 < ${LIMIT_MS}ms`, async () => {
const poller = createLogStreamPoller(logQueries(h.db, { engineSchema: h.engineSchema }), { system: "1" });
for (let i = 0; i < 2; i++) {
const t = performance.now();
await poller.poll();
const ms = performance.now() - t;
console.log(`[logs-volume] 流轮询 #${i + 1}: ${ms.toFixed(0)}ms`);
expect(ms).toBeLessThan(LIMIT_MS);
}
});
});