plan/react-rewrite #1

Merged
gnezim merged 138 commits from plan/react-rewrite into main 2026-04-15 12:21:16 +03:00
2 changed files with 127 additions and 0 deletions
Showing only changes of commit 1a40686490 - Show all commits
@@ -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();
});
});
+46
View File
@@ -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);
}
}