Node.js 日志最佳实践十一条:从 console.log 到生产级日志体系
Node.js 生产环境的日志该怎么打?本文总结十一条 Node.js 日志最佳实践——选对日志框架、结构化输出、正确用级别、带上时间戳与上下文、错误带堆栈、敏感数据脱敏、输出到 stdout 并集中管理,并结合观测云给出落地方式。
Node.js 日志最佳实践是一套让 Node.js 应用的日志"可检索、可告警、可审计、成本可控"的工程规范,核心是从 console.log 过渡到结构化日志框架,并把采集、分析职责移交给专业平台。 console 模块适合开发期调试,但缺少级别、时间戳、结构化与输出管理,需额外实现生产所需的结构化、级别筛选与输出策略。
核心要点速览
- 生产环境按需求管理
console.log,选用 Winston/Pino 等成熟日志框架; - 结构化(JSON)+ 时间戳 + 级别 + 上下文字段,是日志可用的四个最低条件;
- 错误日志必须带完整堆栈,同时覆盖未捕获异常与未处理 Promise 拒绝;
- 容器应用优先写 stdout/stderr,采集、路由、留存交给 DataKit 与观测云平台。
十一条实践总览
| # | 实践 | 影响力 | 上手难度 |
|---|---|---|---|
| 1 | 选用成熟日志框架 | ★★★★★ | ★★★ |
| 2 | 结构化日志 | ★★★★★ | ★ |
| 3 | 正确使用日志级别 | ★★★★★ | ★★ |
| 4 | 写有信息量的消息 | ★★★★ | ★★ |
| 5 | 始终携带时间戳 | ★★★★★ | ★ |
| 6 | 为日志补充上下文字段 | ★★★★ | ★★★ |
| 7 | 错误必须带堆栈 | ★★★★★ | ★ |
| 8 | 敏感数据不进日志 | ★★★★★ | ★★★★ |
| 9 | 日志不只用于排障 | ★★★ | ★★★ |
| 10 | 按部署方式选择标准输出或文件 | ★★★ | ★★ |
| 11 | 集中到日志管理系统 | ★★★★★ | ★★★★ |
一、选用成熟日志框架
console.log 家族(log/info/warn/error/trace)写 stdout/stderr,没有级别控制、没有时间戳、不会自动提供结构化输出(但可输出序列化 JSON)。生产可评估 Winston(功能全、生态大)与 Pino(性能强、Fastify 默认)之间,详见本系列两篇专项指南。
二、结构化日志
自由文本 "image 'file.jpg' was uploaded" 机器难以解析;改写成 JSON 后每个变量都是独立字段,可分组、可过滤:
{"level":"info","timestamp":"2026-08-25T07:12:46.743Z","filename":"file.jpg","msg":"图片上传成功"}
开发期要可读性,管道接 pino-pretty 即可,不必牺牲生产结构化。
三、正确使用日志级别
级别是最基本的严重程度信号:调试环境用 DEBUG/TRACE,生产默认 INFO。可在初始化时读取环境变量;修改外部环境不会自动更新已运行进程,热调级别需受控运行时机制:
const logger = pino({ level: process.env.LOG_LEVEL || 'info' });
接入观测云时,按 DataKit 日志配置将应用级别映射为 status,先用 info 和 error 样本核对映射,再设置筛选条件。
四、写有信息量的消息
反例:"认证失败"(谁失败了?)、"出错了"(什么错?)、"数值超过 100"(什么数值?为什么是 100?)。正例:用户 USR-1234 认证失败:密码错误、订单 ORD-34567 支付处理异常。每条日志都应能独立回答"谁、什么、为什么"。
五、始终携带时间戳
没有时间戳的日志几乎无法排障。框架默认一般都有,但格式需明确时区与精度,ISO-8601 或约定单位的 Unix 时间戳都可用——Pino 用 pino.stdTimeFunctions.isoTime,Winston 用 timestamp() format。
六、为日志补充上下文字段
request_id、user_id、service 名等上下文字段,是把分散日志串成"一次请求的完整故事"的关键。用 child logger 绑定公共字段避免重复书写:
const reqLogger = logger.child({ request_id: 'f9ed4675f1c53513c61a3b3b4e25b4c0' });
若请求已采集为 Trace,将上下文中的 trace_id 写入日志,并核对与 Span 一致;观测云的关联字段要求还包括服务、环境等上下文。
七、错误必须带堆栈
错误消息只告诉你"怎么了",堆栈告诉你"在哪儿"。Winston 需显式加 errors({ stack: true }),Pino 对 err 键默认有错误序列化,使用 logger.error({ err }, '操作失败') 可保留堆栈。另外务必覆盖两类逃逸异常:
// 应接入应用既有的受控关闭流程:停止接收请求、限时清理并排空日志。
// 不要在记录后立即 process.exit(),异步transport可能尚未写出。
process.on('uncaughtExceptionMonitor', (err) => { logger.fatal({ err }, '未捕获异常'); });
// Monitor只观察,不阻止默认退出;致命日志仍需按所用transport验证同步兜底/排空。
八、敏感数据不进日志
密码、令牌、银行卡号乃至手机号都不应出现在日志里,日志泄露会扩大事故影响。两道防线:应用侧仅记录必要字段,用户 ID 也需按可识别信息管理,避免记录完整对象;框架侧用 Pino redact 之类配置兜底。用含敏感字段的测试对象检查最终输出,尤其注意自定义序列化与异常上下文是否绕过脱敏。
九、日志不只用于排障
采样或丢弃后的日志不能当作完整审计证据。结构良好的日志还可用于审计(谁在何时操作了什么)、行为分析(功能使用频率)与粗粒度性能测量的数据源(如 Winston 的 startTimer() 输出 durationMs)。
十、按部署方式选择标准输出或文件
容器环境通常优先 stdout/stderr,其他部署也可选择受管理的日志文件:路由、缓冲、重试、多目的地分发都是采集器(如 DataKit)的职责,应用进程不该背着网络重试的负担 [4]。容器/K8s 环境下 stdout 会被平台天然接管,是最云原生的做法。
十一、集中到日志管理系统
应用上线后日志散落在各实例本地,逐机查看不可扩展。集中化的收益:跨实例全局视角、按 ERROR/FATAL 等模式告警、可视化看板、已送达并持久保存的日志在服务器宕机后仍可查;留存与访问控制需另行配置,集中化不自动保证合规。使用观测云时,先验证各实例的测试日志都能按服务与时间检索,再根据日志检测配置设置错误规则和通知;实例没有数据时,应检查采集链路而不是直接认定没有错误。
总结
十一条可以浓缩为:框架化(1)、结构化(2)、有级别有时间有上下文(3/5/6)、错误带堆栈(7)、敏感数据双保险(8)、按部署选择输出(10)、分析告警上平台(11)。逐条对照检查现有应用,补齐短板即可达到生产水位。
常见问题(FAQ)
Q:小项目也需要上日志框架吗?
需要。框架切换成本在初期最低,等项目长大再换,代价是全代码库的调用点改造。pino() 一行即可创建 logger,上手成本并不比 console 高。
Q:stdout 输出在高并发下会不会成为瓶颈?
stdout 是否同步取决于运行时、平台和目标,序列化、队列与下游背压都可能成为瓶颈,不能一概认为它总是异步或更快。这也是"输出到 stdout + 独立采集器"模式成为主流的原因。
Q:日志里到底该不该记用户 ID?
仅在有必要且符合数据策略时记录;用户 ID 仍可能识别人或关联个人行为,并非天然不敏感。要防的是密码、令牌、证件号、完整手机号这类内容,配合脱敏即可。
Q:十一条实践的优先落地顺序?
先做 1、2、3、5、7(框架、结构化、级别、时间戳、堆栈)这五个"地基项",再补 6、8(上下文、脱敏),最后做 10、11 的平台化。
系列阅读:日志最佳实践十二条 | 防止敏感数据进入日志
[1]: 观测云文档《日志采集》https://docs.guance.com/logs/collection/
[2]: 观测云《APM 平台》https://www.guance.com/product/application-performance-monitoring
[3]: 观测云《通过敏感数据扫描能力保障数据安全合规》https://www.guance.com/learn/articles/sensitive-data-scanning
[4]: 观测云文档《DataKit 日志采集综述》https://docs.guance.com/integrations/datakit-logging/
[5]: 观测云文档《监控》https://docs.guance.com/monitoring/