带筛选(级别、关键词、类型)时原查询要对全部引擎运行算标题子查询与 search 拼接, 3 万条运行上超时。改为每个来源两段:第一段只用原始列与一次性算好的业务引用筛选并 order + limit,第二段只对取出的行算标题 / 对象标签;关键词先在业务对象小表里查出 命中 id 集合;查询内关掉 JIT。新增 3 万条运行的耗时测试与改写前口径对照测试。 Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Cxwgnn2pbzQ7rXv3rq7fei
118 lines
5.7 KiB
TypeScript
118 lines
5.7 KiB
TypeScript
import type { LogPage } from "@umt/contracts";
|
|
import { sql } from "@umt/db";
|
|
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
|
import { type LogQueryInput, logQueries } from "../../src/modules/logs/query.ts";
|
|
import { createHarness, type Harness, pgAvailable } from "../auth-settings/harness.ts";
|
|
import { legacyLogQueries } from "./legacy-query.ts";
|
|
import { seedLogVolume } from "./volume-seed.ts";
|
|
|
|
// 改写后的日志查询与改写前(先算派生列再筛选)在同一份小数据上逐条一致:筛选、关键词、对象、时间、游标分页。
|
|
|
|
const hasPg = await pgAvailable();
|
|
|
|
describe.skipIf(!hasPg)("日志查询改写前后口径一致", () => {
|
|
let h: Harness;
|
|
let feedId = "";
|
|
let downloadId = "";
|
|
let agentId = "";
|
|
|
|
beforeAll(async () => {
|
|
h = await createHarness({ engine: true });
|
|
await seedLogVolume(h.db, h.engineSchema, 400);
|
|
const wr = sql.raw(`"${h.engineSchema}".workflow_runs`);
|
|
feedId = (await sql<{ id: string }>`select id::text from feed_source where name like 'my%'`.execute(h.db)).rows[0]!
|
|
.id;
|
|
downloadId = (await sql<{ id: string }>`select id::text from download_task order by name limit 1`.execute(h.db))
|
|
.rows[0]!.id;
|
|
agentId = (
|
|
await sql<{ id: string }>`select id::text from agent_run where status = 'failed' order by goal limit 1`.execute(
|
|
h.db,
|
|
)
|
|
).rows[0]!.id;
|
|
// 没有业务 id 的运行靠业务任务行(task.engine_run_id)关联对象:一条指向失败的智能体会话、一条指向下载任务
|
|
await sql`insert into task (kind, idempotency_key, engine_run_id, subject_type, subject_id)
|
|
values ('agent', 'parity-1', 'vol-000060', 'agent_run', ${agentId}),
|
|
('download', 'parity-2', 'vol-000061', 'download_task', ${downloadId})`.execute(h.db);
|
|
// 失败的智能体会话对应的引擎运行(完成)按智能体结果算失败;定时任务的手动重跑不算定时触发
|
|
await sql`update ${wr} set input = jsonb_build_object('input', jsonb_build_object('agentRunId', ${agentId}::text))
|
|
where id = 'vol-000077'`.execute(h.db);
|
|
await sql`insert into tasks_run_meta (run_id, task_name, task_version, idempotency_key)
|
|
values ('vol-000089', 'purgeTrash', 1, 'manual:1')`.execute(h.db);
|
|
// 审计里指向下载任务的一条(按对象筛选时与任务一起列出)
|
|
await sql`insert into audit_event (actor_type, actor_id, action, target_type, target_id, payload)
|
|
values ('agent', 'run-1', 'capability.denied', 'download_task', ${downloadId}, '{"reason":"超出授权"}'::jsonb)`.execute(
|
|
h.db,
|
|
);
|
|
}, 120_000);
|
|
afterAll(async () => {
|
|
await h?.close();
|
|
});
|
|
|
|
const at = (hoursAgo: number) => new Date(Date.now() - hoursAgo * 3600_000).toISOString();
|
|
const filters = (): [string, Omit<LogQueryInput, "limit">][] => [
|
|
["全部", {}],
|
|
["含例行", { system: "1" }],
|
|
["error", { level: ["error"] }],
|
|
["warn", { level: ["warn"] }],
|
|
["success", { level: ["success"] }],
|
|
["running + info", { level: ["running", "info"] }],
|
|
["error 含例行", { level: ["error"], system: "1" }],
|
|
["task", { type: ["task"] }],
|
|
["task 含例行", { type: ["task"], system: "1" }],
|
|
["auto", { type: ["auto"], system: "1" }],
|
|
["auth", { type: ["auth"] }],
|
|
["audit", { type: ["audit"] }],
|
|
["task + auth + warn", { type: ["task", "auth"], level: ["warn"] }],
|
|
["近 6 小时", { from: at(6), system: "1" }],
|
|
["6 到 2 小时前", { from: at(6), to: at(2) }],
|
|
["q 业务标题", { q: "feeds", system: "1" }],
|
|
["q 下载名", { q: "下载 1" }],
|
|
["q 智能体目标", { q: "调查 1", system: "1" }],
|
|
["q 运行 id", { q: "VOL-00012", system: "1" }],
|
|
["q 工作流名", { q: "ingest" }],
|
|
["q 错误", { q: "失败原因 9", system: "1" }],
|
|
["q 审计负载", { q: "10.1.0.5" }],
|
|
["q 定时任务", { q: "超时", system: "1" }],
|
|
["q 例行定时", { q: "清理回收站", system: "1" }],
|
|
["q 字面 %", { q: "%" }],
|
|
["q 不命中", { q: "zz-nomatch", system: "1" }],
|
|
["对象 feed", { objectType: "feed_source", objectId: feedId }],
|
|
["对象 下载(只给 id)", { objectId: downloadId }],
|
|
["对象 下载 + 类型", { objectType: "download_task", objectId: downloadId }],
|
|
["对象 定时任务", { objectType: "scheduled_job", objectId: "trash.purge" }],
|
|
["对象 智能体", { objectType: "agent_run", objectId: agentId }],
|
|
["对象类型不存在", { objectType: "nope" }],
|
|
];
|
|
|
|
it("各种筛选一次取全:逐条一致", async () => {
|
|
const next = logQueries(h.db, { engineSchema: h.engineSchema });
|
|
const old = legacyLogQueries(h.db, { engineSchema: h.engineSchema });
|
|
const empty: string[] = [];
|
|
for (const [name, f] of filters()) {
|
|
const a: LogPage = await next.list({ ...f, limit: 200 });
|
|
const b: LogPage = await old.list({ ...f, limit: 200 });
|
|
expect(a, name).toEqual(b);
|
|
if (!a.items.length) empty.push(name);
|
|
}
|
|
// 对照有意义:除了故意不命中的,筛选都有结果
|
|
expect(empty).toEqual(["q 字面 %", "q 不命中", "对象类型不存在"]);
|
|
}, 120_000);
|
|
|
|
it("游标分页:每一页与改写前一致", async () => {
|
|
const next = logQueries(h.db, { engineSchema: h.engineSchema });
|
|
const old = legacyLogQueries(h.db, { engineSchema: h.engineSchema });
|
|
for (const f of [{ system: "1" as const }, { level: ["error" as const] }, { q: "下载", system: "1" as const }]) {
|
|
let cursor: string | null = null;
|
|
let pages = 0;
|
|
do {
|
|
const a: LogPage = await next.list({ ...f, limit: 37, cursor });
|
|
const b: LogPage = await old.list({ ...f, limit: 37, cursor });
|
|
expect(a).toEqual(b);
|
|
cursor = a.nextCursor;
|
|
pages++;
|
|
} while (cursor && pages < 40);
|
|
expect(pages).toBeGreaterThan(1);
|
|
}
|
|
}, 120_000);
|
|
});
|