import { describe, expect, spyOn, test } from "bun:test"; import { logKubernetesRequest, logServerRequest, processLogger, } from "../../lib/request-log"; describe("process request lines", () => { test("hides health GET access lines while preserving events and in-scope Kubernetes lines", async () => { const output = spyOn(console, "log").mockImplementation(() => {}); const logs: Record[] = []; const logger = { log(entry: Record) { logs.push(entry); processLogger.log(entry); }, }; try { await logServerRequest( new Request("https://kuber.astrxl.dev/api/v2/health?probe=ready"), "health-request", async () => { await logKubernetesRequest( { method: "GET", pathname: "/api/v1/health-dependency" }, async () => "ok", ); return new Response(null, { status: 200 }); }, logger, ); await expect( logServerRequest( new Request("https://kuber.astrxl.dev/api/v2/health?probe=failed"), "failed-health-request", () => { throw new Error("unavailable"); }, logger, ), ).rejects.toThrow("unavailable"); await logServerRequest( new Request("https://kuber.astrxl.dev/api/v2/health"), "default-health-request", () => new Response(null, { status: 200 }), ); await logServerRequest( new Request("https://kuber.astrxl.dev/api/v2/health", { method: "POST", }), "post-health-request", () => new Response(null, { status: 405 }), ); await logServerRequest( new Request("https://kuber.astrxl.dev/api/v2/other"), "other-request", () => new Response(null, { status: 200 }), ); expect(output.mock.calls.map(([line]) => line)).toEqual([ " GET /api/v1/health-dependency 200", "PST /api/v2/health 405", "GET /api/v2/other 200", ]); expect(logs).toContainEqual( expect.objectContaining({ event: "kuber.server.request.end", requestId: "health-request", method: "GET", pathname: "/api/v2/health", status: 200, }), ); expect(logs).toContainEqual( expect.objectContaining({ event: "kuber.server.request.failed", requestId: "failed-health-request", pathname: "/api/v2/health", }), ); } finally { output.mockRestore(); } }); test("prints one application line with pathname and completion status", async () => { const output = spyOn(console, "log").mockImplementation(() => {}); try { await logServerRequest( new Request("https://kuber.astrxl.dev/path/to/endpoint?token=secret"), "get-request", () => new Response(null, { status: 404 }), ); await logServerRequest( new Request("https://kuber.astrxl.dev/path/to/another?key=value", { method: "POST", }), "post-request", () => new Response(null, { status: 200 }), ); expect(output.mock.calls.map(([line]) => line)).toEqual([ "GET /path/to/endpoint 404", "PST /path/to/another 200", ]); } finally { output.mockRestore(); } }); test("indents Kubernetes lines for incoming requests without a CLI marker", async () => { const output = spyOn(console, "log").mockImplementation(() => {}); try { await logServerRequest( new Request("https://kuber.astrxl.dev/workloads", { headers: { "user-agent": "external-client" }, }), "workloads-request", async () => { await logKubernetesRequest( { method: "GET", pathname: "/path/to/kubernetes/api" }, async () => "ok", ); await logKubernetesRequest( { method: "POST", pathname: "/api/v1/pods" }, async () => "created", ); return new Response(null, { status: 200 }); }, ); expect(output.mock.calls.map(([line]) => line)).toEqual([ " GET /path/to/kubernetes/api 200", " PST /api/v1/pods 200", "GET /workloads 200", ]); } finally { output.mockRestore(); } }); test("keeps full methods in structured events and marks failures", async () => { const logs: Record[] = []; await logServerRequest( new Request("https://kuber.astrxl.dev/items?q=private", { method: "DELETE", }), "delete-request", () => new Response(null, { status: 204 }), { log: (entry) => logs.push(entry) }, ); expect(logs).toContainEqual( expect.objectContaining({ event: "kuber.server.request.end", method: "DELETE", pathname: "/items", status: 204, }), ); const output = spyOn(console, "log").mockImplementation(() => {}); try { await expect( logServerRequest( new Request("https://kuber.astrxl.dev/failing"), "failing-request", async () => { await logKubernetesRequest( { method: "CUSTOM", pathname: "/api/v1/fail" }, async () => { throw new Error("unavailable"); }, ); }, ), ).rejects.toThrow("unavailable"); expect(output.mock.calls.map(([line]) => line)).toEqual([ " CUS /api/v1/fail ERR", "GET /failing ERR", ]); processLogger.log({ event: "kuber.server.request.end", method: "X", pathname: "/short", status: 200, }); expect(output.mock.calls[2]?.[0]).toBe("X__ /short 200"); } finally { output.mockRestore(); } }); test("isolates concurrent requests from background work and closes completed contexts", async () => { const output = spyOn(console, "log").mockImplementation(() => {}); const events: Record[] = []; let releaseFirst!: () => void; const firstGate = new Promise((resolve) => (releaseFirst = resolve)); let releaseSecond!: () => void; const secondGate = new Promise((resolve) => (releaseSecond = resolve)); let startedFirst!: () => void; const firstStarted = new Promise((resolve) => (startedFirst = resolve)); let startedSecond!: () => void; const secondStarted = new Promise( (resolve) => (startedSecond = resolve), ); let releaseDetached!: () => void; const detachedGate = new Promise( (resolve) => (releaseDetached = resolve), ); let detached!: Promise; const customLogger = { log: (entry: Record) => events.push(entry), }; try { const first = logServerRequest( new Request("https://kuber.astrxl.dev/first"), "first", async () => { detached = (async () => { await detachedGate; return logKubernetesRequest( { method: "GET", pathname: "/detached" }, async () => "done", ); })(); startedFirst(); await firstGate; await logKubernetesRequest( { method: "PUT", pathname: "/first-k8s" }, async () => "done", ); return new Response(null, { status: 200 }); }, ); await firstStarted; const second = logServerRequest( new Request("https://kuber.astrxl.dev/second"), "second", async () => { startedSecond(); await secondGate; await logKubernetesRequest( { method: "GET", pathname: "/second-k8s" }, async () => "done", ); return new Response(null, { status: 200 }); }, ); await secondStarted; await logKubernetesRequest( { method: "GET", pathname: "/background" }, async () => "done", customLogger, ); await logKubernetesRequest( { method: "GET", pathname: "/background-default" }, async () => "done", ); await expect( logKubernetesRequest( { method: "DELETE", pathname: "/background-failed" }, async () => { throw new Error("background failure"); }, ), ).rejects.toThrow("background failure"); expect(events.map((entry) => entry.event)).toEqual([ "kuber.k8s.request.start", "kuber.k8s.request.end", ]); releaseFirst(); await first; releaseDetached(); await detached; releaseSecond(); await second; await logKubernetesRequest( { method: "GET", pathname: "/after" }, async () => "done", ); expect(output.mock.calls.map(([line]) => line)).toEqual([ " PUT /first-k8s 200", "GET /first 200", " GET /second-k8s 200", "GET /second 200", ]); } finally { releaseFirst(); releaseSecond(); releaseDetached(); output.mockRestore(); } }); }); describe("server response logging", () => { test("does not consume streaming response bodies", async () => { let pulls = 0; const body = new ReadableStream({ pull(controller) { pulls += 1; controller.enqueue(new TextEncoder().encode('{"message":"live"}\n')); }, }); const logs: Record[] = []; const response = new Response(body, { headers: { "content-type": "application/x-ndjson; charset=utf-8" }, }); await Bun.sleep(0); const pullsBeforeLogging = pulls; const loggedResponse = await logServerRequest( new Request("https://kuber.astrxl.dev/logs"), "streaming-response", () => response, { log: (entry) => logs.push(entry) }, ); expect(pulls).toBe(pullsBeforeLogging); expect(logs).toContainEqual( expect.objectContaining({ event: "kuber.server.response.body", bodySkipped: true, streaming: true, }), ); const reader = loggedResponse.body!.getReader(); expect(new TextDecoder().decode((await reader.read()).value)).toBe( '{"message":"live"}\n', ); await reader.cancel(); }); test("logs bounded non-streaming response bodies", async () => { const logs: Record[] = []; await logServerRequest( new Request("https://kuber.astrxl.dev/health"), "bounded-response", () => new Response("healthy", { headers: { "content-length": "7", "content-type": "text/plain; charset=utf-8", }, }), { log: (entry) => logs.push(entry) }, ); await Bun.sleep(0); expect(logs).toContainEqual( expect.objectContaining({ event: "kuber.server.response.body", body: { encoding: "base64", data: "aGVhbHRoeQ==" }, }), ); }); });