Add LoggerImpl with transport dispatch and child context propagation
This commit is contained in:
@@ -0,0 +1,81 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import type { LogRecord, LogTransport } from "./types.js";
|
||||
import { LoggerImpl } from "./logger-impl.js";
|
||||
|
||||
function mockTransport(): LogTransport & { records: LogRecord[] } {
|
||||
const records: LogRecord[] = [];
|
||||
return {
|
||||
records,
|
||||
write(record: LogRecord) { records.push(record); },
|
||||
flush: vi.fn(async () => {}),
|
||||
};
|
||||
}
|
||||
|
||||
describe("LoggerImpl", () => {
|
||||
it("writes a record at each log level", () => {
|
||||
const t = mockTransport();
|
||||
const logger = new LoggerImpl(t);
|
||||
|
||||
logger.debug("d");
|
||||
logger.info("i");
|
||||
logger.warn("w");
|
||||
logger.error("e");
|
||||
|
||||
expect(t.records).toHaveLength(4);
|
||||
expect(t.records.map(r => r.level)).toEqual(["debug", "info", "warn", "error"]);
|
||||
expect(t.records.map(r => r.msg)).toEqual(["d", "i", "w", "e"]);
|
||||
});
|
||||
|
||||
it("includes fields in the record", () => {
|
||||
const t = mockTransport();
|
||||
const logger = new LoggerImpl(t);
|
||||
logger.info("msg", { userId: 123, action: "click" });
|
||||
expect(t.records[0]?.fields).toEqual({ userId: 123, action: "click" });
|
||||
});
|
||||
|
||||
it("includes an ISO timestamp", () => {
|
||||
const t = mockTransport();
|
||||
const logger = new LoggerImpl(t);
|
||||
logger.info("msg");
|
||||
expect(t.records[0]?.ts).toMatch(/^\d{4}-\d{2}-\d{2}T/);
|
||||
});
|
||||
|
||||
it("child() propagates context fields to all records", () => {
|
||||
const t = mockTransport();
|
||||
const logger = new LoggerImpl(t);
|
||||
const child = logger.child({ traceId: "abc", locale: "ru" });
|
||||
child.info("hello", { extra: true });
|
||||
|
||||
expect(t.records[0]?.fields).toEqual({
|
||||
traceId: "abc",
|
||||
locale: "ru",
|
||||
extra: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("child of child merges contexts", () => {
|
||||
const t = mockTransport();
|
||||
const logger = new LoggerImpl(t);
|
||||
const child1 = logger.child({ traceId: "abc" });
|
||||
const child2 = child1.child({ requestId: "123" });
|
||||
child2.info("deep");
|
||||
|
||||
expect(t.records[0]?.fields).toEqual({
|
||||
traceId: "abc",
|
||||
requestId: "123",
|
||||
});
|
||||
});
|
||||
|
||||
it("error level includes err field if provided", () => {
|
||||
const t = mockTransport();
|
||||
const logger = new LoggerImpl(t);
|
||||
const err = new Error("boom");
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
logger.error("failed", { err, op: "fetch" } as any);
|
||||
|
||||
const record = t.records[0];
|
||||
expect(record?.fields?.["op"]).toBe("fetch");
|
||||
// The err field should be serialized — at minimum the message
|
||||
expect(record?.fields?.["err"]).toBeDefined();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,46 @@
|
||||
import type { Logger, LogFields, LogLevel, LogRecord, LogTransport } from "./types.js";
|
||||
|
||||
export class LoggerImpl implements Logger {
|
||||
private readonly transport: LogTransport;
|
||||
private readonly context: LogFields;
|
||||
|
||||
constructor(transport: LogTransport, context: LogFields = {}) {
|
||||
this.transport = transport;
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
debug(msg: string, fields?: LogFields): void {
|
||||
this.write("debug", msg, fields);
|
||||
}
|
||||
|
||||
info(msg: string, fields?: LogFields): void {
|
||||
this.write("info", msg, fields);
|
||||
}
|
||||
|
||||
warn(msg: string, fields?: LogFields): void {
|
||||
this.write("warn", msg, fields);
|
||||
}
|
||||
|
||||
error(msg: string, fields?: LogFields & { err?: Error }): void {
|
||||
const { err, ...rest } = fields ?? {};
|
||||
const serialized: LogFields = { ...rest };
|
||||
if (err) {
|
||||
serialized["err"] = `${err.name}: ${err.message}`;
|
||||
}
|
||||
this.write("error", msg, serialized);
|
||||
}
|
||||
|
||||
child(context: LogFields): Logger {
|
||||
return new LoggerImpl(this.transport, { ...this.context, ...context });
|
||||
}
|
||||
|
||||
private write(level: LogLevel, msg: string, fields?: LogFields): void {
|
||||
const record: LogRecord = {
|
||||
ts: new Date().toISOString(),
|
||||
level,
|
||||
msg,
|
||||
fields: { ...this.context, ...fields },
|
||||
};
|
||||
this.transport.write(record);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user