
Node.js 服务日志的 JSON 化改造实战用 Bunyan 构建可解析、可监控的结构化日志体系【免费下载链接】nodejs.orgThe Node.js® Website项目地址: https://gitcode.com/GitHub_Trending/no/nodejs.org本文以 nodejs.org 官方博客module 分类发表于 2012-03-28 的经典文章Service logging in JSON with Bunyan作者 trentmick源自 Joyent SmartDataCenter 与 Joyent Cloud 的一线运维实践为核心系统讲解为何要将 Node.js 服务日志从给人看的文本改造成给机器解析的 JSON、如何用开源库Bunyan完成这种改造以及如何在 restify 搭建的 API 服务中落地请求/响应全链路结构化日志。读完本文你将掌握 Bunyan 的 Logger、streams、serializers 三大核心概念理解req_id跨服务关联与time字段跨节点聚合的日志设计思想并能把这一套方案直接迁移到自己的 Node.js 服务中。为什么服务日志值得被认真对待Service logs are gold, if you can mine them.服务日志是金矿前提是你真的能开采它。现实却是大多数团队只在排障时零散地翻翻日志偶尔用 grep 找找错误或警告再高级一点也就是给 Nagios 挂一个正则表达式日志监控——仅此而已。这几乎浪费了服务数据最好的渠道。日志真正有价值的用途有四类调试debugging问题发生时还原现场监控告警monitors/alerting驱动运维工具主动发现异常并通知值班人员非实时分析non real-time analysis业务分析或运营分析历史分析historical analysis回溯趋势、定位回归。现状是主流日志方案连第一项调试都只是勉强够用。要对五花八门的 printf 风格 日志做可靠的分析乃至监控是一件极其折磨人且 hacky 的工作——大多数人干脆不做或者花钱买第三方方案或者对网站而言转向基于 JavaScript 的 Web 分析工具。问题的根源不在日志本身而在格式。现状五花八门的日志格式与可解析性困境随手挑五份日志看看当下服务日志的格式现状# apache access log 10.0.1.22 - - [15/Oct/2010:11:46:46 -0700] GET /favicon.ico HTTP/1.1 404 209 fe80::6233:4bff:fe29:3173 - - [15/Oct/2010:11:46:58 -0700] GET / HTTP/1.1 200 44 # apache error log [Fri Oct 15 11:46:46 2010] [error] [client 10.0.1.22] File does not exist: /Library/WebServer/Documents/favicon.ico [Fri Oct 15 11:46:58 2010] [error] [client fe80::6233:4bff:fe29:3173] File does not exist: /Library/WebServer/Documents/favicon.ico # Mac /var/log/secure.log Oct 14 09:20:56 banana loginwindow[41]: in pam_sm_authenticate(): Failed to determine Kerberos principal name. Oct 14 12:32:20 banana com.apple.SecurityServer[25]: UID 501 authenticated as user trentm (UID 501) for right system.privilege.admin # an internal joyent agent log [2012-02-07 00:37:11.898] [INFO] AMQPAgent - Publishing success. [2012-02-07 00:37:11.910] [DEBUG] AMQPAgent - { req_id: 8afb8d99-df8e-4724-8535-3d52adaebf25, timestamp: 2012-02-07T00:37:11.898Z, # typical expressjs log output [Mon, 21 Nov 2011 20:52:11 GMT] 200 GET /foo (1ms) Blah, some other unstructured output to from a console.log call.五份日志五种不同的日期格式有[15/Oct/2010:11:46:46 -0700]、[Fri Oct 15 11:46:46 2010]、Oct 14 09:20:56、[2012-02-07 00:37:11.898]还有[Mon, 21 Nov 2011 20:52:11 GMT]。正如 Paul Querna 所指出的20 年来日志的可解析性没有任何进步。可解析性是头号敌人Parsability is enemy number one。日志必须能被解析成记录你才能使用它们而面对上述格式唯一的出路就是写一次性正则表达式。现有的武器库——各类解析库、webalizer/awstats 之类的分析工具、从 grep 到 Perl 的家庭作坊脚本——其适用范围都局限于少数几种 niche 格式。换言之工具被格式绑架了。换一种思路让机器成为日志的第一受众JSON.parse()能一次性解决所有解析问题——前提是我们要把日志写成交织成 JSON。但这意味着一次思维转变日志文件的第一受众不应该是人而应该是机器。这句话并非轻率之言。Unix 之道——用文本输出将小而专注的工具松散耦合——确实重要。JSON 比 Apache common log format 之类的格式更不文本化它让grep、awk变得笨拙直接less查看日志的便利性也打了折扣。但这种不便并不致命——让你难受的不是 JSON 本身而是你的工具。是时候找一个json工具trentm/json 就是其一下文要讲的bunyan是另一个是时候去熟悉一门 JSON 库JavaScript、Python、Ruby、Java、Perl 都有成熟的 JSON 实现而不是继续钻研你的正则库了。更进一步烧掉 log4j 的 Layout 类把格式化搬到工具侧。在应用里构造一条携带语义信息的日志记录然后为了拼一个字符串把它丢掉这是很愚蠢的。将日志记录轻易解析出来的收益是巨大的而向单条日志记录附加临时的结构化字段更是打开了无数可能性记录程序状态指标program state metrics直接喂给 Splunk 或 loggly 之类的分析平台轻松产出审计日志audit logs。认识 BunyanNode.js 的 JSON 日志模块与 CLI 工具Bunyan是两样东西一个用于 JSON 日志的 Node.js 模块以及一个用于查看这些日志的bunyanCLI 工具。库的命名致敬了美国民间传说人物 Paul Bunyan伐木巨人正是上图中他与蓝牛 Babe 的形象。用 Bunyan 打日志基本就是这样// hi.js var Logger require(bunyan); var log new Logger({name: hello /*, ... */}); log.info(hi %s, paul);运行后得到这样一条日志记录$ node hi.js {name:hello,hostname:banana.local,pid:40026,level:30,msg:hi paul,time:2012-03-28T17:25:37.050Z,v:0}这是一条完整的 JSON 记录字段含义一目了然nameLogger 名称这里是 hellohostname主机名用于跨节点区分日志来源pid进程 IDlevel日志级别数值结合下文示例可见 30 对应 INFO、20 对应 DEBUGmsg人类可读的消息文本支持 printf 风格的%s格式化timeISO 8601 时间戳带毫秒与 UTC 时区v日志记录格式的 schema 版本号。把输出管道给随 node-bunyan 一起安装的bunyan工具就能得到更可读的展示$ node hi.js | ./node_modules/.bin/bunyan # formatted text output [2012-02-07T18:50:18.003Z] INFO: hello/40026 on banana.local: hi paul $ node hi.js | ./node_modules/.bin/bunyan -j # indented JSON output { name: hello, hostname: banana.local, pid: 40087, level: 30, msg: hi paul, time: 2012-03-28T17:26:38.431Z, v: 0 }bunyan工具默认输出格式化文本并带颜色高亮-j参数输出缩进后的 JSON。注意格式化发生在工具侧而不是应用侧——应用永远只负责产出结构化的 JSON 记录。Bunyan 的 API 与 log4j 类似创建一个带名字的 Logger然后调用log.info(...)等。但它无意复刻 log4j 的大部分功能——在作者看来对用 Node.js 写的这类服务而言log4j 的许多特性都是过度的。轻量正是它的设计取向。完整实战用 Bunyan restify 打造可观测的 Hello API下面通过一个更大的示例展示 Bunyan 的几个有趣特性。我们将用 restify 库Joyent 内部重度使用Bunyan 本身并不依赖 restify换成 Express 或其他框架同样可行搭建一个极小的 Hello API 服务。示例代码位于 trentm 的 hello-json-logging 示例仓库复现步骤为克隆该仓库、进入目录后执行make。需要说明的是原文写作于 2012 年使用的是当时 bunyan 与 restify 的最新代码示例要求 Node 0.6.x 环境具体细节可能与今天的版本略有出入——但这不影响其日志设计思想的普适性。创建 Bunyan Loggerserver 首先创建一个 Bunyan loggervar Logger require(bunyan); var log new Logger({ name: helloapi, streams: [ { stream: process.stdout, level: debug, }, { path: hello.log, level: trace, }, ], serializers: { req: Logger.stdSerializers.req, res: restify.bunyan.serializers.response, }, });这里出现了 Bunyan 的三个核心概念name每个 Bunyan logger 必须有名字。与 log4j 不同这不是层级化的点分命名空间dotted namespace而只是日志记录上的一个 name 字段。streams每个 Bunyan logger 有一个或多个 stream日志记录会写入这些 stream。上面的配置定义了两个DEBUG 及以上级别输出到 stdoutTRACE 及以上级别追加写入hello.log文件。serializers一个函数注册表负责把日志记录中某个字段的 JavaScript 对象转换成适合记录的 JSON 表示。例如这里用字段名 req 注册了Logger.stdSerializers.req用于把 HTTP Request 对象序列化为 JSON。关于 serializers下文还会展开。接入 restifyRestify 1.x 及以后版本内置了 bunyan 支持把 Logger 传进去即可var server restify.createServer({ name: Hello API, log: log, // Pass our logger to restify. });这个 API 只有一个GET /hello?nameNAME端点server.get({ path: /hello, name: SayHello }, function (req, res, next) { var caller req.params.name || caller; req.log.debug(caller is %s, caller); res.send({ hello: caller }); return next(); });运行node server.js后调用端点得到预期的 restify 响应$ curl -iSs http://0.0.0.0:8080/hello?namepaul HTTP/1.1 200 OK Access-Control-Allow-Origin: * Access-Control-Allow-Headers: Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, X-Api-Version Access-Control-Expose-Headers: X-Api-Version, X-Request-Id, X-Response-Time Server: Hello API X-Request-Id: f6aaf942-c60d-4c72-8ddd-bada459db5e3 Access-Control-Allow-Methods: GET Connection: close Content-Length: 16 Content-MD5: Xmn3QcFXaIaKw9RPUARGBA Content-Type: application/json Date: Tue, 07 Feb 2012 19:12:35 GMT X-Response-Time: 4 {hello:paul}注意响应头里的X-Request-Id——稍后它会成为关联日志的关键线索。挂接请求与响应日志接下来给服务加两件事。首先用server.pre在 restify 路由之前挂一个钩子记录请求server.pre(function (request, response, next) { request.log.info({ req: request }, start); // (1) return next(); });这是我们第一次见到log.info这种对象作第一个参数的调用方式。Bunyan 的所有日志方法log.trace、log.debug、log.info等都支持可选的第一个对象参数来附带额外的日志记录字段log.info(object fields, string msg, ...)这里传入的是 restify 的 Request 对象req。前面注册的 req serializer 即将派上用场。还记得端点处理器里已有的那条 debug 日志吗req.log.debug(caller is %s, caller); // (2)注意这里用的是req.log——restify 为每个请求注入的请求级 logger。第二件事监听 restify 服务器的after事件来记录响应server.on(after, function (req, res, route) { req.log.info({ res: res }, finished); // (3) });日志输出逐条剖析再次访问端点服务器端产出的日志如下为清晰起见省略了 listening at 的启动日志$ curl -iSs http://0.0.0.0:8080/hello?namepaul HTTP/1.1 200 OK ... X-Request-Id: 9496dfdd-4ec7-4b59-aae7-3fed57aed5ba ... {hello:paul}服务器日志经node server.js | ./node_modules/.bin/bunyan -j美化{ // (1) name: helloapi, hostname: banana.local, pid: 40442, level: 30, req: { method: GET, url: /hello?namepaul, headers: { user-agent: curl/7.19.7 (universal-apple-darwin10.0) libcurl/7.19.7 OpenSSL/0.9.8r zlib/1.2.3, host: 0.0.0.0:8080, accept: */* }, remoteAddress: 127.0.0.1, remotePort: 59834 }, msg: start, time: 2012-03-28T17:39:44.880Z, v: 0 }这是用request.log.info({req: request}, start)记录的请求开始日志。使用 req 字段会触发创建 Logger 时注册的 req serializer于是整个请求对象method、url、headers、remoteAddress、remotePort被序列化成一个干净的 JSON 子对象而不是被 printf 压扁成一行难以解析的字符串。接着是处理器里的req.log.debug{ // (2) name: helloapi, hostname: banana.local, pid: 40442, route: SayHello, req_id: 9496dfdd-4ec7-4b59-aae7-3fed57aed5ba, level: 20, msg: caller is \paul\, time: 2012-03-28T17:39:44.883Z, v: 0 }以及after事件中的响应日志{ // (3) name: helloapi, hostname: banana.local, pid: 40442, route: SayHello, req_id: 9496dfdd-4ec7-4b59-aae7-3fed57aed5ba, level: 30, res: { statusCode: 200, headers: { access-control-allow-origin: *, access-control-allow-headers: Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, X-Api-Version, access-control-expose-headers: X-Api-Version, X-Request-Id, X-Response-Time, server: Hello API, x-request-id: 9496dfdd-4ec7-4b59-aae7-3fed57aed5ba, access-control-allow-methods: GET, connection: close, content-length: 16, content-md5: Xmn3QcFXaIaKw9RPUARGBA, content-type: application/json, date: Wed, 28 Mar 2012 17:39:44 GMT, x-response-time: 5 } }, msg: finished, time: 2012-03-28T17:39:44.886Z, v: 0 }这三条记录里有两点非常值得注意req_id字段后两条日志消息都带有一个req_id字段由 restify 注入到req.loglogger 上注意它与 curl 响应头里的X-Request-Id是同一个 UUID。这意味着只要你在 API 处理器里用req.log打日志就能轻松地把某个请求相关的所有日志串起来。如果你的系统是多个服务组成的 SOA 架构最佳实践是把 X-Request-Id/req_id 一路传递下去从而能够关联一次顶层请求在整个系统中的处理过程。route字段后两条日志还带有route字段标识 restify 把请求路由到了哪个处理器这里是 SayHello。它除了方便调试对基于日志做端点级监控尤其有用——你可以在不改动应用代码的情况下按 route 统计每个端点的调用与错误情况。回顾一下我们还把所有日志TRACE 级别起写进了hello.log文件。restify 在 trace 级别会记录更多内部操作细节bunyan工具对多行消息以及 req/res 键的格式化输出带颜色也做得相当不错。这才是能真正用起来的日志。其他 JSON 日志方案Bunyan 只是 Node.js 生态中 JSON 日志的众多选项之一。同期已知支持 JSON 日志的还有winstonflatiron 出品的通用 Node.js 日志库也支持 JSON 格式输出logmagicPaul Querna 的 Node 日志库。Paul Querna 本人有一篇关于用 JSON 打日志的著名文章其中展示了 logmagic 的用法并涉及 GELF 日志格式、日志传输、索引与搜索等更上游的话题日志收集、投递、检索是独立于日志产生一端的完整链路。最佳实践与最终思考解析问题不会彻底消失但你的日志可以。只要使用 JSON解析难题就与你无关了。跨节点聚合不同节点日志之间的合并靠的是统一的time字段。跨服务关联在日志中携带统一的req_id或等价物即可在多个服务之间串联同一次请求的处理轨迹。单服务拆多个日志文件是反模式。Apache 那种 access log / error log 分开的经典做法是历史遗留不值得效仿。JSON 日志本身已经提供了足够的结构让工具轻松过滤出某类记录无需靠文件拆分。JSON 日志带来可能性喂给 Splunk 之类的工具变得毫不费力临时字段可以成为一种轻规格的服务间通信渠道——比如带metric字段的记录喂给 statsd带loggly: true字段的记录喂给 loggly.com。本文用 restify 与 bunyan 演示了一个非常简单的 Node.js API 服务 JSON 日志化案例restify 为健壮的 API 服务提供了强力框架bunyan 则为漂亮的 JSON 日志提供了轻量 API以及消费 bunyan JSON 日志的工具的开端。参考本文在仓库中的原始出处与配套实现原文文档apps/site/pages/en/blog/module/service-logging-in-json-with-bunyan.mdfrontmatter 声明了category: module、layout: blog-post、作者trentmick发布日期 2012-03-28原文在次日还做过一次针对 RSS 读者的样式修正。博客数据流水线apps/site/scripts/blog-data/generate.mjs 会读取pages/en/blog下所有 Markdown 的 frontmatter使用 gray-matter生成分类module、year-2012、all与 slug/blog/module/service-logging-in-json-with-bunyan可见这类历史技术文章是 nodejs.org 博客module分类下的常驻内容。博客数据结构定义apps/site/types/blog.ts 中的BlogPost类型。原博文配图apps/site/public/static/images/blog/module/bunyan.png。【免费下载链接】nodejs.orgThe Node.js® Website项目地址: https://gitcode.com/GitHub_Trending/no/nodejs.org创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考