// server/utils/logging.ts - 安全日志工具,避免输出密钥、cookie 和完整敏感内容 import { randomUUID } from "node:crypto"; import type { H3Event } from "h3"; /** * 常见敏感片段匹配 * * 这里不是完整 DLP,只处理最容易误打出来的 key、Bearer token 和 token=xxx * 真实请求体、响应体本来就不应该进入日志 */ const SENSITIVE_VALUE_RE = /(sk-[A-Za-z0-9_-]+|Bearer\s+[A-Za-z0-9._-]+|token[=:]\s*[^,\s}]+)/giu; /** 对错误摘要做最小脱敏和截断,避免日志过长或泄露密钥 */ const sanitizeLogText = (value: string) => { return value.replace(SENSITIVE_VALUE_RE, "[redacted]").slice(0, 500); }; /** * 将 unknown 异常变成安全可打印对象 * * API 对前端统一返回“服务器内部错误”,日志里只保留类型和脱敏摘要, * 既能排查方向,又不把上游响应体或 key 打出去 */ export const toSafeLogError = (error: unknown) => { if (error instanceof Error) { return { name: error.name, message: sanitizeLogText(error.message) }; } return { name: "UnknownError", message: sanitizeLogText(String(error)) }; }; /** * 创建带 requestId 的 API 日志器 * * 每个 handler 只需要记录“阶段”和少量结构化字段;这里统一补 method、path、 * 耗时等上下文,方便后续按一次请求串起来查 */ export const createApiLogger = (event: H3Event, scope: string) => { const requestId = event.node.req.headers["x-request-id"]?.toString() || randomUUID(); const startedAt = Date.now(); /** 内部统一出口,避免不同 level 打出的字段不一致 */ const write = ( level: "info" | "warn" | "error", stage: string, extra: Record = {} ) => { const payload = { requestId, scope, stage, method: event.node.req.method, path: event.path, elapsedMs: Date.now() - startedAt, ...extra }; console[level](`[${scope}] ${stage}`, JSON.stringify(payload)); }; /** 返回轻量 API,handler 不直接接触 console,便于以后替换日志实现 */ return { requestId, info: (stage: string, extra?: Record) => write("info", stage, extra), warn: (stage: string, extra?: Record) => write("warn", stage, extra), error: (stage: string, extra?: Record) => write("error", stage, extra) }; };