diff --git a/src/observability/logger/console-transport.test.ts b/src/observability/logger/console-transport.test.ts new file mode 100644 index 00000000..a8722f9e --- /dev/null +++ b/src/observability/logger/console-transport.test.ts @@ -0,0 +1,45 @@ +import { describe, expect, it, vi } from "vitest"; +import type { LogRecord } from "./types.js"; +import { ConsoleTransport } from "./console-transport.js"; + +describe("ConsoleTransport", () => { + it("pipes debug records to console.debug", () => { + const spy = vi.spyOn(console, "debug").mockImplementation(() => {}); + const transport = new ConsoleTransport(); + const record: LogRecord = { ts: "2025-01-01T00:00:00Z", level: "debug", msg: "hello", fields: {} }; + transport.write(record); + expect(spy).toHaveBeenCalledTimes(1); + expect(spy.mock.calls[0]?.[0]).toContain("hello"); + spy.mockRestore(); + }); + + it("pipes info records to console.info", () => { + const spy = vi.spyOn(console, "info").mockImplementation(() => {}); + const transport = new ConsoleTransport(); + transport.write({ ts: "2025-01-01T00:00:00Z", level: "info", msg: "info msg", fields: { key: "val" } }); + expect(spy).toHaveBeenCalledTimes(1); + expect(spy.mock.calls[0]?.[0]).toContain("info msg"); + spy.mockRestore(); + }); + + it("pipes warn records to console.warn", () => { + const spy = vi.spyOn(console, "warn").mockImplementation(() => {}); + const transport = new ConsoleTransport(); + transport.write({ ts: "2025-01-01T00:00:00Z", level: "warn", msg: "w", fields: {} }); + expect(spy).toHaveBeenCalledTimes(1); + spy.mockRestore(); + }); + + it("pipes error records to console.error", () => { + const spy = vi.spyOn(console, "error").mockImplementation(() => {}); + const transport = new ConsoleTransport(); + transport.write({ ts: "2025-01-01T00:00:00Z", level: "error", msg: "e", fields: {} }); + expect(spy).toHaveBeenCalledTimes(1); + spy.mockRestore(); + }); + + it("flush is a no-op that resolves immediately", async () => { + const transport = new ConsoleTransport(); + await expect(transport.flush()).resolves.toBeUndefined(); + }); +}); diff --git a/src/observability/logger/console-transport.ts b/src/observability/logger/console-transport.ts new file mode 100644 index 00000000..bf58d63c --- /dev/null +++ b/src/observability/logger/console-transport.ts @@ -0,0 +1,36 @@ +import type { LogRecord, LogTransport } from "./types.js"; + +/** + * Dev-mode transport that pipes log records to the browser/Node console. + * Each record is printed as `[LEVEL] ts msg {fields}`. + */ +export class ConsoleTransport implements LogTransport { + write(record: LogRecord): void { + const prefix = `[${record.level.toUpperCase()}] ${record.ts}`; + const hasFields = Object.keys(record.fields).length > 0; + const msg = hasFields + ? `${prefix} ${record.msg} ${JSON.stringify(record.fields)}` + : `${prefix} ${record.msg}`; + + switch (record.level) { + case "debug": + // eslint-disable-next-line no-console + console.debug(msg); + break; + case "info": + // eslint-disable-next-line no-console + console.info(msg); + break; + case "warn": + console.warn(msg); + break; + case "error": + console.error(msg); + break; + } + } + + async flush(): Promise { + // Console output is synchronous — nothing to flush. + } +}