凌晨三点被报警电话叫起来是什么体验?Node.js 服务崩了,你看板上一片飘红,登录服务器翻了半天日志,最后只找到一行 undefined is not a function,没有请求参数、没有用户标识、没有堆栈上下文。那一刻你就会明白,平时写完就扔的日志,其实是排查线上事故的唯一线索。这篇文章聊的就是我在生产环境里,把 Node.js 日志从“能打印”做到“能检索、能告警、能定位”的全过程:用 Pino 做高性能结构化序列化,用 PM2 守护进程并统一托管日志,最后接进 ELK 做聚合分析。如果你正在被日志分散、日志格式混乱、检索困难这些问题折磨,这篇内容应该能帮你省掉不少弯路。
1. 为什么日志值得被认真对待
1.1 一次凌晨三点的线上事故
先讲个挺典型的真实场景。我维护过一个 Node.js 的电商后端服务,平时挺稳,结果有一次大促活动开始后半小时,用户反馈下单失败率飙升。当时我第一反应是去看日志,可我们当时的日志是怎么写的?全是 console.log,格式不统一,有的带时间戳,有的不带,错误信息散落在多个文件里。我对着几十万行日志用 grep 找了半天,才勉强拼凑出一张时间线,发现是某个第三方支付接口偶发超时,我们的代码没有做超时兜底,导致整个请求链路雪崩。
那次之后我做了个决定:日志不能继续这么写了。要打印得结构化,要能自动聚合,要能在十秒钟内从几千条请求里定位到某一笔交易的完整链路。这就是整套方案的起点。
1.2 日志全链路的核心思路
所谓“日志全链路”,说白了就是把日志从“生成”到“消费”的整个过程打通。我把它拆成三个环节:
- 生成端:应用内部用什么样的方式写日志、写什么格式、写哪些字段。这里我用 Pino 做序列化,保证日志是结构化 JSON,并且序列化开销足够低,不至于拖垮业务。
- 进程端:Node.js 进程怎么被守护、日志怎么落盘、怎么按日期切分、怎么避免多实例日志互相交错。这里我用 PM2 托管进程,同时管理日志输出。
- 聚合端:多台服务器、多个实例产生的日志怎么统一收集、清洗、索引,最后能在 Kibana 里用一条查询语句快速定位问题。这里走的是 ELK 这套组合(Elasticsearch + Logstash + Kibana,可选加 Filebeat 做轻量采集)。
这个方案不是拍脑袋选的。团队里有人提过直接用云厂商的日志服务,也有人提过自建 Grafana Loki,但我当时的诉求很明确:日志量不算特别大,但要求全字段可检索,要求字段类型可控,要求团队现有的技术栈能低成本维护。ELK 的生态最成熟,踩坑资料最多,Kibana 的查询体验对新人也友好,所以最终敲定了这条路。下面按这个顺序展开。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 从 Pino 开始:写出高质量的结构化日志
2.1 为什么不用 console.log 和 winston
很多刚接触 Node.js 日志的同学第一反应是:我用 console.log 打日志,不也能凑合吗?能凑合,但有没有想过几个问题:console.log 默认是同步写向 stdout 的,高并发请求下会阻塞事件循环,官方文档里其实也提过这一点;还有,它输出的内容是一段文本,没有字段概念,到了 ELK 里你只能对整条消息做全文检索,想按 userId 精确过滤、按耗时排序、按错误码聚合,全都做不了。
那为什么不选 winston?winston 是 Node.js 里非常老牌的日志库,功能全面,支持 transport、格式化、分级,社区里一堆项目在用。但我在压测里发现,winston 的格式化开销明显比 Pino 高。Pino 官方基准测试里的数据,Pino 的日志吞吐量可以达到其他主流日志库的 5 倍以上,这意味着在极端高并发下,日志库本身不容易成为瓶颈。当然,如果你的服务对日志性能要求没那么苛刻,winston 也完全够用,这个看场景。只是我一旦试过上游服务因为日志库卡住事件循环的惨状,就不太想走回头路了。
2.2 Pino 的核心概念和配置要点
Pino 的核心概念不多,但理解清楚能省不少事。
首先是一个 logger 实例,对应一个命名空间,通常一个服务一个实例:
javascript复制const pino = require('pino');
const logger = pino({
level: process.env.LOG_LEVEL || 'info',
timestamp: pino.stdTimeFunctions.isoTime,
});
level 是日志级别,从 trace、debug、info、warn、error、fatal 一路递增。生产环境一般开到 info,开发环境可以开到 debug。
timestamp 这里我用了 isoTime,输出的是 ISO 8601 格式的时间字符串,比如 2024-06-01T12:34:56.789Z。为什么要强调这点?因为默认配置下输出的是毫秒时间戳,人类看起来不直观,而且后续 Logstash 解析时间字段时,ISO 字符串比纯数字更好处理,不容易踩时区坑。
然后是 child logger,这是 Pino 的杀手级功能。它基于父 logger 创建子实例,子实例会自动继承父实例的绑定字段(bindings),这样你可以在不同模块、不同请求上下文里各持有一个 logger,而在最终输出里带上统一的关联字段:
javascript复制const requestLogger = logger.child({ requestId: req.id, userId: req.user?.id });
requestLogger.info('user checked out');
这条日志最终输出的 JSON 里会自动带上 requestId 和 userId,不需要每次手动传。这个特性对全链路排查有无可替代的价值。
2.3 序列化器:序列化不是简单的 JSON.stringify
Pino 里最容易被人忽略的就是 serializers。它的作用是对特定字段做预处理后再序列化,比如错误对象:
javascript复制const pino = require('pino');
const logger = pino({
serializers: {
err: pino.stdSerializers.err,
req: pino.stdSerializers.req,
},
});
logger.error({ err: new Error('database connection failed') }, 'query failed');
这个 err 序列化器会把 Error 对象转换成标准的 { type, message, stack } 结构。如果没有序列化器直接打印 Error 对象,很多日志库只输出 {},因为 Error 的属性和普通 JSON 对象不太一样,console.log 能显示 message,但放到 JSON 序列化里就丢了。类似的还有请求对象 req、响应对象 res,Pino 都提供了内置序列化器,推荐直接用。
这里额外提醒一点:序列化别滥用。我看到有人喜欢在日志里塞超大的对象,比如把整个请求 body 原样打出来,里面可能包含密码、token、手机号。Pino 提供了 redact 配置,能在日志输出前把指定字段替换成 [Redacted]:
javascript复制const logger = pino({
redact: {
paths: ['req.headers.authorization', 'req.body.password', '*.phone'],
censor: '[Redacted]',
},
});
这个配置在合规审计里特别重要。你无法保证所有开发者在写日志时都有脱敏意识,但 redact 配置能从框架层面兜底,拦截一部分敏感字段。生产环境必须开,这个没得商量。
2.4 生产环境下的 Pino 配置示例
我最后落地的生产配置大概是这样的:
javascript复制const pino = require('pino');
const isProd = process.env.NODE_ENV === 'production';
const logger = pino({
level: process.env.LOG_LEVEL || (isProd ? 'info' : 'debug'),
timestamp: pino.stdTimeFunctions.isoTime,
redact: {
paths: [
'password',
'*.password',
'phone',
'token',
'req.headers.cookie',
'req.headers.authorization',
],
censor: '[Redacted]',
},
serializers: {
err: pino.stdSerializers.err,
req: pino.stdSerializers.req,
},
// 生产环境输出纯 JSON,开发环境用 pino-pretty 方便阅读
transport: isProd ? undefined : {
target: 'pino-pretty',
options: { colorize: true },
},
});
这个配置里有个 transport 选项,是 Pino v7 之后引入的机制,可以把日志通过 worker 线程交给另一个线程做预处理,比如 pino-pretty 格式化。开发环境开着很爽,生产环境坚决不开,因为 pino-pretty 输出的不是纯 JSON,会直接破坏 ELK 的解析。这个坑我踩过,后面会专门说。
如果你是新手,可以先不做复杂封装,直接从 pino 官方文档把基础配置抄下来跑通,再逐步加 redact 和 serializers。记住一个原则:生产日志只输出 JSON,一切人类可读的美化都放在开发环境或日志查询端做。
3. PM2 进程守护:日志管理的另一个维度
3.1 为什么选 PM2 而不是 systemd
如果你的 Node.js 服务只是一个单进程脚本,跑起来没人管,崩了没人拉,那日志写得再好也没意义。生产环境里进程守护是标配,我选择的 PM2。
也许有人会问,Linux 上不是有 systemd 吗?systemd 当然可以做进程守护,我也用过。但 PM2 在 Node.js 生态里有一个非常大的优势:它原生支持集群模式(cluster),可以一键把 Node.js 进程按 CPU 核心数拉起多个实例,同时内置了负载均衡。这在用 systemd 时需要自己写不少配置,而 PM2 里就一行:
bash复制pm2 start app.js -i max
另外 PM2 对日志的封装也做得很到位。它在启动时会给每个应用生成日志文件路径,统一管理 stdout 和 stderr。对于一个中大型 Node.js 团队来说,PM2 的学习成本更低,生态更统一。所以我最后的取舍是:进程守护和日志文件管理交给 PM2,系统开机自启用 pm2 startup 设置,两者分工明确。
3.2 PM2 日志管理的关键配置
用 PM2 的时候,大多数人默认不管日志,结果日志文件被扔到 ~/.pm2/logs/ 下面,而且越攒越大,服务器磁盘被写满,这是个非常常见的生产事故。我在 ecosystem.config.js 里的配置长这样:
javascript复制module.exports = {
apps: [
{
name: 'api-server',
script: './dist/index.js',
instances: 'max',
exec_mode: 'cluster',
max_memory_restart: '1G',
out_file: '/var/log/node/api-server.stdout.log',
error_file: '/var/log/node/api-server.stderr.log',
merge_logs: true,
log_date_format: 'YYYY-MM-DD HH:mm:ss Z',
time: true,
},
],
};
几个参数要展开说说:
out_file和error_file:分别指定标准输出和标准错误的重定向文件。console.log会进 stdout,console.error以及未捕获异常会进 stderr。如果前面用 Pino,且 Pino 没有单独写文件,那所有 Pino 的 JSON 日志都会打到 stdout,由 PM2 统一写文件。这么做的好处是应用层不用关心文件路径、权限这些事,PM2 全管了。merge_logs: 在 cluster 模式、多实例情况下,如果每个实例各自写一个日志文件,排查问题时会非常痛苦。打开这个选项,所有实例的输出会合并到同一个文件里,方便按时间线串联。log_date_format: 在每行日志前加一个 PM2 写入的时间戳。注意,这个时间戳加在 JSON 日志之外,所以不会破坏 JSON 结构,它就是行首的一段前缀文本。time: 这个选项让 PM2 在日志行首自动添加时间前缀,和log_date_format配合使用,便于在日志系统内部时间出现问题时有外部参照。
这些都是我在实际部署里反复验证过的参数。尤其是 out_file 路径,我建议不要放在 home 目录下,因为 PM2 经常用 root 跑,home 目录可能在 /root,和业务日志混在一起会显得很乱。放到 /var/log/node/ 这样统一的目录下,后续 Filebeat 采集时配置也更干净。
3.3 与 Pino 配合时的典型问题:双写和格式错位
Pino 和 PM2 配合时,我遇到过一个很典型的双写问题。一开始我图省事,在 Pino 里配置了 destination,让日志直接写入文件,同时 PM2 又把 stdout 重定向到另一个文件,结果两边都有日志,内容还不完全一样,排查起来极其痛苦。
后来我定了规矩:Pino 只负责把结构化 JSON 写到 stdout,PM2 负责把 stdout 收进日志文件。也就是不在 Pino 里配置 destination。这样做的好处是:
- 文件路径、日志轮转、权限管理全部统一由 PM2 体系负责;
- 应用层不感知日志文件的存在,本地开发时直接看终端输出,非常自然;
- 日志格式只保留一层 JSON,不会因为多级写入造成格式污染。
另一个常见问题是:在本地开发时无意中用 pino-pretty 启动了服务,日志变成多行彩色文本,然后同事把这套配置不小心带到了生产环境,结果 ELK 里接到的全是非 JSON 格式,kibana 里完全没法按字段过滤。遇到这种情况,我的排查思路是先在服务器上直接看日志文件:
bash复制tail -n 20 /var/log/node/api-server.stdout.log
如果看到的是漂亮的彩色多行文本,说明 pino-pretty 被打进了生产配置;如果看到的是单行 JSON,说明配置没问题。
3.4 pm2-logrotate:日志切分不能忘
日志文件无限增长的问题,靠 PM2 自带的 max_size 参数其实很难精确控制,而且它只对 PM2 自己写的日志生效,不会做压缩和定期清理。我选择用官方推荐的 pm2-logrotate 模块来维护日志生命周期:
bash复制pm2 install pm2-logrotate
pm2 set pm2-logrotate:max_size 100M
pm2 set pm2-logrotate:retain 7
pm2 set pm2-logrotate:compress true
pm2 set pm2-logrotate:rotateInterval '0 0 * * *'
max_size: 日志文件超过 100MB 就触发轮转;retain: 保留最近 7 个文件,之前的自动删除;compress: 轮转后的旧文件用 gzip 压缩;rotateInterval: 每天零点强制轮转一次。
这个配置是为了防止突然的大流量日志在一天内把磁盘塞满。日志轮转看起来是个小功能,但真等到磁盘满的时候,进程挂掉、系统进入只读模式,整个服务的雪崩速度是很快的。这类“看不见的小配置”往往决定了线上系统的稳定上限。
4. ELK 聚合:把日志变成可检索的资产
4.1 日志聚合的整体架构选型
单机单实例的服务其实用 ELK 是杀鸡用牛刀,但一旦服务上了多台服务器、多个实例,日志就变得非常分散。你排查一个问题,可能要登录三四台机器分别 grep,然后手动拼时间线,效率极低。引入 ELK 的目的就是把这些分散的日志统一收集、建索引、可视化和检索。
我最后落地的架构是:
text复制应用实例(Pino 输出 JSON)
↓ stdout
PM2 写入日志文件(/var/log/node/xxx.log)
↓ 监听文件变化
Filebeat 采集并发送
↓
Logstash 解析、清洗、输出
↓
Elasticsearch 存储和索引
↓
Kibana 可视化查询
这套结构里有一个取舍点:为什么中间要加一个 Logstash,而不是让 Filebeat 直接写 Elasticsearch?有两个原因。一是 Logstash 可以做字段清洗和格式转换,比如把字符串类型的时间转换为 date 类型,把嵌套对象拍平,这类逻辑写在 Filebeat 里不如 Logstash 灵活;二是在日志量大的时候,Logstash 可以充当缓冲层,避免上游突发流量直接打到 Elasticsearch 把集群拖垮。
当然,Logstash 本身是一个 JVM 进程,内存占用比较高。如果你的服务器资源很紧张,也可以让 Filebeat 直接输出到 Elasticsearch,少一层管道。我当时的规模大概是每天几个 GB 日志,用 Logstash 完全扛得住,所以保留了中间层。
4.2 Filebeat 和 Logstash 的配置要点
Filebeat 的配置不长,核心是告诉它“监听哪个文件”和“送到哪里去”。我用的 filebeat.yml 大概是这样:
yaml复制filebeat.inputs:
- type: filestream
id: node-api-logs
paths:
- /var/log/node/*.log
fields:
app: api-server
env: production
fields_under_root: true
output.logstash:
hosts: ["logstash.internal:5044"]
这里有个细节,fields 里定义的 app 和 env 会被加到每条日志里作为通用字段。这个在 Kibana 里做多服务过滤时非常好用,比如只查 api-server 的错误日志,或者只查生产环境的日志。
Logstash 端的配置,重点在 filter 段。因为 Pino 输出的就是 JSON,所以不需要用 grok 去做正则解析,直接用 JSON filter 即可:
ruby复制input {
beats {
port => 5044
}
}
filter {
json {
source => "message"
}
date {
match => ["time", "ISO8601"]
target => "@timestamp"
}
mutate {
rename => { "level" => "log_level" }
remove_field => ["message", "log", "ecs", "agent", "host"]
}
}
output {
elasticsearch {
hosts => ["http://elasticsearch.internal:9200"]
index => "node-api-%{+YYYY.MM.dd}"
}
}
几个关键点:
jsonfilter:把 Pino 输出的message字段解析为 JSON,展开成独立字段。如果解析失败,会保留原始字符串并加一个_jsonparsefailure标签,方便排查。datefilter:Pino 的time字段是 ISO 格式,这里把它解析成@timestamp。这个字段在 Kibana 里是默认的时间轴字段,如果不处理,Kibana 的直方图会显示 Filebeat 采集时间而不是日志实际生成时间,两者可能差好几秒。这个坑特别隐蔽,我在第一次搭 ELK 时被它坑过。mutate:把level改名为log_level,避免和 Elasticsearch 本身的level或其他字段冲突;同时把 Filebeat 加的一堆系统元数据字段删掉,减小索引体积。
另外,因为索引名带了日期,我建议在 Elasticsearch 里配上索引生命周期管理(ILM),比如 7 天后把索引切到冷节点或者删除,不然每天一个索引,几个月下来节点存储压力会非常大。
4.3 Elasticsearch 映射和 Kibana 实战
Elasticsearch 在第一次写入日志时会自动创建索引,并且使用动态映射(dynamic mapping)。这种便利带了一个隐患:同一个字段如果在不同时间写入的数据类型不一致(比如某天 status 字段是数字,某天变成了字符串),新索引会报 mapping conflict,导致查询失败。我踩到过一次后,学到一招:先把最核心的字段用手动映射定义好,比如 time 是 date,log_level 是 keyword,requestId 是 keyword。用 index template 或 data stream 来维护:
json复制{
"index_patterns": ["node-api-*"],
"template": {
"mappings": {
"properties": {
"@timestamp": { "type": "date" },
"time": { "type": "date" },
"log_level": { "type": "keyword" },
"requestId": { "type": "keyword" },
"msg": { "type": "text" }
}
}
}
}
至于 Kibana,日常用得最多的功能是 Discover。我分享一个实际的排查流程:线上报了一个用户下单失败的问题,我会在 KQL 搜索框里输入:
text复制app: "api-server" and log_level: "error" and userId: "u_123456"
然后按 @timestamp 排序,快速过滤出这个用户的所有错误日志。之后用 req.requestId 再查一次,就能看到这个请求在链路里经过了哪些服务、每个环节耗时多久、在哪一层报了错。
4.4 ELK 的运维注意事项:时区、性能和权限
ELK 搭建过程中最容易踩的三个坑,第一个是时区。Elasticsearch 内部默认存 UTC 时间,Kibana 显示时区可以在设置里改成 Asia/Shanghai,不然你会看到所有日志时间都比本地时间差 8 小时。很多人看到日志时间不对,第一反应是改 Logstash,实际上 Kibana 一个设置就能搞定。
第二个是 Elasticsearch 的性能。日志索引的副本数不建议设太高,默认一主一副就够;如果日志量大,可以只在 ELK 的冷节点上保留单副本,降低存储压力。另外 Elasticsearch 对磁盘 IO 非常敏感,不要让日志索引和业务数据库共用同一块机械盘,会互相拖垮。
第三个是权限。很多团队把 Kibana 直接暴露到公网,然后用弱口令登录,这非常危险。合理做法是 Elasticsearch 和 Kibana 部署在内网,对外只暴露经过网关鉴权的入口,同时启用 ES 的 Basic Auth 或 x-pack 安全认证。这属于安全底线,别嫌麻烦。
5. 常见问题与排查技巧实录
5.1 日志文件里看不到内容,或者内容被截断
如果你发现 PM2 日志文件是空的,先别怀疑 Pino。先用命令行直接测:
bash复制pm2 logs api-server --lines 50
PM2 的 logs 命令会直接流式显示应用输出,如果这里能看到日志但文件里没有,说明 out_file 配置没生效,PM2 可能没有重新读取 ecosystem.config.js。改完配置后需要 pm2 delete 再加 pm2 start ecosystem.config.js,单纯 pm2 reload 有时候不生效,这是个让我折腾过两次的细节。
如果日志文件存在但内容只有半行,多半是 PM2 在写文件时进程崩溃,日志缓冲区没有刷新。Pino 有一个异步写入模式(transport 里的 worker 线程),用的是内存缓冲,进程崩溃时确实可能丢日志。我为了追求日志可靠性,生产环境里故意关掉了这个机制,用同步模式。代价是单个日志点会有一点点额外耗时,换来的是一条日志都不会丢。
5.2 多实例和 cluster 模式下日志乱序或串台
PM2 的 cluster 模式拉起多个进程后,每个进程有自己的事件循环,日志写入顺序天然不保证全局有序。如果你在日志里按时间排序发现同一个 requestId 的日志穿插了其他请求,这是正常的。要追踪单个请求的完整链路,方法是在入口中间件生成一个 requestId,用 Pino 的 child logger 把这个 ID 注入到该请求的所有日志里:
javascript复制const crypto = require('crypto');
const pino = require('pino');
const rootLogger = pino({ ... });
app.use((req, res, next) => {
req.id = crypto.randomUUID();
req.log = rootLogger.child({ requestId: req.id });
next();
});
之后这个请求内部所有日志调用都走 req.log,Kibana 里用 requestId 一查就能把所有相关日志串起来。这个方法比单纯修 PM2 的日期格式要靠谱得多。
另外还要注意 merge_logs: true 只影响 PM2 写入日志文件的方式,它并不会给每条日志打上进程 ID 的标签。如果多进程调试时需要区分是哪个实例打的日志,可以在 PM2 的环境变量里注入 NODE_APP_INSTANCE 作为 Pino 的绑定字段,这个变量是 PM2 自动注入的,区分实例很方便:
javascript复制const logger = rootLogger.child({ instance: process.env.NODE_APP_INSTANCE || 0 });
5.3 ELK 里看到 _jsonparsefailure 标签
这个标签出现,通常意味着 Logstash 在解析 message 字段时失败,也就是说文件里混入了非 JSON 的行。最常见的来源就是开篇提到的 pino-pretty 在生产的意外使用。排查方法很简单:
bash复制grep -n "pretty" /var/log/node/api-server.stdout.log | head
有输出,说明配置里混入了美化格式。另外还有一个可能:PM2 的 log_date_format 会在每行前面加日期前缀,但如果加在 JSON 开头,JSON 解析会失败。我实测过,PM2 的日期前缀是在写入日志文件时加的,Pino 输出的原始 stdout 是纯 JSON,所以加前缀不会污染 JSON。如果你在服务器上看到的日志文件里每行前面有 2024-06-01 12:00:00 这样的前缀,而 Logstash 还是解析失败,那就看看是不是 Filebeat 采集的路径有问题,误采了 PM2 的 error 文件或系统日志。
5.4 日志查询慢和索引撑爆磁盘
先说说日志查询慢。一个常见原因是索引里包含大量未映射的嵌套对象,比如你在日志里打印了完整的请求 body,里面可能有几十个字段,ES 会自动为每一个字段建立映射,导致索引体积膨胀,查询自然就慢。建议在 logstash 的 mutate 阶段就删掉不必要的大字段,或者只保留白名单字段。这个在初期设计时就要想清楚,事后改 mapping 非常麻烦。
索引撑爆磁盘的问题,靠 ILM 解决最稳妥。我用的策略是:索引超过 30 天自动删除,超过 5GB 自动滚动到新索引。配置大概是这样:
json复制{
"policy": {
"phases": {
"hot": { "actions": { "rollover": { "max_size": "5GB", "max_age": "7d" } } },
"delete": { "min_age": "30d", "actions": { "delete": {} } }
}
}
}
把 ILM 策略关联到 node-api-* 索引模板上,剩下的交给 ES 自动处理。
5.5 日志内容正确,但 Kibana 里字段类型不对
这种问题经常发生在新加的日志字段上。比如你这次在日志里加了 statusCode,第一次写入的是数字 200,ES 会 mapping 成 long。后来某个分支返回值变成了字符串 "200",新索引动态 mapping 就会冲突,查询会直接报错或查不到数据。处理办法有两个:
- 在 ES 模板里提前声明常见字段的类型,重点字段统一用
keyword或date; - 在 Logstash 的
mutate里做类型转换,比如convert => { "statusCode" => "integer" },从源头统一。
我个人更推荐做模板映射,因为 Logstash 的 convert 只能处理已知字段,如果相关开发忘了加,照样会出问题。模板层的约束是全局的,更稳定。
6. 最后再分享几个我实际养成的习惯
整个日志链路搭好之后,运维体验和之前完全不是一个量级。不过我还有一个额外的建议:把“查询日志”这件事本身沉淀成团队的文档。比如把“突发错误怎么查”、“慢请求怎么定位”、“第三方超时怎么排查”这些场景写成 SOP,附上对应的 Kibana 查询语句,让团队新人遇到问题时直接照做,而不是重新踩一遍坑。
另一个小技巧是,给 Pino 的日志加上 module 字段,每个模块创建 logger 时传入模块名。这样在 Kibana 里可以用 module: "payment" 这样的条件快速过滤出支付模块的日志。这个习惯成本极低,但在模块拆分的服务里排查问题非常高效。
日志这件事,前期花的时间和心思,会在某个凌晨三点的报警电话里连本带利地还给你。希望这套方案能给你一些启发,也可以帮你少走一点我走过的弯路。
