作者:京东零售 张洪
前言
随着互联网和大数据的迅猛发展,分布式日志系统和日志分析系统已广泛应用,几乎所有应用程序都使用各种日志框架记录程序运行信息。因此,作为工程师,了解主流的日志记录框架非常重要。虽然应用程序的运行结果不受日志的有无影响,但没有日志的应用程序是不完整的,甚至可以说是有缺陷的。优秀的日志系统可以记录操作轨迹、监控系统运行状态和解决系统故障。
Java 日志框架进化史
早期 Java 日志框架没有制定统一的标准,使得很多应用程序会同时使用多种日志框架。Java 日志框架的发展历程大致可分为以下几个阶段:
1.Log4j: Apache Log4j是一种基于Java的日志记录工具。该项目由Ceki Gülcü于1999年创建,并几乎成为了Java日志框架的实际标准。
2.JUL: Apache 希望将 Log4j 引入 jdk,不过被 sun 公司拒绝了。随后,sun 模仿 Log4j,在 jdk1.4 中引入了 JUL(java.util.logging)。
3.Commons Logging: 为了解耦日志接口与实现,Apache在2002年推出了JCL(Jakarta Commons Logging)。JCL定义了一套日志接口,具体的实现由Log4j或JUL完成。Commons Logging使用动态绑定来实现日志记录,编码时只需要使用它定义的接口即可,程序运行时会使用ClassLoader来查找和加载底层的日志库,因此可以灵活选择Log4j或JUL来实现日志功能。
4.Slf4j&Logback: Ceki Gülcü与Apache基金会在Commons-Logging标准上存在分歧。后来,Ceki Gülcü离开了Apache,并创建了Slf4j和Logback两个项目。Slf4j是一个日志门面,仅提供接口,可以支持Logback、JUL、log4j等日志实现。而Logback则提供了具体的实现。相比于log4j,Logback具有更快的执行速度和更完善的功能。
5.Log4j 2: 为了保持在Java日志领域的地位,防止JCL和Log4j被Slf4j和Logback取代,Apache在2014年推出了Log4j 2。Log4j 2与log4j不兼容,经过大量深度优化,其性能得到显著提升。
日志框架介绍
在上文中已经提及,目前常用的日志框架有 Log4j,Log4j 2,Commons Logging,Slf4j,Logback,JUL。这些日志框架可以分为两种类型:门面日志和日志系统。
日志门面
日志门面(Logging Facade) 是一种设计模式,用于在应用程序中实现日志记录的抽象层。它提供了一组统一的接口和方法,即相应的 API,而不提供具体的接口实现。日志门面在使用时,可以动态或者静态地指定具体的日志框架实现,解除了接口和实现的耦合,使用户可以灵活地选择日志的具体实现框架。
日志系统
日志系统(Logging System) 是指用于记录和管理应用程序运行时产生的日志信息的软件工具或框架。与日志门面相对,它提供了具体的日志接口实现,应用程序通过它执行日志打印的功能,如日志级别管理、日志格式化、日志输出目标设置等。常见的日志系统包括Log4j、Logback、Java Util Logging等。
通过使用日志门面,我们可以在应用程序中使用统一的API进行日志记录,而具体的日志实现可以根据需要选择和配置。这样,我们可以根据项目需求和团队喜好来灵活选择、切换和配置日志系统,而不会对应用程序代码造成太大影响。
避免环形依赖
Slf4j 的作者 Ceki Gülcü 当年因为觉得 Commons-Logging 的 API 设计的不好,性能也不够高,因而设计了 Slf4j。而他为了 Slf4j 能够兼容各种类型的日志系统实现,还设计了相当多的 adapter 和 bridge 来连接,如下图所示:
鉴于此,在引入日志框架依赖的时候要尽力避免,比如以下组合就不能同时出现:
•jcl-over-slf4j 和 slf4j-jcl
•log4j-over-slf4j 和 slf4j-log4j12
•jul-to-slf4j 和 slf4j-jdk14
日志框架的使用选择
常用的组合使用方式是 Slf4j & Logback 组合使用,Commons Logging & Log4j 组合使用。
推荐:
Slf4j & Logback
原因:
Slf4j 实现机制决定 Slf4j 限制较少,使用范围更广。相较于 Commons-Logging,Slf4j 在编译期间便静态绑定本地的 Log 库,其通用性要好得多;
Logback 拥有更好的性能。Logback 声称:某些关键操作,比如判定是否记录一条日志语句的操作,其性能得到了显著的提高,这个操作在 Logback 中只需 3 ns,而在 Log4j 则需要 30 ns;
Slf4j 支持参数化,使用占位符号,代码更为简洁,如下例子:
// 在使用 Commons-Logging 时,通常的做法是
if(log.isDebugEnabled()){
log.debug("User name: " + user.getName() + " buy goods id :" + good.getId());
}
// 在 Slf4j 阵营,你只需这么做:
log.debug("User name:{} ,buy goods id :{}", user.getName(),good.getId());
Logback 的所有文档是免费提供的,Log4j 只提供部分免费文档而需要用户去购买付费文档;
MDC (Mapped Diagnostic Contexts) 用 Filter,将当前用户名等业务信息放入 MDC 中,在日志 format 定义中即可使用该变量。具体而言,在诊断问题时,通常需要打出日志。如果使用 Log4j,则只能降低日志级别,但是这样会打出大量的日志,影响应用性能;如果使用 Logback,保持原定日志级别而过滤某种特殊情况,如 Alice 这个用户登录,日志将打在 DEBUG 级别而其它用户可以继续打在 WARN 级别。实现这个功能只需加 4 行 XML 配置;
自动压缩日志。RollingFileAppender 在产生新文件的时候,会自动压缩已经打出来的日志文件。压缩过程是异步的,因此在压缩过程中应用几乎不会受影响。
Slf4j+Logback入门实践
maven依赖
pom.xml
<!--日志框架接口-->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</dependency>
<!--日志框架接口实现-->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
</dependency>
<!--日志框架核心组件-->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-core</artifactId>
</dependency>
<!--自动化注解工具-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.16</version>
</dependency>
配置文件
logback.xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!--默认日志配置-->
<include resource="org/springframework/boot/logging/logback/defaults.xml"/>
<!-- 控制台日志 -->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder charset="UTF-8">
<pattern>${CONSOLE_LOG_PATTERN}</pattern>
</encoder>
</appender>
<!-- Info日志 -->
<appender name="FILE-INFO" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_PATH}/${LOG_FILE}-info.log</file>
<append>true</append>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>INFO</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>NEUTRAL</onMismatch>
</filter>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${LOG_PATH}/${LOG_FILE}-info-%d{yyyy-MM-dd}.%i.log</fileNamePattern> <!-- 日志文件的路径和名称 -->
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>200MB</maxFileSize> <!-- 单个日志文件的最大大小 -->
</timeBasedFileNamingAndTriggeringPolicy>
<maxHistory>15</maxHistory> <!-- 保留的历史日志文件数量 -->
<totalSizeCap>2GB</totalSizeCap> <!-- 所有日志文件的总大小上限 -->
<cleanHistoryOnStart>true</cleanHistoryOnStart> <!-- 在启动时清除历史日志文件 -->
</rollingPolicy>
<encoder charset="UTF-8">
<pattern>${FILE_LOG_PATTERN}</pattern>
</encoder>
</appender>
<!-- Warn日志 -->
<appender name="FILE-WARN" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_PATH}/${LOG_FILE}-warn.log</file>
<append>true</append>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>WARN</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${LOG_PATH}/${LOG_FILE}-warn-%d{yyyy-MM-dd}.%i.log</fileNamePattern> <!-- 日志文件的路径和名称 -->
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>200MB</maxFileSize> <!-- 单个日志文件的最大大小 -->
</timeBasedFileNamingAndTriggeringPolicy>
<maxHistory>15</maxHistory> <!-- 保留的历史日志文件数量 -->
<totalSizeCap>2GB</totalSizeCap> <!-- 所有日志文件的总大小上限 -->
<cleanHistoryOnStart>true</cleanHistoryOnStart> <!-- 在启动时清除历史日志文件 -->
</rollingPolicy>
<encoder charset="UTF-8">
<pattern>${FILE_LOG_PATTERN}</pattern>
</encoder>
</appender>
<!-- Error日志 -->
<appender name="FILE-ERROR" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_PATH}/${LOG_FILE}-error.log</file>
<append>true</append>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>ERROR</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${LOG_PATH}/${LOG_FILE}-error-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>200MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
<maxHistory>15</maxHistory>
<totalSizeCap>2GB</totalSizeCap>
<cleanHistoryOnStart>true</cleanHistoryOnStart>
</rollingPolicy>
<encoder charset="UTF-8">
<pattern>${FILE_LOG_PATTERN}</pattern>
</encoder>
</appender>
<!-- 异步输出 -->
<appender name="info-asyn" class="ch.qos.logback.classic.AsyncAppender">
<appender-ref ref="FILE-INFO"/>
<queueSize>512</queueSize> <!-- 异步队列的大小 -->
</appender>
<appender name="warn-asyn" class="ch.qos.logback.classic.AsyncAppender">
<appender-ref ref="FILE-WARN"/>
<queueSize>512</queueSize> <!-- 异步队列的大小 -->
</appender>
<appender name="error-asyn" class="ch.qos.logback.classic.AsyncAppender">
<appender-ref ref="FILE-ERROR"/>
<queueSize>512</queueSize>
</appender>
<!-- 应用日志 -->
<logger name="com.improve.fuqige.bronze" additivity="false">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="FILE-INFO"/>
<appender-ref ref="FILE-WARN"/>
<appender-ref ref="FILE-ERROR"/>
</logger>
<!-- 总日志出口 -->
<root level="${logging.level.root}">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="info-asyn"/>
<appender-ref ref="warn-asyn"/>
<appender-ref ref="error-asyn"/>
</root>
</configuration>
applicantion.properties
logging.file=fuqige-bronze
logging.path=XXXXXX/Logs/XXXXXX
logging.level.root=info
logging.level.com.improve.fuqige.bronze=info
logging.pattern.console=%cyan(%d{yyyy-MM-dd HH:mm:ss.SSS}) %yellow([%thread]) %highlight(%-5level) %boldGreen(%logger{80}[LineNumber:%L]): %highlight(%msg%n)
logging.pattern.file=%d{yyyy-MM-dd HH:mm:ss.SSS} [%X{requestId}] %-5level --- [%thread] %logger{80}[LineNumber:%L]: %msg%n
测试用例
@Slf4j
@RestController
@RequestMapping("/test")
public class TestController {
@GetMapping("/hello")
public String hello() {
log.info("进来了!");
log.warn("进来了!");
log.error("进来了!");
return "hello, world! requestId=" + MDC.get("requestId");
}
}
参考资料
Java 日志框架: https://zhuanlan.zhihu.com/p/365154773
SLF4J框架常见的用法和最佳实践: https://juejin.cn/post/7215569601161166906