import pino, { type Logger as PinoLogger, type LoggerOptions } from "pino"; import { AppEnvUtil } from "./app-env"; type LogArgs = unknown[]; type PinoLogArgs = Parameters; /** * 全局 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 个 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; 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; } }