ai-chatbot/lib/logger.ts
2026-05-26 19:32:19 +08:00

197 lines
4.6 KiB
TypeScript
Raw Permalink 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.

/**
* 结构化日志系统
*
* 替代散布在代码中的 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, unknown>]
| [string, Error, Record<string, unknown>?];
// 生产环境只输出 warn 及以上级别
const MIN_LEVEL: Record<LogLevel, number> = {
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<string, unknown>,
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<string, unknown> = {};
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<string, unknown> | 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<string, unknown>) =>
log.debug(message, { ...extra, module }),
info: (message: string, extra?: Record<string, unknown>) =>
log.info(message, { ...extra, module }),
warn: (message: string, extra?: Record<string, unknown>) =>
log.warn(message, { ...extra, module }),
error: (message: string, err?: Error, extra?: Record<string, unknown>) => {
if (err) {
log.error(message, err, { ...extra, module });
return;
}
log.error(message, { ...extra, module });
},
};
}
export { createLogger as logger };
export default log;