212 lines
5.6 KiB
TypeScript
212 lines
5.6 KiB
TypeScript
import pino, { type Logger as PinoLogger, type LoggerOptions } from "pino";
|
||
|
||
import { AppEnvUtil } from "./app-env";
|
||
|
||
type LogArgs = unknown[];
|
||
type PinoLogArgs = Parameters<PinoLogger["debug"]>;
|
||
type LogLevel = "debug" | "info" | "warn" | "error" | "fatal";
|
||
|
||
/**
|
||
* 全局 pino root logger(单例,进程级复用)。
|
||
*
|
||
* 配置驱动(基于 `app-env.ts` 的 `AppEnvUtil`):
|
||
* - production: silent(不输出日志)
|
||
* - test/dev: debug 级别 + ISO 时间戳
|
||
* - browser: 保留原始 message 参数,避免多行日志被转义成 `\n`
|
||
*/
|
||
const rootLogger: PinoLogger = pino(createLoggerOptions());
|
||
|
||
function createLoggerOptions(): LoggerOptions {
|
||
if (!AppEnvUtil.canOutputLogs()) {
|
||
return { level: "silent" };
|
||
}
|
||
if (AppEnvUtil.isTest()) {
|
||
return {
|
||
level: "debug",
|
||
timestamp: pino.stdTimeFunctions.isoTime,
|
||
browser: { asObjectBindingsOnly: true },
|
||
};
|
||
}
|
||
// development
|
||
return {
|
||
level: "debug",
|
||
timestamp: pino.stdTimeFunctions.isoTime,
|
||
browser: { asObjectBindingsOnly: true },
|
||
};
|
||
}
|
||
|
||
/**
|
||
* Logger 工具类(pino 包装)
|
||
*
|
||
* 设计:
|
||
* - 构造时绑定 `component` 名(业务模块标识)
|
||
* - 所有日志自动带 `{ component: "AuthRepository" }` 字段
|
||
* - 透传 pino 原生 5 个 level:debug / info / warn / error / fatal
|
||
*
|
||
* 用法:
|
||
* const log = new Logger("AuthRepository");
|
||
* log.info("login success");
|
||
* log.warn({ err }, "storage write failed");
|
||
* log.error(e, "consumer error");
|
||
* log.debug({ userId: 123 }, "fetched user");
|
||
*/
|
||
export class Logger {
|
||
private readonly logger: PinoLogger;
|
||
private readonly component: string;
|
||
|
||
constructor(component: string) {
|
||
this.component = component;
|
||
this.logger = rootLogger.child({ component });
|
||
}
|
||
|
||
debug(...args: LogArgs): void {
|
||
this.write("debug", args);
|
||
}
|
||
info(...args: LogArgs): void {
|
||
this.write("info", args);
|
||
}
|
||
warn(...args: LogArgs): void {
|
||
this.write("warn", args);
|
||
}
|
||
error(...args: LogArgs): void {
|
||
this.write("error", args);
|
||
}
|
||
fatal(...args: LogArgs): void {
|
||
this.write("fatal", args);
|
||
}
|
||
|
||
/**
|
||
* 把日志中的任意数据转成人眼友好的多行文本。
|
||
* - object / array: pretty JSON
|
||
* - JSON string: parse 后 pretty JSON
|
||
* - FormData / File: 使用轻量描述,避免直接输出二进制
|
||
*/
|
||
static formatValue(value: unknown): string {
|
||
if (value === undefined || value === null) return "";
|
||
|
||
if (typeof FormData !== "undefined" && value instanceof FormData) {
|
||
const entries: string[] = [];
|
||
value.forEach((entryValue, key) => {
|
||
if (typeof File !== "undefined" && entryValue instanceof File) {
|
||
entries.push(`${key}=File(${entryValue.name})`);
|
||
} else {
|
||
entries.push(`${key}=${String(entryValue)}`);
|
||
}
|
||
});
|
||
return `FormData{\n ${entries.join(",\n ")}\n}`;
|
||
}
|
||
|
||
if (typeof value === "string") {
|
||
const trimmed = value.trim();
|
||
if (trimmed.length === 0) return "";
|
||
try {
|
||
return JSON.stringify(JSON.parse(trimmed), null, 2);
|
||
} catch {
|
||
return value;
|
||
}
|
||
}
|
||
|
||
try {
|
||
return JSON.stringify(value, null, 2);
|
||
} catch {
|
||
return String(value);
|
||
}
|
||
}
|
||
|
||
private static normalizeLogArgs(args: LogArgs): PinoLogArgs {
|
||
const [first, second, ...rest] = args;
|
||
if (typeof first !== "string" || second === undefined) return args as PinoLogArgs;
|
||
|
||
if (second instanceof Error) {
|
||
return [{ err: second }, first, ...rest] as PinoLogArgs;
|
||
}
|
||
|
||
if (
|
||
typeof second === "object" &&
|
||
second !== null &&
|
||
!Array.isArray(second)
|
||
) {
|
||
return [second, first, ...rest] as PinoLogArgs;
|
||
}
|
||
|
||
return args as PinoLogArgs;
|
||
}
|
||
|
||
private write(level: LogLevel, args: LogArgs): void {
|
||
if (Logger.shouldUseBrowserConsole()) {
|
||
Logger.writeBrowserConsole(level, this.component, args);
|
||
return;
|
||
}
|
||
|
||
switch (level) {
|
||
case "debug":
|
||
this.logger.debug(...Logger.normalizeLogArgs(args));
|
||
return;
|
||
case "info":
|
||
this.logger.info(...Logger.normalizeLogArgs(args));
|
||
return;
|
||
case "warn":
|
||
this.logger.warn(...Logger.normalizeLogArgs(args));
|
||
return;
|
||
case "error":
|
||
this.logger.error(...Logger.normalizeLogArgs(args));
|
||
return;
|
||
case "fatal":
|
||
this.logger.fatal(...Logger.normalizeLogArgs(args));
|
||
return;
|
||
}
|
||
}
|
||
|
||
private static shouldUseBrowserConsole(): boolean {
|
||
return typeof window !== "undefined" && AppEnvUtil.canOutputLogs();
|
||
}
|
||
|
||
private static writeBrowserConsole(
|
||
level: LogLevel,
|
||
component: string,
|
||
args: LogArgs,
|
||
): void {
|
||
const { message, data } = Logger.toBrowserConsolePayload(args);
|
||
const prefix = `${new Date().toISOString()} ${level.toUpperCase()} [${component}]`;
|
||
const method = level === "fatal" ? "error" : level;
|
||
|
||
if (data.length === 0) {
|
||
console[method](`${prefix} ${message}`);
|
||
return;
|
||
}
|
||
|
||
console[method](`${prefix} ${message}`, ...data);
|
||
}
|
||
|
||
private static toBrowserConsolePayload(args: LogArgs): {
|
||
message: string;
|
||
data: unknown[];
|
||
} {
|
||
const [first, second, ...rest] = args;
|
||
|
||
if (typeof first === "string") {
|
||
return {
|
||
message: first,
|
||
data: second === undefined ? rest : [second, ...rest],
|
||
};
|
||
}
|
||
|
||
if (typeof second === "string") {
|
||
return {
|
||
message: second,
|
||
data: [first, ...rest],
|
||
};
|
||
}
|
||
|
||
return {
|
||
message: "",
|
||
data: args,
|
||
};
|
||
}
|
||
|
||
/** 透传 raw pino logger(高级用法:自定义 binding / child / serializers) */
|
||
get raw(): PinoLogger {
|
||
return this.logger;
|
||
}
|
||
}
|