在 02 课时,我带你重新认识了系统日志,介绍了日志在系统中的重要性。既然日志如此重要,那我们应该如何编写它呢?
这一节,我将带你从编写日志的工具、编写日志的方式,以及日志编写后的管理,就像是购物的售前、售中、售后,这 3 个方面来讲解,怎么样才可以写出更具有“可观测性”的日志内容。
在编写日志之前,咱们先来了解一下有哪些日志框架可以协助我们编写日志。
在介绍日志框架之前,我需要说明一下,如果你仍在使用 System.out.println、Exception.printStackTrace 或类似的控制台输出日志的方式,我推荐你改用第三方日志框架编写。这种控制台输出的方式,可以从它们的源码了解到它们是线程同步的,大量使用这种方式,会对程序性能造成严重的影响,因为它们同一时间只能有一个线程在进行执行。
日志框架在系统中一般分为 2 种类型:日志实现框架和日志接口框架。下面我会对它们分别说明。
顾名思义,就是日志的实现方,每个框架都提供自己的日志管理和写入的方式。Java 中的日志框架很不统一,这里我就以认知度最高的 3 个项目来说明。
对于这三种框架,目前市面上都有在用,但是大多是 logback 和 log4j 2.x。虽然异步日志的效率 logback 和 log4j 2.x 相差无几,但 log4j 2.x 仍有些微弱的优势。
可以看到,Java 中是存在多种不同日志框架的实现的,这就会造成 2 个问题:
由此就出现了面向接口的日志框架,它提供了统一的 API。开发人员在编写代码的时候,直接使用这套面向接口的日志框架,当业务项目人员在使用时,只需要选择好实现框架,就可以统一日志实现框架。
目前使用最为广泛的日志接口框架是 SLF4J,出自 logback 的开发者,目前基本已经形成规范。SLF4J 提供了动态占位符的功能,大大提高了程序的性能,无须开发人员再对参数信息进行拼接。
比如默认情况下程序是 info 级别的,在原先的代码方式中想要进行日志输出需要自行拼接字符串:
logger.debug("用户" + userId + "开始下单:" + orderNo + ",请求信息:" + Gson.toJson(req));
这会产生一个问题,系统中如果存在大量类似的代码,同时系统只输出 info 及 info 以上级别的日志,那么,在输出 debug 日志时会产生大量的字符串,而并不会输出 debug 日志,最后造成字符串不停地拼接,浪费系统性能。此时,SLF4J 就可以使用占位符的功能编写日志,比如像下面这样:
logger.debug("用户{}开始下单:{},请求信息:", userId, orderNo, Gson.toJson(req));
通过这样的形式,SLF4J 就可以根据日志等级判断,只对符合要求的日志进行数据拼接和打印。
有些时候日志输出需要进行数值计算,或者 JSON 转换,此时就需要一定的计算任务。但方法调用如果被当作参数传递的话一样会被执行,所以 Java8 中 SLF4J 还可以通过 Supplier 来传递。如下所示:
logger.debug("用户{}开始下单:{},请求信息:", userId, orderNo, () -> Gson.toJson(req))
可以看到,SLF4J 不仅为我们制定了一套面向接口开发的方式,还为我们明确了如何有效地编写日志。这也是为什么越来越多人喜欢用 SLF4J。
在详细介绍了我们在开发时需要使用的日志框架后,我将正式展开我们的标题:“如何编写‘可观测’的日志?”我会从日志编写位置、写入性能、占位符、可读性、关键信息隐蔽、减少代码位置信息的输出、文件分类、和日志 review 这 8 个点来讲解,并将它们分成了 2 个方向:
日志编写的位置可以说是重中之重,好的日志位置可以帮你解决问题,也可以让你更加了解代码的运行情况。我总结了几点比较重要的编写日志的位置,以供参考。
日志的写入性能则会受到如下 5 个因素的影响:
好的日志一定是便于你去排查问题的。在编写日志时你一定要思考这个日志可以帮你做什么。
在介绍日志接口框架时我提到过,在日志编写时尽可能地选择基于占位符的编写方式。这里我会告诉你为什么要用占位符:
日志的可读性也是日志编写的关键之一。一个好的日志就像一篇好的文章,能让你很快了解到这个日志中的关键信息。我在工作中发现很多人写的日志都是无意义的,根本无法帮你定位问题的根源,比如像下面的这段代码:
boolean success = doSomeThing();
if (success) {
logger.info("数据保存成功!");
}
这段代码乍一看似乎没什么问题,但是运行后系统会大量地输出“数据保存成功!”的消息,这个输出和没有是一样的,起不到任何的作用。
我总结了几点在日志中容易遗漏的信息:
对于关键的信息不显示或者进行掩码显示,以免信息被盗取后出现数据内容泄漏。推特在 2018 年曾将用户的密码打印在日志中,这一行为泄露了 3.3 亿人的密码。
如果不是必要,尽量不要在日志格式中输出当前日志所在的代码行和方法名称信息。如果你看过 logback,log4j 的源码你就知道,这都是通过获取当前线程堆栈快照信息来进行实现的,这种实现方式会极大地影响程序执行的效率。
在 log4j 的文档中有这样一段话:“使用同步方式进行获取位置信息会慢 1.3 到 5 倍,如果是使用异步日志,因为会涉及跨线程获取位置信息,会慢 30 到 100 倍。原文: https://logging.apache.org/log4j/2.0/manual/async.html#Location。
所以,关闭代码位置信息的输出可以节省系统资源的使用,提升性能。
文件分类可以帮助你提高检索日志信息时的效率。将不同的业务逻辑按照不同的日志文件来分类,可以保证你看到的信息都是和这个功能相关的,不会被其他的日志干扰。这也是在大型系统中经常会使用到的功能。
比如拉勾的单点登录系统,就会将用户的极验验证功能和登录验证功能拆成两个单独的日志文件,当出现问题时,可以根据相关功能的日志来快速筛查问题,减少了筛选所需的时间。
每一次功能上线后,除了要对业务功能进行回归,还要对日志进行观察,确认日志内容的输出情况,比如日志内容是否符合预期,会不会有不合适的地方?
好的日志不是一次就能写好的,一定是要和代码一样不停地迭代,才能写出更方便处理问题,也更具有可读性的日志。
就像店家在卖出商品后还要负责其售后,编写完日志,对于它的管理也是十分重要的。好的日志管理方式可以提高阅读日志的效率,而这需要开发人员和运维人员共同协作。
日志的格式布局会影响运维人员将这些日志内容收集与管理的效率。如果编写者和管理者能够通过协商,规定出一套完整的日志格式,这样就能在排查问题时事半功倍。
我会简单介绍几点在日志编写时需要注意的事项:
日志归档是一件很重要的事情。如果你将日志内容全部写到一个文件中,这个日志文件会变得越来越烦冗,不利于日志的收集和查看。
一般情况下,我们会对日志按照日期来归档,每天生成一个日志文件,这样在日志备份和恢复时,可以按照日期来进行。如果感觉天级别的日志仍然太大了,可以考虑按照小时细分。
这节课,我带你了解了日志编写的工具、日志编写需要注意的 8 个事项以及日志管理的方式,有哪些是你原来犯过的错误,又有哪些是你原来没有想到的呢?欢迎你在留言区分享与讨论。希望你在日后的日志编写中可以注意到这些问题。