为何日志监控需要最佳实践?
良好的日志记录实践对于监控和排除 Node.js 服务器的故障至关重要。它们帮助我们跟踪应用程序中的错误,发现性能优化机会,并对系统进行不同类型的分析(例如在出现中断或安全问题的情况下)以做出关键的产品决策。
1.关于log库的选择 —— 善假于物
console.log的优缺点
虽然console.log()
有其用途,但它不是在生产应用程序中实现日志记录的适当解决方案,它缺乏对良好日志记录设置必不可少的功能和配置选项的支持。
例如,控制台方法不支持日志级别一样warn
,error
或者debug
,尽管它提供了类似的方法 console.warn()
,console.error()
和console.debug()
,但这些只是打印到标准输出或标准错误而不指示日志严重性的函数。
框架选择四要素
选择合适的日志库主要考虑四个问题:方便记录、格式化、消息存储、性能。
由于日志记录器将在整个代码库中大量使用,因此可能会损害应用程序的运行时性能。因此,对库的选择中应该考虑到待选库的性能能力,并查看它与替代品之间的差异。
目前推荐的log库
- Winston — 最流行的日志库,支持多种传输。这使我们可以轻松配置日志的首选存储位置。
- Pino — Pino 最大的吸引力在于它的速度。在许多情况下,它声称比其他框架快五倍。
- Bunyan — 另一个功能丰富的日志框架,默认以 JSON 格式输出,并提供用于查看日志的 CLI 工具。
- Roarr — Roarr 是一种不同类型的记录器,适用于 Node.js 和浏览器。
本期以Winston为例,输出一些相关的示例。(因为他是目前较流行的日志框架)
2. 日志级别 —— 快速甄别重要紧急事件
日志的级别是一种最简单、有效的方式,用以区分系统中事件类型的解决方案;如果在应用程序中正确使用日志级别,将很容易区分需要立即解决的关键事件与纯信息性事件。
尽管日志系统为严重性级别提供了不同的名称,但概念在很大程度上保持不变。以下比较常见的日志级别,无论是选择哪种日志记录框架(按严重性降序排列):
- FATAL:用于表示灾难性情况 - 应用程序无法恢复。此级别的日志记录通常表示程序结束。
- ERROR:表示系统中的错误情况,它恰好停止了特定操作,而不是整个系统。当第三方 API 返回错误时,您可以在此级别进行记录。
- WARN:表示不受欢迎或异常的运行时条件,但不一定是错误。例如,当主要数据源不可用时,可以使用备份数据源。
- INFO:信息消息纯粹是提供信息。用户驱动的或特定于应用程序的事件可能会在此级别记录。此级别的常见用途是记录有趣的运行时事件,例如服务的启动或关闭。
- DEBUG:用于表示故障排除可能需要的诊断信息。
- TRACE:在开发过程中捕获有关应用程序行为的每一个可能的细节。
3.日志输出标准 —— 高效处理
输出的标准最好能满足两方面需求:
- 人类阅读
- 机器阅读
易于人类阅读
好的日志格式可以让人们方便的知道其输出的含义,以及代表的信息;它有时候并不仅包含报错的原因,可能会带有报错的参数、报错的上下游信息等;
易于机器阅读
易于机器阅读的主要原因是可以收集日志,并统一格式化,作为机器收集,自动报警,智能报警的前提;
example
以winston框架为例
const { createLogger, format, transports } = require('winston');
const logger = createLogger({
format: format.combine(format.timestamp(), format.json()),
transports: [new transports.Console({})],
});
他可以输出以下信息
{"message":"Connected to DB!","level":"info","timestamp":"2021-07-28T22:35:27.758Z"}
{"message":"已收到付款","level":"info","timestamp":"2021-07-28T22:45:27.758Z"}
4.写描述信息 —— 更有价值
日志条目应充分描述它们所代表的事件。每条消息都应该针对具体情况,并且应该清楚地解释当时发生的事件。在紧急情况下,您的日志条目可能是帮助您了解发生了什么的唯一信息来源,因此正确记录这方面的信息非常重要!
下面是个例子
request failed,will retry
上面这条消息没有提供以下几个方面的任何帮助:
- 失败的请求信息
- 失败的原因分析
- 请求重试前的时间间隔
下面是个更好的案例
"POST" request to "https://example.com/api" failed. Response code: "429", response message: "too many requests". Retrying after "60" seconds.
第二条消息要好得多,因为它提供了有关失败请求的足够信息,包括状态代码和响应消息,并且还指出该请求将在 60 秒后重试。如果所有消息都有相关的描述,那么我们在理解日志时候会更愉快。
5.为日志添加上下文 —— 添加关键信息
除了写一些必要的描述信息外,可以为日志信息添加一些关键上下文信息,如:关键数据点、事件的时间戳和相关函数等;
案例
比如在一笔订单日志中,可能包含多个关键数据信息:
- 会话标识符
- 用户名和 ID
- 产品或交易标识符
- 用户所在的当前页面
上述每个数据点可以用于分析用户在整个下单过程中的流程。如果发生重要事件,可用数据将自动附加到日志输出中,并且可以识别:
- 导致事件发生的情况(例如经历事件的用户)
- 它发生的页面
- 触发事件的交易和产品 ID。
我们可以通过这些关键信息,对日志进行过滤。达到知晓并能复盘该用户在系统中流转链路的目的;
Winston案例
Winston提供了全局信息注入的能力;
全局注入service信息:
const logger = createLogger({
format: format.combine(format.timestamp(), format.json()),
defaultMeta: {
service: 'billing-service',
},
transports: [new transports.Console({})],
});
输出的结果:
{"message":"订单"1234"已成功处理","level":"info","service":"billing-service","timestamp":"2021-07-29T10:56:14.651Z "}
6.避免记录敏感信息 —— 安全第一
无论我们所在的行业是否对合规性有严格的规定(例如医疗保健或金融),都必须避免在日志中包含敏感信息。
敏感信息包括身份证信息、地址、密码、信用卡详细信息、访问令牌和类似的数据类型。由于日志消息通常以纯文本形式存储,如果日志落入他人之手,这些数据将被暴露。我们还必须通过记录某些信息来确保您不会违反适用于您的产品运营所在国家/地区的法规(例如 GDPR)。
通过最小化原则,系统的哪些部分使用该数据,可以避免意外泄漏日志中的敏感数据。例如,信用卡详细信息只能由系统的计费组件看到,敏感数据应该保留在 URL 之外 - 在可能的情况下进行编辑。
虽然这不是一个万无一失的解决方案,但也可以使用阻止列表来防止特定字段进入日志。