fix(logger): preserve multiline browser logs
This commit is contained in:
+92
-9
@@ -4,14 +4,15 @@ import { AppEnvUtil } from "./app-env";
|
|||||||
|
|
||||||
type LogArgs = unknown[];
|
type LogArgs = unknown[];
|
||||||
type PinoLogArgs = Parameters<PinoLogger["debug"]>;
|
type PinoLogArgs = Parameters<PinoLogger["debug"]>;
|
||||||
|
type LogLevel = "debug" | "info" | "warn" | "error" | "fatal";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 全局 pino root logger(单例,进程级复用)。
|
* 全局 pino root logger(单例,进程级复用)。
|
||||||
*
|
*
|
||||||
* 配置驱动(基于 `app-env.ts` 的 `AppEnvUtil`):
|
* 配置驱动(基于 `app-env.ts` 的 `AppEnvUtil`):
|
||||||
* - production: silent(不输出日志)
|
* - production: silent(不输出日志)
|
||||||
* - test: warn 级别(减少 vitest 噪音)
|
* - test/dev: debug 级别 + ISO 时间戳
|
||||||
* - development: debug 级别 + ISO 时间戳(人眼友好)
|
* - browser: 保留原始 message 参数,避免多行日志被转义成 `\n`
|
||||||
*/
|
*/
|
||||||
const rootLogger: PinoLogger = pino(createLoggerOptions());
|
const rootLogger: PinoLogger = pino(createLoggerOptions());
|
||||||
|
|
||||||
@@ -20,10 +21,18 @@ function createLoggerOptions(): LoggerOptions {
|
|||||||
return { level: "silent" };
|
return { level: "silent" };
|
||||||
}
|
}
|
||||||
if (AppEnvUtil.isTest()) {
|
if (AppEnvUtil.isTest()) {
|
||||||
return { level: "debug", timestamp: pino.stdTimeFunctions.isoTime };
|
return {
|
||||||
|
level: "debug",
|
||||||
|
timestamp: pino.stdTimeFunctions.isoTime,
|
||||||
|
browser: { asObjectBindingsOnly: true },
|
||||||
|
};
|
||||||
}
|
}
|
||||||
// development
|
// 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 {
|
export class Logger {
|
||||||
private readonly logger: PinoLogger;
|
private readonly logger: PinoLogger;
|
||||||
|
private readonly component: string;
|
||||||
|
|
||||||
constructor(component: string) {
|
constructor(component: string) {
|
||||||
|
this.component = component;
|
||||||
this.logger = rootLogger.child({ component });
|
this.logger = rootLogger.child({ component });
|
||||||
}
|
}
|
||||||
|
|
||||||
debug(...args: LogArgs): void {
|
debug(...args: LogArgs): void {
|
||||||
this.logger.debug(...Logger.normalizeLogArgs(args));
|
this.write("debug", args);
|
||||||
}
|
}
|
||||||
info(...args: LogArgs): void {
|
info(...args: LogArgs): void {
|
||||||
this.logger.info(...Logger.normalizeLogArgs(args));
|
this.write("info", args);
|
||||||
}
|
}
|
||||||
warn(...args: LogArgs): void {
|
warn(...args: LogArgs): void {
|
||||||
this.logger.warn(...Logger.normalizeLogArgs(args));
|
this.write("warn", args);
|
||||||
}
|
}
|
||||||
error(...args: LogArgs): void {
|
error(...args: LogArgs): void {
|
||||||
this.logger.error(...Logger.normalizeLogArgs(args));
|
this.write("error", args);
|
||||||
}
|
}
|
||||||
fatal(...args: LogArgs): void {
|
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;
|
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) */
|
/** 透传 raw pino logger(高级用法:自定义 binding / child / serializers) */
|
||||||
get raw(): PinoLogger {
|
get raw(): PinoLogger {
|
||||||
return this.logger;
|
return this.logger;
|
||||||
|
|||||||
Reference in New Issue
Block a user