在现如今的应用中,日志已经成为了一个非常重要的工具。通过系统打印的日志,可以监测系统的运行情况,排查系统错误的原因。日志从最早期的 System.out.print
到如今各种成熟的框架,使得日志打印更加规范化和清晰化。尤其是SLF4J的出现,为日志框架定义了通用的FACADE接口和能力。只需要在应用中引入SLF4J包和具体实现该FACADE的日志包,上层应用就可以只需要面向SLF4J接口编程,而无需关心具体的底层的日志框架,实现了上层应用和底层日志框架的解耦。Logback作为一个支持SLF4J通用能力的框架,成为了炙手可热的日志框架之一。今天就来稍微了解一下Logback日志的一些基础能力以及配置文件。
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-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允许多配置文件,其加载时读取配置文件的顺序如下:
[com.qos.logback.classic.spi.Configurator](http://logback.qos.ch/xref/ch/qos/logback/classic/spi/Configurator.html)接口的实现
在简单的了解了logback配置文件的基础结构后,这一章详细介绍一下logback中比较常用的几个标签以及各自代表的含义。
作为配置文件的根标签,configuration更多的是对整个Logback配置读取的模式进行定义,configuration标签汇中可以定义的属性如下:
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可以绑定在多个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 logger的配置信息,它的默认的日志级别为debug。所有的logger的最终的父logger一定是root logger。
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标签也是用来声明变量的,但是和上面的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标签允许引入另一个路径下存储的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。