300 lines
10 KiB
TypeScript
Executable File
300 lines
10 KiB
TypeScript
Executable File
// The Logs dashboard trail, end-to-end through createHandler.
|
|
//
|
|
// Regression cover for two defects found on a live deployment (2026-07-25):
|
|
//
|
|
// 1. NO producer populated provider/model/token/cost on a LogEntry. The
|
|
// request-logger middleware is the only publisher and it sees just
|
|
// method/path/status, so the Logs view rendered "N/A" for every inference
|
|
// dimension no matter how much real traffic flowed. On the old code every
|
|
// assertion about ctx.logBus entry.model / totalTokens / costMicroUsd
|
|
// below fails with `undefined`.
|
|
// 2. Machine probes (/healthz, /metrics) were 4982 of 5000 stored entries,
|
|
// so the capped trail pruned real requests away. On the old code the
|
|
// health-probe exclusion test below fails, because the probe is stored.
|
|
|
|
import { assert, assertEquals } from "@std/assert";
|
|
import { createHandler } from "../../apps/gateway/main.ts";
|
|
import {
|
|
type AppContext,
|
|
createLogEnrichment,
|
|
NullToolExecutor,
|
|
VERSION,
|
|
} from "../../apps/gateway/context.ts";
|
|
import { ProviderManager } from "../../packages/providers/src/mod.ts";
|
|
import { Metrics } from "../../packages/telemetry/src/metrics.ts";
|
|
import {
|
|
LogBus,
|
|
makePathExcluder,
|
|
} from "../../packages/telemetry/src/logbus.ts";
|
|
import { LogStore } from "../../packages/telemetry/src/logstore.ts";
|
|
import { UsageTracker } from "../../packages/telemetry/src/usagestore.ts";
|
|
import type { StateStore } from "../../packages/config/src/store.ts";
|
|
import { MemoryStateStore } from "../../packages/config/src/store_memory.ts";
|
|
import { VirtualKeyManager } from "../../packages/governance/src/virtual_keys.ts";
|
|
import { GovernanceHierarchy } from "../../packages/governance/src/hierarchy.ts";
|
|
import { PricingCatalog } from "../../packages/governance/src/pricing.ts";
|
|
import { MCPRegistry } from "../../packages/mcp/src/registry.ts";
|
|
import { PluginManager } from "../../packages/plugins/src/lifecycle.ts";
|
|
import {
|
|
jsonResponse,
|
|
MockProvider,
|
|
openAIChatBody,
|
|
sseResponse,
|
|
} from "../../packages/testing/src/mod.ts";
|
|
|
|
interface Harness {
|
|
ctx: AppContext;
|
|
handler: (req: Request) => Promise<Response>;
|
|
close: () => void;
|
|
}
|
|
|
|
/**
|
|
* Wires the log trail exactly as the composition root does, so these tests
|
|
* exercise the production sink rather than a re-implementation of it.
|
|
*/
|
|
async function makeHarness(
|
|
mockUrl: string,
|
|
opts: { store?: boolean; excludePaths?: string } = {},
|
|
): Promise<Harness> {
|
|
const metrics = new Metrics();
|
|
const pricing = new PricingCatalog();
|
|
metrics.setKnownModels(pricing.modelKeys());
|
|
|
|
const logBus = new LogBus();
|
|
let configStore: StateStore | undefined;
|
|
let logStore: LogStore | undefined;
|
|
if (opts.store) {
|
|
const dir = await Deno.makeTempDir();
|
|
configStore = MemoryStateStore.named(`${dir}/logs.kv`);
|
|
logStore = new LogStore(configStore);
|
|
}
|
|
|
|
const ctx: AppContext = {
|
|
providers: new ProviderManager([{
|
|
id: "openai",
|
|
type: "openai",
|
|
apiKey: "sk-test",
|
|
baseUrl: mockUrl,
|
|
enabled: true,
|
|
models: ["gpt-4o"],
|
|
priority: 0,
|
|
retry: { maxRetries: 0 },
|
|
}], "openai"),
|
|
metrics,
|
|
logBus,
|
|
logStore,
|
|
logEnrichment: createLogEnrichment(logBus, logStore),
|
|
logExcludedPath: makePathExcluder(opts.excludePaths),
|
|
usage: new UsageTracker(),
|
|
virtualKeys: new VirtualKeyManager(),
|
|
hierarchy: new GovernanceHierarchy(),
|
|
pricing,
|
|
mcp: new MCPRegistry(),
|
|
plugins: new PluginManager(),
|
|
toolExecutor: new NullToolExecutor(),
|
|
version: VERSION,
|
|
};
|
|
|
|
return {
|
|
ctx,
|
|
handler: createHandler(ctx),
|
|
close: () => configStore?.close(),
|
|
};
|
|
}
|
|
|
|
function chat(body: unknown): Request {
|
|
return new Request("http://gw.test/v1/chat/completions", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify(body),
|
|
});
|
|
}
|
|
|
|
const CHAT_BODY = {
|
|
model: "openai/gpt-4o",
|
|
messages: [{ role: "user", content: "hi" }],
|
|
};
|
|
|
|
/** The log entry for the inference request (probes are excluded by default). */
|
|
function inferenceEntry(ctx: AppContext) {
|
|
return ctx.logBus.recent(50).find((e) => e.path === "/v1/chat/completions");
|
|
}
|
|
|
|
Deno.test("log trail: a non-streamed completion records provider, model, tokens, cost", async () => {
|
|
const mock = new MockProvider(() =>
|
|
jsonResponse(openAIChatBody("hi", {
|
|
model: "gpt-4o",
|
|
usage: {
|
|
prompt_tokens: 1000,
|
|
completion_tokens: 500,
|
|
total_tokens: 1500,
|
|
},
|
|
}))
|
|
);
|
|
const h = await makeHarness(mock.url);
|
|
try {
|
|
const res = await h.handler(chat(CHAT_BODY));
|
|
assertEquals(res.status, 200);
|
|
await res.body?.cancel();
|
|
|
|
const entry = inferenceEntry(h.ctx);
|
|
assert(entry, "the inference request must be logged");
|
|
assertEquals(entry.provider, "openai");
|
|
assertEquals(entry.model, "gpt-4o");
|
|
assertEquals(entry.promptTokens, 1000);
|
|
assertEquals(entry.completionTokens, 500);
|
|
assertEquals(entry.totalTokens, 1500);
|
|
assert(
|
|
typeof entry.costMicroUsd === "number",
|
|
"a priced model records integer micro-USD cost",
|
|
);
|
|
// Base fields are untouched by enrichment.
|
|
assertEquals(entry.status, 200);
|
|
assertEquals(entry.method, "POST");
|
|
assert(typeof entry.durationMs === "number");
|
|
} finally {
|
|
await mock.close();
|
|
h.close();
|
|
}
|
|
});
|
|
|
|
Deno.test("log trail: a streamed completion is patched in place when usage arrives", async () => {
|
|
const frames = [
|
|
'data: {"model":"gpt-4o","choices":[{"delta":{"content":"hi"}}]}\n\n',
|
|
'data: {"model":"gpt-4o","choices":[{"delta":{},"finish_reason":"stop"}],' +
|
|
'"usage":{"prompt_tokens":20,"completion_tokens":10,"total_tokens":30}}\n\n',
|
|
"data: [DONE]\n\n",
|
|
];
|
|
const mock = new MockProvider(() => sseResponse(frames));
|
|
const h = await makeHarness(mock.url);
|
|
try {
|
|
const res = await h.handler(chat({ ...CHAT_BODY, stream: true }));
|
|
assertEquals(res.status, 200);
|
|
|
|
// The entry is published while the body is still unread, so at this point
|
|
// the request logger has emitted with no usage available.
|
|
const beforeDrain = inferenceEntry(h.ctx);
|
|
assert(beforeDrain, "the streamed request is logged immediately");
|
|
assertEquals(beforeDrain.totalTokens, undefined);
|
|
assert(typeof beforeDrain.durationMs === "number");
|
|
|
|
// Draining the stream flushes the usage tap, which patches the entry.
|
|
await res.text();
|
|
|
|
const entry = inferenceEntry(h.ctx);
|
|
assertEquals(entry?.model, "gpt-4o");
|
|
assertEquals(entry?.promptTokens, 20);
|
|
assertEquals(entry?.completionTokens, 10);
|
|
assertEquals(entry?.totalTokens, 30);
|
|
// Patched in place: one row per request, not a duplicate.
|
|
assertEquals(
|
|
h.ctx.logBus.recent(50).filter((e) => e.path === "/v1/chat/completions")
|
|
.length,
|
|
1,
|
|
);
|
|
} finally {
|
|
await mock.close();
|
|
h.close();
|
|
}
|
|
});
|
|
|
|
Deno.test("log trail: the durable store carries the same enrichment", async () => {
|
|
const mock = new MockProvider(() =>
|
|
jsonResponse(openAIChatBody("hi", {
|
|
model: "gpt-4o",
|
|
usage: { prompt_tokens: 7, completion_tokens: 3, total_tokens: 10 },
|
|
}))
|
|
);
|
|
const h = await makeHarness(mock.url, { store: true });
|
|
try {
|
|
await (await h.handler(chat(CHAT_BODY))).body?.cancel();
|
|
|
|
const { entries } = await h.ctx.logStore!.query({ limit: 50 });
|
|
const stored = entries.find((e) => e.path === "/v1/chat/completions");
|
|
assert(stored, "the inference request reaches the durable trail");
|
|
assertEquals(stored.model, "gpt-4o");
|
|
assertEquals(stored.totalTokens, 10);
|
|
|
|
// The dimensions the UI facets read are no longer honestly-empty.
|
|
const facets = await h.ctx.logStore!.filterData();
|
|
assertEquals(facets.models, ["gpt-4o"]);
|
|
assertEquals(facets.providers, ["openai"]);
|
|
|
|
const stats = await h.ctx.logStore!.stats();
|
|
assertEquals(stats.totalTokens, 10);
|
|
} finally {
|
|
await mock.close();
|
|
h.close();
|
|
}
|
|
});
|
|
|
|
Deno.test("log trail: health probes are excluded from bus and store, not from stdout", async () => {
|
|
const mock = new MockProvider(() => jsonResponse({ ok: true }));
|
|
const h = await makeHarness(mock.url, { store: true });
|
|
try {
|
|
for (let i = 0; i < 5; i++) {
|
|
await (await h.handler(new Request("http://gw.test/healthz"))).body
|
|
?.cancel();
|
|
}
|
|
await (await h.handler(new Request("http://gw.test/metrics"))).body
|
|
?.cancel();
|
|
|
|
assertEquals(
|
|
h.ctx.logBus.recent(50).length,
|
|
0,
|
|
"probe traffic never reaches the dashboard trail",
|
|
);
|
|
assertEquals((await h.ctx.logStore!.query({ limit: 50 })).total, 0);
|
|
|
|
// A real request still lands.
|
|
await (await h.handler(new Request("http://gw.test/api/logs/dropped")))
|
|
.body?.cancel();
|
|
assertEquals(h.ctx.logBus.recent(50).length, 1);
|
|
assertEquals(h.ctx.logBus.recent(50)[0].path, "/api/logs/dropped");
|
|
} finally {
|
|
await mock.close();
|
|
h.close();
|
|
}
|
|
});
|
|
|
|
Deno.test("log trail: exclusion is overridable, so the trail can stay complete", async () => {
|
|
const mock = new MockProvider(() => jsonResponse({ ok: true }));
|
|
const h = await makeHarness(mock.url, { excludePaths: "off" });
|
|
try {
|
|
await (await h.handler(new Request("http://gw.test/healthz"))).body
|
|
?.cancel();
|
|
assertEquals(h.ctx.logBus.recent(50).length, 1);
|
|
assertEquals(h.ctx.logBus.recent(50)[0].path, "/healthz");
|
|
} finally {
|
|
await mock.close();
|
|
h.close();
|
|
}
|
|
});
|
|
|
|
Deno.test("log trail: non-inference requests gain no borrowed enrichment", async () => {
|
|
const mock = new MockProvider(() =>
|
|
jsonResponse(openAIChatBody("hi", {
|
|
model: "gpt-4o",
|
|
usage: { prompt_tokens: 5, completion_tokens: 5, total_tokens: 10 },
|
|
}))
|
|
);
|
|
const h = await makeHarness(mock.url);
|
|
try {
|
|
await (await h.handler(chat(CHAT_BODY))).body?.cancel();
|
|
await (await h.handler(new Request("http://gw.test/api/logs/dropped")))
|
|
.body?.cancel();
|
|
|
|
const admin = h.ctx.logBus.recent(50).find((e) =>
|
|
e.path === "/api/logs/dropped"
|
|
);
|
|
assert(admin);
|
|
assertEquals(admin.model, undefined);
|
|
assertEquals(admin.provider, undefined);
|
|
assertEquals(admin.totalTokens, undefined);
|
|
assertEquals(admin.costMicroUsd, undefined);
|
|
} finally {
|
|
await mock.close();
|
|
h.close();
|
|
}
|
|
});
|