日志在系统开发和运维过程中扮演着极其重要的角色。
无论是定位线上问题、分析异常行为,还是复盘一次生产事故,日志往往都是最直接、也是最可靠的依据。很多时候,系统“看不见”的地方,最终只能通过日志来还原现场。
这篇文章,聊聊日志打印的几点技巧。
日志框架在开发和维护过程中扮演着不可或缺的角色,它能够帮助我们快速定位错误并进行有效的故障排查。
你可能听说过一些与日志框架相关的名词,比如 slf4j、log4j、logback和 JDK Logging ,但他们之间到底是什么关系可能会让人感到困惑,所以我们先介绍他们之间的关系。
slf4j 是一个日志框架的简单门面,它提供了一个统一的日志接口,使得开发者可以在应用中使用统一的日志API。slf4j 本身并不提供日志的实现,而是通过与其他日志框架(如log4j、logback等)结合使用,来实现日志记录的功能。

图中,Logback、Log4j 都是日志框架,每一种日志框架都有自己单独的 API,要使用对应的框架就要使用其对应的 API,这就大大的增加应用程序代码对于日志框架的耦合性。
为了解决这个问题,就是在日志框架和应用程序之间架设一个沟通的桥梁,对于应用程序来说,无论底层的日志框架如何变,都不需要有任何感知。只要门面服务做的足够好,随意换另外一个日志框架,应用程序不需要修改任意一行代码,就可以直接上线。
在短信平台 SDK 模块里,我们并没有依赖日志框架,而是仅仅依赖 slf4j。

SDK 需要打印日志的类定义日志对象 Logger :

下图是代码简化图,Logger 对象定义了不同的日志级别输出方式。

业务应用中, 我们一般使用 debug 调试、info 信息、warn 警告、error 错误 ,等级由低到高 。
我们开发测试一般输出DEBUG级别的日志,生产环境配置只输出INFO级别甚至只输出ERROR级别的日志。
Spring Boot 默认的日志框架 SLF4J + Logback ,官方推荐优先使用带有 -spring 的文件名作为你的日志配置(如使用 logback-spring.xml,而不是 logback.xml ),命名为 logback-spring.xml的日志配置文件,将 xml 文件放至 src/main/resource下面。

下图是一个示例 Logback 文件:

Logback 日志框架中,有三个主要的元素:appender、logger 和 root。
1、Appender(输出器):用于指定日志输出的目的地的组件。
定义了两个输出器(appender),一个用于控制台输出( CONSOLE ),另一个用于文件输出( FILE )。
2、Logger(记录器):用于记录日志事件的组件,负责接收应用程序中产生的日志事件并将它们传送到相应的 Appender。
对于 com.example.Main类,将其日志级别设置为 DEBUG,只有 DEBUG 级别及以上的日志事件才会被记录,并且仅会输出到控制台。
对于 com.example.service 包,将其日志级别设置为 WARN 。只有WARN级别及以上的日志事件才会被记录,并且仅会输出到文件。
3、Root(根Logger):日志记录器树结构的根节点.
我们将根Logger的日志级别设置为INFO,并将其绑定到两个输出器CONSOLE和FILE,这意味着所有未特别指定Logger的日志事件将遵循此配置。
下图是短信服务的 logback 配置文件:

这里面有两个要点:
1、日志格式
我们定义了两个 Appender , 分别输出到控制台、文件,他们的日志格式都是:
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %p %t %logger{36}:%L - %msg%n</pattern>
格式说明:

服务启动后的输出格式如下图所示:

启动日志里,我们可以看到线程名,以及类名以及代码行号,便于定位问题。
2、备份历史文件
因为本地磁盘容量限制,我们不可能永久的保存日志,笔者曾经遇到过因为 log4j 将磁盘占满,导致 tomcat 所有线程阻塞的场景 , 通过 jstack 命令定位到线程阻塞在 log4j 写日志的代码上。
因此我们可以配置文件的保留策略:日志文件大小达到指定阈值或每天生成新日志文件时进行滚动备份,并保留最近的一定数量的历史备份文件。
有的同学可能会问:全量数据去哪里查呢? 我们可以在机器上部署 filebeat 将日志异构存储到 ElasticSearch 或者其他的存储。

1、日志打印出参入参
核心接口以及关键方法的入参和返回值都建议加上日志。
2、日志级别判断
这一条针对debug和trace这种低级别日志,同时为了减少线上调用日志打印没有日志浪费的情况:
User user = new User(666L, "xxxx", "xxxx");
if (log.isDebugEnabled()) {
log.debug("userId is: {}", user.getId());
}
3、使用日志框架SLF4J中的API
还是要重点强调使 SLF4J 的 API ,减少应用程序对于日志实现框架的耦合。
4、占位符而不是+号,性能高且更优雅
Object[] paramArray = {newVal, below, above};
logger.debug("Value {} was inserted between {} and {}.", paramArray);
5、异常堆栈打印
下面的日志输出就不怎么规范:
try {
//业务代码处理
} catch (Exception e) {
// 错误
LOG.error('你的程序有异常啦');
}
正确方式是:
try{
// 业务代码处理
}catch(Exception e){
log.error("你的程序有异常啦",e);
}
另外需要注意,e.getMessage()不会记录详细的堆栈异常信息,只会记录错误基本描述信息,不利于排查问题。
6、禁止在线上环境开启 debug
除了系统产生的大量 debug 日志,还可能存在框架本身输出 debug 日志的问题。在线上环境开启 debug 日志很容易导致日志文件不断增大,最终耗尽磁盘空间,并且可能引发 CPU 和磁盘 I/O 等待,进而直接影响系统的正常运行。
7、不要使用e.printStackTrace()
控制台打印出的堆栈日志跟业务代码日志是交错混合在一起的,容易造成排查异常日志不太方便。
往期推荐:
基于 RuoYi-Vue-Pro 定制了一个后台管理系统 , 开源出来!
如果我的文章对你有所帮助,还请帮忙点赞、在看、转发一下,你的支持会激励我输出更高质量的文章,非常感谢!