fix(logger): preserve multiline browser logs

This commit is contained in:
2026-06-18 16:09:56 +08:00
parent d87bfcc859
commit e70ee3e22f
+92 -9
View File
@@ -4,14 +4,15 @@ 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: warn 级别(减少 vitest 噪音)
* - development: debug 级别 + ISO 时间戳(人眼友好)
* - test/dev: debug 级别 + ISO 时间戳
* - browser: 保留原始 message 参数,避免多行日志被转义成 `\n`
*/
const rootLogger: PinoLogger = pino(createLoggerOptions());
@@ -20,10 +21,18 @@ function createLoggerOptions(): LoggerOptions {
return { level: "silent" };
}
if (AppEnvUtil.isTest()) {
return { level: "debug", timestamp: pino.stdTimeFunctions.isoTime };
return {
level: "debug",
timestamp: pino.stdTimeFunctions.isoTime,
browser: { asObjectBindingsOnly: true },
};
}
// development
return { level: "debug", timestamp: pino.stdTimeFunctions.isoTime };
return {
level: "debug",
timestamp: pino.stdTimeFunctions.isoTime,
browser: { asObjectBindingsOnly: true },
};
}
/**
@@ -43,25 +52,27 @@ function createLoggerOptions(): LoggerOptions {
*/
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.logger.debug(...Logger.normalizeLogArgs(args));
this.write("debug", args);
}
info(...args: LogArgs): void {
this.logger.info(...Logger.normalizeLogArgs(args));
this.write("info", args);
}
warn(...args: LogArgs): void {
this.logger.warn(...Logger.normalizeLogArgs(args));
this.write("warn", args);
}
error(...args: LogArgs): void {
this.logger.error(...Logger.normalizeLogArgs(args));
this.write("error", args);
}
fatal(...args: LogArgs): void {
this.logger.fatal(...Logger.normalizeLogArgs(args));
this.write("fatal", args);
}
/**
@@ -121,6 +132,78 @@ export class Logger {
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;