打印日志

打印什么 (What Log)

where: 这条日志在哪个服务器,哪个进程,哪个模块发生的?

who: 这条日志是由谁产生的?

why: 产生的原因是什么?日志中需要记录关键参数,出错时的关键原因等

输出日志时要考虑日志的使用者,例如如果日志主要由系统的运维人员来看,那就不能输出:

[INFO] requestID:b1946ac92492d2347c6235b4d2611184, ErrorCode:1426

至少应该是:

[INFO] requestID:b1946ac92492d2347c6235b4d2611184, ErrorCode:1426, Message: callback request (to http://example.com/callback) failed due to socket timeout

这样运维人员一眼就能清楚问题的原因,而不需要再通过开发来查看 ErrorCode 对应的具体错误。

可能会遗漏的日志

整理一下通常情况下会遗漏的日志:

  • 系统的配置参数: 系统在启动过程中通常会首先读启动参数,可以在系统启动后将这些参数输出到日志中,方便确认系统是按照期望的参数启动的;
  • 后台定期执行的任务: 如定期更新缓存的任务,可以记录任务开始时间,任务结束时间,更新了多少条缓存配置等等,这样可以掌握定期执行的任务的状态;
  • 异常处理逻辑: 如对于分布式存储系统来说,当系统在一个存储节点上读数据失败时,需要去另一个数据节点上进行重试,可以将读数据失败这件事情记录下来,之后可以通过对日志的分析确认是否某些节点的磁盘可能存在故障。

再比如,如果系统需要请求一个外部资源,可以将请求这个外部资源偶尔失败又重试成功这件事情记录下来,具体来说:

[INFO] requestID:b1946ac92492d2347c6235b4d2611184, auth request (to http://auth1.example.com/v2) timeout ... 1 try

[INFO] requestID:b1946ac92492d2347c6235b4d2611184, auth request (to http://auth1.example.com/v2) timeout ... 2 try

[INFO] requestID:b1946ac92492d2347c6235b4d2611184, auth request (to http://auth1.example.com/v2) success

要好于

[INFO] requestID:b1946ac92492d2347c6235b4d2611184, auth request (to http://auth1.example.com/v2) success

因为前者可以让我们预判被依赖的服务器服务质量有风险,也许需要进行扩容;

日志中需要记录关键参数,出错时的关键原因等。例如:

[INFO] requestID:b1946ac92492d2347c6235b4d2611184, auth failed

[INFO] requestID:b1946ac92492d2347c6235b4d2611185, content digest does not match

[INFO] requestID:b1946ac92492d2347c6235b4d2611186, request ip not in whitelist

就不如:

[INFO] requestID:b1946ac92492d2347c6235b4d2611184, auth failed due to token expiration

[INFO] requestID:b1946ac92492d2347c6235b4d2611185, content digest does not match, expect 7b3f050bfa060b86ba781151c563c953, actual f60645e7107917250a6408f2f302d048

[INFO] requestID:b1946ac92492d2347c6235b4d2611186, request ip(=202.17.34.1) not in whitelist

日志级别 (Log Level)

日志分不同级别,按重要程度从高到低排序说明:

  • fatal 表示需要立即被处理的系统级错误。当该错误发生时,表示服务已经出现了某种程度的不可用,系统管理员需要立即介入。这属于最严重的日志级别,因此该日志级别必须慎用,如果这种级别的日志经常出现,则该日志也失去了意义。通常情况下,一个进程的生命周期中应该只记录一次 FATAL 级别的日志,即该进程遇到无法恢复的错误而退出时。当然,如果某个系统的子系统遇到了不可恢复的错误,那该子系统的调用方也可以记入 FATAL 级别日志,以便通过日志报警提醒系统管理员修复;
  • error 该级别的错误也需要马上被处理,但是紧急程度要低于 FATAL 级别。当 ERROR 错误发生时,已经影响了用户的正常访问。从该意义上来说,实际上 ERROR 错误和 FATAL 错误对用户的影响是相当的。FATAL 相当于服务已经挂了,而 ERROR 相当于好死不如赖活着,然而活着却无法提供正常的服务,只能不断地打印 ERROR 日志。特别需要注意的是,ERROR 和 FATAL 都属于服务器自己的异常,是需要马上得到人工介入并处理的。而对于用户自己操作不当,如请求参数错误等等,是绝对不应该记为 ERROR 日志的;
  • warn 该日志表示系统可能出现问题,也可能没有,这种情况如网络的波动等。对于那些目前还不是错误,然而不及时处理也会变为错误的情况,也可以记为 WARN 日志,例如一个存储系统的磁盘使用量超过阀值,或者系统中某个用户的存储配额快用完等等。对于 WARN 级别的日志,虽然不需要系统管理员马上处理,也是需要及时查看并处理的。因此此种级别的日志也不应太多,能不打 WARN 级别的日志,就尽量不要打;
  • info 该种日志记录系统的正常运行状态,例如某个子系统的初始化,某个请求的成功执行等等。通过查看 INFO 级别的日志,可以很快地对系统中出现的 WARN,ERROR,FATAL 错误进行定位。INFO 日志不宜过多,通常情况下,INFO 级别的日志应该不大于 TRACE 日志的 10%;
  • debug or trace 这两种日志具体的规范应该由项目组自己定义,该级别日志的主要作用是对系统每一步的运行状态进行精确的记录。通过该种日志,可以查看某一个操作每一步的执 行过程,可以准确定位是何种操作,何种参数,何种顺序导致了某种错误的发生。可以保证在不重现错误的情况下,也可以通过 DEBUG(或 TRACE)级别的日志对问题进行诊断。需要注意的是,DEBUG 日志 也需要规范日志格式,应该保证除了记录日志的开发人员自己外,其他的如运维,测试人员等也可以通过 DEBUG(或 TRACE)日志来定位问题;

实际代码示范

请不要使用字符串拼接的形式输出日志,请使用占位符 字符串%s 对象%o 数字%d 打印日志

import { Logger } from "@app/loaders/logger";
// 指定模块
export const logger = Logger.child({ module: "test-module" });
// 打印
logger.debug("username: %s, balance %d, detail %o", "jeek", 2000, {
sex: "m",
age: 23,
});
logger.info("username: %s, balance %d, detail %o", "jeek", 2000, {
sex: "m",
age: 23,
});
logger.warn("username: %s, balance %d, detail %o", "jeek", 2000, {
sex: "m",
age: 23,
});
logger.error("username: %s, balance %d, detail %o", "jeek", 2000, {
sex: "m",
age: 23,
});
logger.fatal("username: %s, balance %d, detail %o", "jeek", 2000, {
sex: "m",
age: 23,
});