/** * 结构化日志系统 * * 替代散布在代码中的 console.log/error/warn,提供: * - 统一的日志格式 (JSON) * - 日志级别过滤 * - 请求关联 ID 追踪 * - 错误堆栈保留 * * 用法: * import { logger, log } from "@/lib/logger"; * log.info("User logged in", { userId: "123" }); * log.error("Payment failed", error, { orderId: "456" }); */ type LogLevel = "debug" | "info" | "warn" | "error"; interface LogEntry { level: LogLevel; message: string; timestamp: string; error?: { name: string; message: string; stack?: string; }; [key: string]: unknown; } type LogArgs = | [string] | [string, Record] | [string, Error, Record?]; // 生产环境只输出 warn 及以上级别 const MIN_LEVEL: Record = { debug: 0, info: 1, warn: 2, error: 3, }; function resolveMinLevel(): LogLevel { const configured = process.env.LOG_LEVEL as LogLevel | undefined; if (configured && configured in MIN_LEVEL) return configured; return process.env.NODE_ENV === "production" ? "warn" : "info"; } const currentMinLevel = resolveMinLevel(); function shouldLog(level: LogLevel): boolean { return MIN_LEVEL[level] >= MIN_LEVEL[currentMinLevel]; } function formatError(err: Error): LogEntry["error"] { return { name: err.name, message: err.message, ...(err.stack ? { stack: err.stack.split("\n").slice(0, 5).join("\n") } : {}), }; } function createEntry( level: LogLevel, message: string, extra?: Record, err?: Error ): LogEntry { const entry: LogEntry = { level, message, timestamp: new Date().toISOString(), }; if (err) { entry.error = formatError(err); } if (extra) { // Skip internal fields to avoid overwriting for (const [key, value] of Object.entries(extra)) { if ( key !== "level" && key !== "message" && key !== "timestamp" && key !== "error" ) { entry[key] = value; } } } return entry; } function formatEntry(entry: LogEntry): string { // 开发环境使用可读格式,生产环境使用 JSON if (process.env.NODE_ENV !== "production") { const parts = [ `[${entry.timestamp}]`, `[${entry.level.toUpperCase()}]`, entry.message, ]; if (entry.error) { parts.push(`| ${entry.error.name}: ${entry.error.message}`); } // 额外字段 const extraKeys = Object.keys(entry).filter( (k) => k !== "level" && k !== "message" && k !== "timestamp" && k !== "error" ); if (extraKeys.length > 0) { const extra: Record = {}; for (const k of extraKeys) extra[k] = entry[k]; parts.push(JSON.stringify(extra)); } return parts.join(" "); } return JSON.stringify(entry); } function write(entry: LogEntry): void { const formatted = formatEntry(entry); switch (entry.level) { case "error": console.error(formatted); break; case "warn": console.warn(formatted); break; case "info": console.info(formatted); break; case "debug": console.debug(formatted); break; } } // --- 公共 API --- function logFn(level: LogLevel) { return (...args: LogArgs): void => { if (!shouldLog(level)) return; const [message, arg2, arg3] = args; let extra: Record | undefined; let err: Error | undefined; if (arg2 instanceof Error) { err = arg2; extra = arg3; } else if (arg2 && typeof arg2 === "object") { extra = arg2; } write(createEntry(level, message, extra, err)); }; } export const log = { debug: logFn("debug"), info: logFn("info"), warn: logFn("warn"), error: logFn("error"), }; /** * 带模块名的日志工厂 * * 用法: * const log = createLogger("EmailVerification"); * log.info("Code sent", { email: "user@example.com" }); */ export function createLogger(module: string) { return { debug: (message: string, extra?: Record) => log.debug(message, { ...extra, module }), info: (message: string, extra?: Record) => log.info(message, { ...extra, module }), warn: (message: string, extra?: Record) => log.warn(message, { ...extra, module }), error: (message: string, err?: Error, extra?: Record) => { if (err) { log.error(message, err, { ...extra, module }); return; } log.error(message, { ...extra, module }); }, }; } export { createLogger as logger }; export default log;