Files
cozsweet-frontend-nextjs/src/utils/logger.ts
T

129 lines
3.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import pino, { type Logger as PinoLogger, type LoggerOptions } from "pino";
import { AppEnvUtil } from "./app-env";
type LogArgs = unknown[];
type PinoLogArgs = Parameters<PinoLogger["debug"]>;
/**
* 全局 pino root logger(单例,进程级复用)。
*
* 配置驱动(基于 `app-env.ts` 的 `AppEnvUtil`):
* - production: silent(不输出日志)
* - test: warn 级别(减少 vitest 噪音)
* - development: debug 级别 + ISO 时间戳(人眼友好)
*/
const rootLogger: PinoLogger = pino(createLoggerOptions());
function createLoggerOptions(): LoggerOptions {
if (!AppEnvUtil.canOutputLogs()) {
return { level: "silent" };
}
if (AppEnvUtil.isTest()) {
return { level: "warn" };
}
// development
return { level: "debug", timestamp: pino.stdTimeFunctions.isoTime };
}
/**
* Logger 工具类(pino 包装)
*
* 设计:
* - 构造时绑定 `component` 名(业务模块标识)
* - 所有日志自动带 `{ component: "AuthRepository" }` 字段
* - 透传 pino 原生 5 个 leveldebug / 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;
constructor(component: string) {
this.logger = rootLogger.child({ component });
}
debug(...args: LogArgs): void {
this.logger.debug(...Logger.normalizeLogArgs(args));
}
info(...args: LogArgs): void {
this.logger.info(...Logger.normalizeLogArgs(args));
}
warn(...args: LogArgs): void {
this.logger.warn(...Logger.normalizeLogArgs(args));
}
error(...args: LogArgs): void {
this.logger.error(...Logger.normalizeLogArgs(args));
}
fatal(...args: LogArgs): void {
this.logger.fatal(...Logger.normalizeLogArgs(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;
}
/** 透传 raw pino logger(高级用法:自定义 binding / child / serializers */
get raw(): PinoLogger {
return this.logger;
}
}