日志搭建
第一天装结构化日志带级别、请求 ID 和关联上下文。
为什么
console.log 对 50 行脚本可,生产致命:不能按级别过滤、不能跨请求关联、不能搜需 SSH 进机。结构化日志(JSON 带级别、时间戳、request_id、user_id)把日志变可查数据——你能问'过去 1 小时用户 42 的每个错误'秒得答。请求 ID 是乘数:它把请求内每行日志系一起,用户报'3 点结账失败'变单次 grep 非 10k 行滚动。事后补痛苦因每调用点要改;第一天做 是 20 行配置。
何时使用
紧跟 project-init,第一个功能前。约 20-30 分钟。仅无服务器的 CLI 工具可跳过。
如何做
1. 选结构化 logger:pino(Node,最快)、structlog(Python)、zap(Go)
- 避免 winston/log4j——可配但慢且鼓励字符串模板
2. 定义日志级别及每级对应用含义
- debug:仅 dev;info:请求生命周期;warn:降级但处理;error:需关注
- prod 级别设 info,staging 设 debug——prod 永不 trace(量杀你)
3. 加请求 ID 中间件,每请求生 UUID 并每行记
- Express:req.id = uuid();logger.child({ requestId: req.id })
- 经 x-request-id 头传下游调用,trace 跨服务
4. 记结构化对象非插值字符串
- logger.info({ userId, action: "checkout", durationMs }, "checkout completed")
- 永不 logger.info(`User ${userId} checked out in ${ms}ms`)——不可搜
5. 日志发可搜 sink:Datadog、Loki、CloudWatch——非磁盘文件
- dev 留本地文件回退;prod stdout → 容器日志驱动 → 聚合示例
// logger.ts——pino 带请求 id 子 logger
import pino from "pino";
export const logger = pino({
level: process.env.LOG_LEVEL ?? "info",
formatters: { level: (label) => ({ level: label }) },
});
// middleware.ts——每请求子 logger
import { v4 as uuid } from "uuid";
export function requestLogger(req, res, next) {
req.id = req.headers["x-request-id"] ?? uuid();
req.log = logger.child({ requestId: req.id, userId: req.user?.id });
req.log.info({ method: req.method, url: req.url }, "request start");
res.on("finish", () => req.log.info({ status: res.statusCode }, "request end"));
next();
}
// 路由用法——结构化、可搜
router.post("/checkout", async (req, res) => {
const t0 = Date.now();
await processCheckout(req.body);
req.log.info({ action: "checkout", durationMs: Date.now() - t0 }, "checkout ok");
});常见问题
- ⚠info 级日志记密钥(密码、token、PII)发第三方聚合——合规事件。
- ⚠dev 用 console.log,prod 用别 logger,prod 行为成意外。
- ⚠无请求 ID,用户报'3 点失败'意味滚 10k 行碰运气找错。
- ⚠热循环内(10k 行批每行)info 级记,量淹聚合。
最佳实践
- ✓加脱敏格式器,日志离进程前剥已知密钥键(password、token、authorization)。
- ✓用跨服务边界传播的关联 ID(HTTP 头、消息队列属性),多服务 trace 一查询。
- ✓prod 采样 debug/trace(1000 请求取 1),偶得深 trace 无量成本。
- ✓对错误率突增告警非单个错——单错正常;5 分钟 10 倍突增是事件。