前言
在现如今的应用中,日志已经成为了一个非常重要的工具。通过系统打印的日志,可以监测系统的运行情况,排查系统错误的原因。日志从最早期的 System.out.print
到如今各种成熟的框架,使得日志打印更加规范化和清晰化。尤其是 SLF4J 的出现,为日志框架定义了通用的 FACADE 接口和能力。只需要在应用中引入 SLF4J 包和具体实现该 FACADE 的日志包,上层应用就可以只需要面向 SLF4J 接口编程,而无需关心具体的底层的日志框架,实现了上层应用和底层日志框架的解耦。Logback 作为一个支持 SLF4J 通用能力的框架,成为了炙手可热的日志框架之一。今天就来稍微了解一下 Logback 日志的一些基础能力以及配置文件。
快速上手 Logback
引入 MAVEN 依赖
logback 主要由三个模块组成,分别是 logback-core
,logback-classic
和logback-access
。其中 logback-core
是整个 Logback 的核型模块,logback-classic
支持了 SLF4J FACADE,而 logback-access
则集成了 Servlet 容齐来提供 HTTP 日志功能,适用于 web 应用。下面主要是基于 logback-classic
来进行介绍。
引入 logback-classic 的包如下:
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.3.0-alpha5</version>
</dependency>
上面拉取的 Maven 包基于传递性远离,会自动拉取 logback-classic,logback-core 和 slf4j-api.jar,因此无需在项目中再额外声明 SLF4J 和 logback-core 的依赖。
使用 Logback
因为 logback-classic
实现了 SLF4J FACADE,所以上层应用只需要面向 SLF4J 的调用语法即可。下面代码展示了如何获取到 Logger 对象用来打印日志。
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ch.qos.logback.classic.LoggerContext;
import ch.qos.logback.core.util.StatusPrinter;
public class HelloWorld2 {public static void main(String[] args) {
// 这里的 Logger 和 LoggerFactory 均为 SLF4J 的类,真正调用时会使用 Logback 的日志能力
//getLogger 方法中传入的是 Logger 的名称,这个名称在后面讲解配置文件中的 <logger> 时会继续提到
Logger logger = LoggerFactory.getLogger("chapters.introduction.HelloWorld2");
// 打印一条 Debug 级别的日志
logger.debug("Hello world.");
// 获取根 Logger,使用场景比较少
Logger rootLogger = LoggerFactory.getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME);
}
}
日志级别
Logback 中每一个 Logger 都有对应的日志级别,该日志级别可以是 Logger 自己定义的,也可以是从父 Logger 上继承下来的。Logback 一共支持 5 个日志级别,从高到低分别是 ERROR,WARN,INFO,DEBUG,TRACE。Logger 的日志级别决定了哪些级别的日志可以被输出。只有大于等于该 Logger 级别的日志才会被打印出来。比如假设上文中获取的名为 ”chapters.introduction.HelloWorld2″ 的 Logger 日志级别为 INFO,则调用 logger.debug(“xxx”)不会输出日志内容,因为 DEBUG 日志级别低于 INFO 日志级别。
日志级别可以帮助我们控制日志打印的粒度,比如在开发环境可以将日志级别设置到 DEBUG 帮助排查问题,而在生产环境则可以将日志级别设置到 INFO,从而减少不必要的打印日志带来的性能影响。
参数化输出
有时候我们往往并不只是打印出一条完整的日志,而是希望在日志中附带一些运行中参数,如下:
Logger logger = LoggerFactory.getLogger("chapters.introduction.HelloWorld2");
logger.debug("Hello World To" + username);
上文的日志中除了打印了一些结构化的语句,还拼接了运行时执行这段逻辑的用户的名称。这里就会带来一个问题,即字符串拼接的问题。虽然 JVM 对 String 字符串的拼接已经进行了优化,但是假如当前的日志级别为 INFO,那么这段代码所执行字符串拼接操作就是完全不必要的。因此,建议在代码加上一行日志级别的判断进行优化,如下:
// 非 debug 级别不会执行字符串拼接操作,但是 debug 级别会执行两次 isDebugEnabled 操作,性能影响不大
if(logger.isDebugEnabled()) {logger.debug("Hello World To" + username);
}
但是,logback 并不推荐在系统中使用字符串拼接的方式来输出日志,而是提倡使用参数传递的方式,由 logback 自己来执行日志的序列化。如下:
//logger 方法会判断是否为 debug 级别,再决定将 entry 序列化拼接如字符串
logger.debug("The entry is {}.", entry);
这种日志输出方式就无需额外包一层日志级别的判断,因为 logger.debug 方法内部自己会判断一次日志级别,再去执行日志内容转码的操作。注意,传入的参数必须实现了 toString 方法,不然日志在对对象进行转码时,只会打印出对象的内存地址,而不是对象中的具体内容
整体架构
前文已经简单介绍了 logback 包含的三个主要模块,以及如何在代码中基于 SLF4J FACADE 自由的使用日志框架。下面开始从配置文件的角度来了解如何配置 Logback。
Logback 主要支持 XML 和 groovy 结构的配置文件,下文中将以 XML 结构为基础进行介绍。
上图为官网中对 Logback 配置文件整体结构的描述。配置文件以 <configuration>
作为根元素,其下包含 1 个 <root>
元素用于定义根日志的配置信息,还有 0 到多个 <logger>
元素以及 0 到多个 <appender>
元素。其中 <logger>
元素对应了应用中通过 LoggerFactory.getLogger()
获取到的日志工具,<appender>
元素定义了日志的输出目的地,一个 <logger>
可以关联多个<appender>
,即允许将同样的一行日志输出到多个目的地。
一个简单的 Logback 配置文件如下:
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<!-- encoders are by default assigned the type
ch.qos.logback.classic.encoder.PatternLayoutEncoder -->
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<root level="debug">
<appender-ref ref="STDOUT" />
</root>
</configuration>
该配置文件声明了一个输出到控制台名称为 STDOUT 的 appender,再声明了 root logger 的日志级别为 debug,且规定将日志输出到 STDOUT 流中。
logback 允许多配置文件,其加载时读取配置文件的顺序如下:
- 在 classpath 查找 logback-test.xml(一般 classpath 为 src/test/resources)
- 如果该文件不存在,logback 尝试寻找 logback.groovy
- 如果该文件不存在,logback 尝试寻找 logback.xml
- 如果该文件不存在,logback 会在 META-INF 下查找
[com.qos.logback.classic.spi.Configurator](http://logback.qos.ch/xref/ch/qos/logback/classic/spi/Configurator.html) 接口的实现
- 如果依然找不到,则会使用默认的 BasicConfigurator,导致日志直接打印到控制台,日志等级为 DEBUG,日志的格式为_%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} – %msg%n_
配置文件语法
在简单的了解了 logback 配置文件的基础结构后,这一章详细介绍一下 logback 中比较常用的几个标签以及各自代表的含义。
configuration 标签
作为配置文件的根标签,configuration 更多的是对整个 Logback 配置读取的模式进行定义,configuration 标签汇中可以定义的属性如下:
- debug: 默认 debug 值为 false,如果 debug 设置为 true 的话,则无论配置读取成功与否,都会将日志框架的状态打印出来,为 false 的话则只有在读取配置出错时才会打印状态日志。
- scan:默认为 false,将 scan 设为 true 的话,则 logback 会自动的定期扫描配置文件,如果配置文件发生变更,则 logback 能够快速识别并重新配置。可以通过 scanPeriod 来覆盖默认的扫描间隔。这个功能在生产环境建议不要开启,因为基本上生产环境的日志框架的配置都是稳定的。只有在开发环境需要调试日志框架的行为时,可以将该功能开启,减少因为修改配置进行调试而重启应用的麻烦。
logger 标签
logger 是日志流隔离的基本单元,每个 logger 都会绑定到一个 LoggerContext。Logger 之间存在树状层级关系,即 A Logger 可以是 B Logger 的父 Logger。而它们之间的层级关系则是根据 logger 的名称来决定的。假如 logger A 的 name 为com.moduleA
,而 logger B 的 name 为com.moduleA.packageA
,则可以说 A 是 B 的父 logger。这种树状结构的作用在于,假如 B 并没有定义自己的日志级别,则会继承 A 的日志级别。其它的如 appender 也会根据继承关系计算得出。
logger 只有一个 name 属性是必填的,通常来说,除了需要特殊定义的几个 logger name 之外,其它的基本都会以 module 的维度进行定义,从而确保模块下的每一个类在以自己的类名获取 Logger 时,能够向上找到对应的 Logger。
举个例子,假如现在定义了一个 name 为 com.rale.service
的 logger,则位于 com.rale.service.HelloService.java
类中使用 LoggerFactory.getLogger(HelloService.class)
获取到的 Logger,虽然在配置文件中并没有声明,但是会以该类的全路径作为 logger 的名称,按照 Logger 的层级不断向上找到最近的父 Logger,并最终返回 name 为 com.rale.service
的 logger。
logger 还有一个标签为 level,可以为该 logger 分配对应的日志级别,只有高于该级别的日志会输出。如果没有显示定义 level 的值,则会从最近的显式声明了日志级别的父节点继承其日志级别。
一个基础的 logger 配置如下:
<logger name="integration" level="INFO" additivity="false">
<appender-ref ref="integration"/>
<appender-ref ref="common-error"/>
</logger>
一个 logger 下可以包含多个 appender-ref 标签,该标签声明了该 logger 的日志会打印到这些输出流中。这里还有一个比较特殊的属性 additivity,它是用来约束 appender 继承行为的。在默认情况下,aditicity 的值为 true,即 logger 除了会打印到当前显式声明的 appender-ref 中,还会打印到所有从父 Logger 中继承的 appender 中。例如假设 root 中声明了<appender-ref ref="common">
,则 integration 会同时向这三个输出流中打印日志。如果父 logger 和子 logger 中存在相同的 appender,该日志也会向该 appender 打印两遍。因此,通过 additivity 设置为 false,可以减少因为意料之外的 appender 继承导致日志的过量输出。
appender 标签
一个 appender 对应一个日志输出流。同一个 appender 可以绑定在多个 logger 上,即多个 logger 均可以向该 appender 输出日志。因此 appender 的实现内部进行了并发控制,防止日志乱码。
appender 支持的输出端很多,包括控制台,文件,远程 Socket 服务器,MySQL,PostgreSQL 等数据库,远程 UNIX 日志进程,JMS 等。
<appender> 有两个强制属性 name 和 class(Appender 类的全路径),包含 0 到多个 <layout class=””> 标签,0 到多个 <encoder class=””> 标签,0 到多个 <filter> 标签。它还可以包含任意多个 Appender Bean 类的成员变量属性值。
其中 layout 和 encoder 标签用来对 appender 中的日志进行格式化,filter 标签则支持对 appender 中传来的日志信息进行过滤,来决定哪些日志打印哪些不打印,因此可以通过 filter 来定义 appender 维度的日志级别。
一个典型的 appender 如下:
<appender name="common-error"
class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_PATH}/sls-common-error.log</file>
<encoder>
<pattern>${LOCAL_FILE_LOG_PATTERN}</pattern>
</encoder>
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>ERROR</level>
</filter>
</appender>
这里声明了一个文件输出流,并且用 file 标签定义了输出文件的位置,用 encoder 定义了日志打印的格式。这里通过引用变量的形式来定义,变量将在后面 property 标签中详细介绍。接着绑定了一个 filter,并且使用该 filter 定义了 appender 只会打印出日志级别大于等于 ERROR 级别的日志。
root 标签
root 标签要求在配置中必须声明一次,root 标签其实定义的是 root logger 的配置信息,它的默认的日志级别为 debug。所有的 logger 的最终的父 logger 一定是 root logger。
property 标签
property 标签支持在配置文件中声明变量。配置文件的变量有三种来源,分别是通过 JVM COMMAND,JAVA COMMAND,Classpth 以及当前的配置文件。举个例子,JAVA 命令传入变量的格式如下java -DUSER_HOME="/home/sebastien" MyApp2
。<property> 标签支持 configuration 文件中声明成员变量,它支持三种类型:KV,文件相对路径,Classpth 下的文件。
<!-- 键值型声明 -->
<property name="USER_HOME" value="/home/sebastien" />
<!-- 配置文件声明 -->
<property file="src/main/java/chapters/configuration/variables1.properties" />
<!--Classpath 资源 -->
<property resource="resource1.properties"/>
对于这些变量的引用采用标准 Linux 变量引用方法,通过 ${变量名称}即可以引用变量的值。同样也支持为这些变量声明默认值,通过 ${变量名称:- 默认值}
的语法结构。
一个简单的声明配置并使用的例子如下:
<configuration>
<property name="USER_HOME" value="/home/sebastien" />
<appender name="FILE" class="ch.qos.logback.core.FileAppender">
<file>${USER_HOME}/myApp.log</file>
<encoder>
<pattern>%msg%n</pattern>
</encoder>
</appender>
<root level="debug">
<appender-ref ref="FILE" />
</root>
</configuration>
define 标签
define 标签也是用来声明变量的,但是和上面的 property 的不同点在于,define 声明的是动态变量,即这些变量的值是在程序运行起来后才能得到的。比如配置文件中默认存在的 ${HOSTNAME}变量,就是通过 define 标签实现的,它会在程序运行后动态的获取当前所处容器的主机名,并且赋值给 HOSTNAME 变量。
一个典型的 define 标签用法如下,要求 define 的 class 中填入的类必须是 PropertyDefiner 接口的实现。
<configuration>
<define name="rootLevel" class="a.class.implementing.PropertyDefiner">
<shape>round</shape>
<color>brown</color>
<size>24</size>
</define>
<root level="${rootLevel}"/>
</configuration>
logback 提供了几个基础的 Definer 的实现,如 FileExistsPropertyDefiner
就是用来判断 path 中声明的文件是否存在的一个 definer。
include 标签
include 标签允许引入另一个路径下存储的 logback 配置,示例如下:
<configuration>
<include file="src/main/java/chapters/configuration/includedConfig.xml"/>
<root level="DEBUG">
<appender-ref ref="includedConsole" />
</root>
</configuration>
src/main/java/chapters/configuration/includedConfig.xml
文件的内容如下:
<included>
<appender name="includedConsole" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>"%d - %m%n"</pattern>
</encoder>
</appender>
</included>
要求被 include 进来的文件的内容必须包含在 included 标签内,且语法满足 logback 配置文件的语法。这里就是引入了 includeConfig.xml 中声明的一个 appender。