Some checks failed
Lint / build (20) (push) Has been cancelled
- Remove hardcoded admin password, use env var or auto-generate - Fix trustHost to be conditional (Vercel-only by default) - Add HTTP security headers (CSP, X-Frame-Options, HSTS) - Create middleware.ts to activate existing rate limiting - Implement structured logging system - Rewrite error handling in ai-cad service - Update security documentation
177 lines
4.4 KiB
TypeScript
177 lines
4.4 KiB
TypeScript
/**
|
||
* 结构化日志系统
|
||
*
|
||
* 替代散布在代码中的 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>) =>
|
||
log.error(message, err, { ...extra, module }),
|
||
};
|
||
}
|
||
|
||
export { createLogger as logger };
|
||
export default log;
|