feat: 前端统一 Logger 模块 — 接口、双流 Sink、ESLint 规则、测试
This commit is contained in:
@@ -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() {
|
||||
|
||||
14
src/web/hooks/use-logger.ts
Normal file
14
src/web/hooks/use-logger.ts
Normal 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
163
src/web/utils/logger.ts
Normal 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("");
|
||||
}
|
||||
Reference in New Issue
Block a user