feat: 前端统一 Logger 模块 — 接口、双流 Sink、ESLint 规则、测试

This commit is contained in:
2026-06-01 14:26:17 +08:00
parent 60843f7dbf
commit 4c72754739
11 changed files with 1003 additions and 2 deletions

View File

@@ -3,6 +3,8 @@ import type { ErrorInfo, ReactNode } from "react";
import { Button, Result } from "antd";
import { Component } from "react";
import { createConsoleLogger } from "../utils/logger";
interface Props {
children: ReactNode;
}
@@ -19,7 +21,7 @@ export class ErrorBoundary extends Component<Props, State> {
}
override componentDidCatch(error: Error, info: ErrorInfo): void {
console.error("渲染错误:", error, info.componentStack);
createConsoleLogger().error("渲染错误", { componentStack: info.componentStack, error });
}
override render() {

View File

@@ -0,0 +1,14 @@
import { App } from "antd";
import { useMemo } from "react";
import type { Logger } from "../utils/logger";
import { AntdMessageSink, ConsoleSink, createDefaultLogger } from "../utils/logger";
export function useLogger(): Logger {
const { message } = App.useApp();
return useMemo(() => {
const isProduction = !!import.meta.env["PROD"];
return createDefaultLogger([new ConsoleSink(isProduction), new AntdMessageSink(message)], isProduction);
}, [message]);
}

163
src/web/utils/logger.ts Normal file
View File

@@ -0,0 +1,163 @@
import type { MessageInstance } from "antd/es/message/interface";
export type LogLevel = "debug" | "error" | "info" | "warn";
const LEVEL_ORDER: Record<LogLevel, number> = { debug: 0, error: 3, info: 1, warn: 2 };
export interface Logger {
child(bindings: Record<string, unknown>): Logger;
debug(message: string, data?: unknown): void;
error(message: string, data?: unknown): void;
info(message: string, data?: unknown): void;
setLevel(level: LogLevel): void;
warn(message: string, data?: unknown): void;
}
export interface Sink {
write(level: LogLevel, message: string, data: unknown, bindings: Record<string, unknown>): void;
}
class AntdMessageSink implements Sink {
constructor(private messageApi: MessageInstance) {}
write(level: LogLevel, message: string, _data: unknown, _bindings: Record<string, unknown>): void {
if (level === "warn") this.messageApi.warning(message);
else if (level === "error") this.messageApi.error(message);
}
}
class BaseLogger implements Logger {
private minLevel: LogLevel;
constructor(
minLevel: LogLevel,
protected sinks: Sink[],
protected bindings: Record<string, unknown>,
) {
this.minLevel = minLevel;
}
child(bindings: Record<string, unknown>): Logger {
return new BaseLogger(this.minLevel, this.sinks, { ...this.bindings, ...bindings });
}
debug(message: string, data?: unknown): void {
this.log("debug", message, data);
}
error(message: string, data?: unknown): void {
this.log("error", message, data);
}
info(message: string, data?: unknown): void {
this.log("info", message, data);
}
setLevel(level: LogLevel): void {
this.minLevel = level;
}
warn(message: string, data?: unknown): void {
this.log("warn", message, data);
}
private log(level: LogLevel, message: string, data?: unknown): void {
if (LEVEL_ORDER[level] < LEVEL_ORDER[this.minLevel]) return;
for (const sink of this.sinks) {
sink.write(level, message, data, this.bindings);
}
}
}
class ConsoleSink implements Sink {
constructor(private isProduction: boolean) {}
write(level: LogLevel, message: string, data: unknown, bindings: Record<string, unknown>): void {
if (this.isProduction && LEVEL_ORDER[level] < LEVEL_ORDER.warn) return;
const prefix = `[Alfred:${level.toUpperCase()}]`;
const bindingStr = formatBindings(bindings);
const fullMessage = `${prefix} ${message}${bindingStr}`;
if (level === "error") console.error(fullMessage, data);
else if (level === "warn") console.warn(fullMessage, data);
else console.log(fullMessage, data);
}
}
/* eslint-disable @typescript-eslint/no-empty-function */
class NoopLogger implements Logger {
child(_bindings: Record<string, unknown>): Logger {
return this;
}
debug(_message: string, _data?: unknown): void {}
error(_message: string, _data?: unknown): void {}
info(_message: string, _data?: unknown): void {}
setLevel(_level: LogLevel): void {}
warn(_message: string, _data?: unknown): void {}
}
/* eslint-enable @typescript-eslint/no-empty-function */
export class MemoryLogger implements Logger {
entries: Array<{ data?: unknown; level: LogLevel; message: string }> = [];
child(bindings: Record<string, unknown>): Logger {
void bindings;
return this;
}
debug(message: string, data?: unknown): void {
this.capture("debug", message, data);
}
error(message: string, data?: unknown): void {
this.capture("error", message, data);
}
info(message: string, data?: unknown): void {
this.capture("info", message, data);
}
/* eslint-disable-next-line @typescript-eslint/no-empty-function */
setLevel(_level: LogLevel): void {}
warn(message: string, data?: unknown): void {
this.capture("warn", message, data);
}
private capture(level: LogLevel, message: string, data?: unknown): void {
this.entries.push({ data, level, message });
}
}
export function createConsoleLogger(): Logger {
const isProduction = !!import.meta.env["PROD"];
const minLevel: LogLevel = isProduction ? "warn" : "debug";
return new BaseLogger(minLevel, [new ConsoleSink(isProduction)], {});
}
export function createDefaultLogger(sinks: Sink[], isProduction: boolean): Logger {
const minLevel: LogLevel = isProduction ? "warn" : "debug";
return new BaseLogger(minLevel, sinks, {});
}
export function createMemoryLogger(): MemoryLogger {
return new MemoryLogger();
}
export { AntdMessageSink, ConsoleSink };
export function createNoopLogger(): Logger {
return new NoopLogger();
}
function formatBindings(bindings: Record<string, unknown>): string {
const entries = Object.entries(bindings);
if (entries.length === 0) return "";
return " " + entries.map(([k, v]) => `[${k}=${String(v)}]`).join("");
}