初探Logback:學會看懂Logback配置文件

前言

在現如今的應用中,日誌已經成爲了一個非常重要的工具。通過系統打印的日誌,可以監測系統的運行情況,排查系統錯誤的原因。日誌從最早期的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。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章