9

前言

在现如今的应用中,日志已经成为了一个非常重要的工具。通过系统打印的日志,可以监测系统的运行情况,排查系统错误的原因。日志从最早期的System.out.print到如今各种成熟的框架,使得日志打印更加规范化和清晰化。尤其是SLF4J的出现,为日志框架定义了通用的FACADE接口和能力。只需要在应用中引入SLF4J包和具体实现该FACADE的日志包,上层应用就可以只需要面向SLF4J接口编程,而无需关心具体的底层的日志框架,实现了上层应用和底层日志框架的解耦。Logback作为一个支持SLF4J通用能力的框架,成为了炙手可热的日志框架之一。今天就来稍微了解一下Logback日志的一些基础能力以及配置文件。

快速上手Logback

引入MAVEN依赖

logback主要由三个模块组成,分别是logback-core,logback-classiclogback-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结构为基础进行介绍。

image.png
上图为官网中对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允许多配置文件,其加载时读取配置文件的顺序如下:

  1. 在classpath查找logback-test.xml(一般classpath为src/test/resources)
  2. 如果该文件不存在,logback尝试寻找logback.groovy
  3. 如果该文件不存在,logback尝试寻找logback.xml
  4. 如果该文件不存在,logback会在META-INF下查找[com.qos.logback.classic.spi.Configurator](http://logback.qos.ch/xref/ch/qos/logback/classic/spi/Configurator.html)接口的实现
  5. 如果依然找不到,则会使用默认的BasicConfigurator,导致日志直接打印到控制台,日志等级为DEBUG,日志的格式为_%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n_

配置文件语法

在简单的了解了logback配置文件的基础结构后,这一章详细介绍一下logback中比较常用的几个标签以及各自代表的含义。

configuration标签

作为配置文件的根标签,configuration更多的是对整个Logback配置读取的模式进行定义,configuration标签汇中可以定义的属性如下:

  1. debug: 默认debug值为false,如果debug设置为true的话,则无论配置读取成功与否,都会将日志框架的状态打印出来,为false的话则只有在读取配置出错时才会打印状态日志。
  2. 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。


raledong
2.7k 声望2k 粉丝

心怀远方,负重前行