Spring Boot 整合 slf4j+log4j2 實現日誌管理

 

 

背景

logging.apache

Java 中比較常用的日誌框架

  • log4j(Log for Java):Apache 的一個開源項目,七種日誌級別:OFF、FATAL、ERROR、WARN、INFO、DEBUG、TRACE
  • logback:是一個很成熟的日誌框架,其實 logBack 和 log4j 出自一個人之手,這個人就是 Ceki Gülcü。logback 比 log4j 大約快 10 倍、消耗更少的內存,遷移成本也很低,自動壓縮日誌、支持多樣化配置、不需要重啓就可以恢復 I/O 異常等優勢
  • log4j2:作者認爲,log4j2已經不僅僅是 log4j 的一個升級版本了,而是從頭到尾被重寫的,這可以認爲這其實就是完全不同的兩個框架

Spring Boot 默認使用 logback,但相比較而言,log4j2 在性能上面會更好。SpringBoot 高版本都不再支持 log4j,而是支持 log4j2。log4j2,在使用方面與 log4j 基本上沒什麼區別,比較大的區別是 log4j2 不再支持 properties 配置文件,支持 xml、json 格式的文件。

《阿里巴巴Java開發手冊》,其中有一條規範做了「強制」要求:

應用中不可直接使用日誌系統(Log4j Logback)中的 API,而應依賴使用日誌框架 SLF4J 中的 API,使用日誌門面模式的日誌框架,有利於維護和各個類的日誌處理方式統一。

Java 簡易日誌門面(Simple Logging Facade for Java,縮寫 SLF4J),它並不是真正的日誌框架,他是對所有日誌框架制定的一種規範、標準、接口,並不是一個框架的具體的實現,因爲接口並不能獨立使用,需要和具體的日誌框架實現配合使用。可以在軟件部署的時候決定要使用的 Logging 框架,目前主要支援的有 Java logging API、log4j 及 logback 等框架。

理解 SLF4J

接口用於定製規範,可以有多個實現,使用時是面向接口的(導入的包都是 slf4j 的包而不是具體某個日誌框架中的包),即直接和接口交互,不直接使用實現,所以可以任意的更換實現而不用更改代碼中的日誌相關代碼。

比如:slf4j 定義了一套日誌接口,項目中使用的日誌框架是logback,開發中調用的所有接口都是 slf4j 的,不直接使用 logback,調用是 自己的工程調用 slf4j 的接口,slf4j 的接口去調用 logback 的實現,可以看到整個過程應用程序並沒有直接使用 logback,當項目需要更換更加優秀的日誌框架時(如log4j2)只需要引入 log4j2 的 jar 和 Llg4j2 對應的配置文件即可,完全不用更改 Java 代碼中的日誌相關的代碼 logger.info(“xxx”),也不用修改日誌相關的類的導入的包( import org.slf4j.Logger; import org.slf4j.LoggerFactory;)

總結:使用日誌接口便於更換爲其他日誌框架。

One More Thing:上面的這幾段話是參考文章中截取的,也讓我確實理解了爲何推薦使用 SLF4J 的原因。這種做法感覺就是有點「面向接口編程」的思想,今天也查閱了一些這方面的資料,也讓我想起了爲何項目中寫 Service 代碼時,往往是先寫個接口、然後在寫個該接口的實現類。待有時間好好研究一些這塊的優點!

性能分析

!性能評測](https://gitee.com/michael_xiang/images/raw/master/BBsUn0.jpg)

可以看到在同步日誌模式下, Logback的性能是最糟糕的。

異步日誌模式

log4j2的性能無論在同步日誌模式還是異步日誌模式下都是最佳的!那本文就介紹的是使用 log4j2 作爲 slf4j 的具體實現。

log4j2 依賴

複製代碼
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
        <!-- 去掉logback配置 -->
        <exclusions>
            <exclusion>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-logging</artifactId>
            </exclusion>
        </exclusions>
    </dependency>

    <!-- 引入log4j2依賴 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-log4j2</artifactId>
    </dependency>
</dependencies>
複製代碼

 

log4j2 使用

複製代碼
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

// 這了兩種寫法都 OK,推薦第一種,不用每次都要修改類名
private static final Logger logger = LoggerFactory.getLogger(this.getClass());
private static final Logger logger = LogManager.getLogger(UserController.class);
//...
logger.debug("this is debug");
logger.info("this is info");
複製代碼

 

log4j2 日誌級別

從小到大依次是:all、trace、debug、info、warn、error、fatal、off

由於我們使用的是 slf4j 接口包,該接口包中只提供了未標有刪除線的日誌級別的輸出。

log4j2 配置文件結構

配置文件的主要結構如下:

  • Appenders:
    • Appender
      • Filter
      • Layout
      • Policies
      • Strategy
  • Loggers
    • Logger
    • RootLogger

Appender

Appender 可以理解爲一個管道,定義了日誌內容的去向(保存位置)。

  • 配置一個或者多個 Filter
  • 配置 Layout 來控制日誌信息的輸出格式。
  • 配置 Policies 以控制日誌何時(When)進行滾動。
  • 配置 Strategy 以控制日誌如何(How)進行滾動。

注意點:

  • 多個 appender 不能指向同一個日誌文件,否則會報錯:Configuration has multiple incompatible Appenders pointing to the same resource 'logs/mybatis-demo-warn.log'
  • ImmediateFlush=true,一旦有新日誌寫入,立馬將日誌寫入到磁盤的文件中。當日志很多,這種頻繁操作文件顯然性能很低下
  • immediateFlush:log4j2 接收到日誌事件時,是否立即將日誌刷到磁盤。默認爲 true。
  • BufferedIO: 文件流寫出是否使用緩衝,true 表示使用,默認值爲 false 即不使用緩衝。測試顯示,即使在啓用immediateFlush 的情況下,設置 bufferedIO=true 也能提高性能。
  • 一個 LogConfig 可以使用多個 appender,一個 appender 也可以被多個 LogConfig 使用

官宣——Appender

PatternLayout

這是常用的日誌格式化類,其它日誌格式化類很少用。

<PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n" charset="UTF-8"/>

常用說明:

Copy
%d{HH:mm:ss.SSS} 表示輸出到毫秒的時間
%t 輸出當前線程名稱
%-5level 輸出日誌級別,-5表示左對齊並且固定輸出5個字符,如果不足在右邊補0
%logger 輸出logger名稱,因爲Root Logger沒有名稱,所以沒有輸出
%msg 日誌文本
%n 換行

其他常用的佔位符有:
%F 輸出所在的類文件名,如Client.java
%L 輸出行號
%M 輸出所在方法名
%l 輸出語句所在的行數, 包括類名、方法名、文件名、行數

關於 pattern 的格式點擊 官宣——Pattern Layout

Filter

Filters 決定日誌事件能否被輸出。過濾條件有三個值:ACCEPT(接受)DENY(拒絕)NEUTRAL(中立)

常用的 Filter 實現類有:

  • LevelRangeFilter
  • TimeFilter
  • ThresholdFilter

簡單說就是 log4j2 中的過濾器 ACCEPT 和 DENY 之後,後續的過濾器就不會執行了,只有在 NEUTRAL 的時候纔會執行後續的過濾器

複製代碼
<Console name="Console">

    <!--
        設置 onMismatch="NEUTRAL" 可以讓日誌經過後續的過濾器
        最後一個過濾器建議設置 onMismatch="DENY", 不然日誌就輸出了。
    -->
    <Filters>

        <!-- 從大到小:error, warn, info, debug, trace -->
        <LevelRangeFilter minLevel="error" maxLevel="info" onMatch="ACCEPT" onMismatch="NEUTRAL" />

        <!-- 只允許在每天的 8點~8點半 之間輸出日誌 -->
        <TimeFilter start="08:00:00" end="08:30:00" onMatch="ACCEPT" onMismatch="DENY" />
    </Filters>

    <PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n" charset="UTF-8"/>
</Console>
複製代碼

 

LevelRangeFilter 對它們進行了 ACCEPT,而剩下的 trace Msg 和 debug Msg 則會經過下一個過濾器。

Policy

Policy & Strategy

  • Policy 是用來控制日誌文件何時(When)進行 Rolling/滾動的;
  • Strategy 是用來控制日誌文件如何(How)進行 Rolling/滾動的。

所謂「日誌滾動」就是當達到設定的條件後,日誌文件進行切分。比如:工程師想讓系統中的日誌按日進行切分,並且按月歸檔。

Rolling 的意思是當滿足一定條件後,就重命名原日誌文件用於備份,並重新生成一個新的日誌文件。例如需求是每天生成一個日誌文件,但是如果一天內的日誌文件體積已經超過 1G,就重新生成。兩個條件滿足一個即可

Policy常用的實現類:

  • SizeBasedTriggeringPolicy,根據日誌文件的大小進行滾動。單位有:KBMBGB
  • CronTriggeringPolicy,使用 Cron 表達式進行日誌滾動,很靈活
  • TimeBasedTriggeringPolicy,這個配置需要和 filePattern 結合使用,注意 filePattern 中配置的文件重命名規則。滾動策略依賴於 filePattern 中配置的最具體的時間單位,根據最具體的時間單位進行滾動。這種方式比較簡潔。CronTriggeringPolicy 策略更強大

在 TimeBasedTriggeringPolicy 標籤中加上了 modulate 屬性並設置爲 true,該屬性的意思是是否對日誌生成時間進行調製。若爲 true,則日誌時間將以 0 點爲邊界進行偏移計算。例如第一次日誌保存時間是 3 點,modulate爲 trueinterval 是 4h。那麼下次生成日誌時間是 4點,08:00,12:00……。

複製代碼
<Appenders>
    <RollingRandomAccessFile name="File" fileName="logs/app.log"
                                filePattern="logs/$${date:hh-mm}/%d{hh-mm-ss}.app.%i.log" >
        <PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n" charset="UTF-8"/>

        <Policies>
            <!-- 每 5s 翻滾一次 -->
            <!--<CronTriggeringPolicy schedule="0/5 * * * * ?" />-->

            <!--
                filePattern中最具體的時間單位是 秒。
                這裏用 TimeBasedTriggeringPolicy 替換 CronTriggeringPolicy

                注意:modulate屬性是指從啓動時間開始算5秒,還是從0秒開始算5秒,運行一下就明白了。
                modulate: true(默認值) // 會從啓動時間開始算 5秒
                modulate: false // 從 0秒開始算
            -->
            <TimeBasedTriggeringPolicy interval="5" modulate="true"/>
            <SizeBasedTriggeringPolicy size="10 MB"/>
        </Policies>
        <DefaultRolloverStrategy max="10" />
    </RollingRandomAccessFile>
</Appenders>
複製代碼

 

Strategy

Strategy常用的實現類:

  • DefaultRolloverStrategy
  • DirectWriteRolloverStrategy

這兩個 Strategy 都是控制如何進行日誌滾動的。

DefaultRolloverStrategy 默認的 max爲 7。

<DefaultRolloverStrategy max="7"/>

max 參數指定了計數器的最大值。一旦計數器達到了最大值,過舊的文件將被刪除。

注意:不要認爲 max 參數是需要保留的日誌文件的最大數目。

max 參數是與 filePattern 中的計數器 %i 配合起作用的,其具體作用方式與 filePattern 的配置密切相關。

1.如果filePattern中僅含有date/time pattern,每次rollover時,將用當前的日期和時間替換文件中的日期格式對文件進行重命名。max參數將不起作用。

如,filePattern="logs/app-%d{yyyy-MM-dd}.log"

2.如果 filePattern 中僅含有整數計數器(即%i ),每次 rollover 時,文件重命名時的計數器將每次加1(初始值爲1),若達到 max 的值,將刪除舊的文件。

如,filePattern="logs/app-%i.log"

3.如果 filePattern 中既含有 date/time pattern,又含有 %i,每次 rollover 時,計數器將每次加 1,若達到 max 的值,將刪除舊的文件,直到 data/time pattern 不再符合,被替換爲當前的日期和時間,計數器再從1開始。

如,filePattern="logs/app-%d{yyyy-MM-dd HH-mm}-%i.log"

Appender 類型

FileAppender(File)、RandomAccessFileAppender(RandomAccessFile)

  • 相同點:寫入日誌信息到文件
  • 不同點:使用的 I/O 實現類不同,前者使用 FileOutputStream,後者使用 RandomAccessFile

官方文檔說是在 bufferedIO=true (默認是 true )的情況下,性能提升 20% ~ 200%

常用屬性:

  • fileName:來指定文件位置,文件或目錄不存在則會自動創建。
  • immediateFlush:是否每次寫入都要立刻刷新到硬盤中。默認 true,如果使用默認值可能會影響性能。

RollingFileAppender(RollingFile)、RollingRandomAccessFileAppender(RollingRandomAccessFile)

  • 相同點:寫入日誌信息到文件
  • 不同點:使用的 I/O 實現類不同,前者使用 FileOutputStream,後者使用 RandomAccessFile
  • 上一對的實現類不能進行「日誌滾動」,而帶有 rolling 字樣的 appender 就可以實現「滾動」功能。有「滾動」,會判斷是否滿足封存文件的要求,執行日誌存檔操作。

RollingRandomAccessFile Appender,相比 RollingFileAppender有很大的性能提升,官網宣稱是20-200%

複製代碼
<RollingRandomAccessFile name="File" fileName="logs/app.log"
                            filePattern="logs/$${date:hh-mm}/%d{hh-mm-ss}.app.%i.log" >
    <PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n" charset="UTF-8"/>
    <Policies>
        <!-- 每 5s 翻滾一次 -->
        <CronTriggeringPolicy schedule="0/5 * * * * ?" />
        <SizeBasedTriggeringPolicy size="10 MB"/>
    </Policies>
    <DefaultRolloverStrategy max="10" />
</RollingRandomAccessFile>
</Appenders>
複製代碼

 

常用屬性:

  • filePattern:指定當發生Rolling時,文件的轉移和重命名規則。至於其中的 $${date:hh-mm} 是表示了一個文件夾(以 小時-分鐘)命名。
  • DefaultRolloverStrategy 指定了如何(How)進行翻滾,並且指定了最大翻滾次數(影響%i參數值),超過次數之後會按照相應的規則刪除舊日誌。
  • Policies: 這裏就是規定了何時進行滾動(When),可以有多個Policy。
    • CronTriggeringPolicy,比如設置每 5s 進行一次翻滾
    • SizeBasedTriggeringPolicy 指定當文件體積大於size指定的值時,觸發Rolling。例如,如果當前文件超過了 10MB,但是文件的名字還沒有進行翻滾(建立新文件),那麼就會用%i的方式進行翻滾。

如果配置的是 RollingFile 或 RollingRandomAccessFile,則必須配置一個 Policy

翻滾理解

假設計數器次數設爲2次 <DefaultRolloverStrategy max="2" />filePattern 中既含有 date/time pattern,又含有 %i

當滿足翻滾觸發條件時(時間間隔到了 OR 文件大小超了),就會啓動 Rolling

app.log

第一次翻滾:app.log app.1.log // app.log -> app.1.log
第二次翻滾:app.log app.1.log app.2.lop // app.log -> app.2.log

一個循環結束,到達了最大保存數 2 了,那麼,app1.log 會被刪除,下一個 app3.log 就會覆蓋 app2.logapp2.log會改名爲app1.log

第三次翻滾:app.log app.2.lop app.3.lop // app.log -> app.3.log
第四次翻滾:app.log app.3.lop app.4.lop // app.log -> app.4.log

理解:編號最近的一次也就是最新的一次 log,而採取了 Policy 方式的日誌,fileName 中保存的日誌將不會是全量的日誌,而是根據你 Policy 的條件切分後的最近一次的日誌內容。

一個 Appender 示例

按月歸檔日誌,按日進行切分,限制單文件大小爲 500MB, 一天最多生成20個文件,也就是(20 * 500)MB大小的日誌

複製代碼
<?xml version="1.0" encoding="UTF-8"?>
<Configuration name="baseConf" status="warn" monitorInterval="30">

    <Appenders>
        <RollingRandomAccessFile name="File" fileName="logs/app.log"
                                 filePattern="logs/$${date:yyyy-MM}/%d{yyyy-MM-dd}.app.%i.log" >
            <PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n" charset="UTF-8"/>
            <Policies>
                <TimeBasedTriggeringPolicy interval="1" modulate="false"/>
                <SizeBasedTriggeringPolicy size="500MB"/>
            </Policies>
            <DefaultRolloverStrategy max="20" />
        </RollingRandomAccessFile>
    </Appenders>

    <Loggers>
        <Root level="info">
            <AppenderRef ref="File"/>
        </Root>
    </Loggers>

</Configuration>
複製代碼

 

Logger

簡單說 Logger 就是一個路由器,指定類、包中的日誌信息流向哪個管道,以及控制他們的流量(日誌級別)

Logger 部分爲兩個 Logger:

  • RootLogger(必須配置)
  • Logger

注意:Logger 中也可以加過濾器的!

日誌重複打印問題

如果 Root 中的日誌包含了 Logger 中的日誌信息,並且 AppenderRef 是一樣的配置,則日誌會打印兩次。

這時候我們需要使用一個 Logger 的屬性來解決,那就是 additivity,其默認值爲 true,需要配置爲false

複製代碼
<?xml version="1.0" encoding="UTF-8"?>
<Configuration name="baseConf" status="warn" monitorInterval="30">

    <Appenders>
        <Console name="Console">
            <PatternLayout>
                <Pattern>%d %p %c{1.} [%t] %m%n</Pattern>
            </PatternLayout>
        </Console>
    </Appenders>

    <Loggers>
        <Logger name="me.master.snail.log.LogMain" level="info" additivity="false">
            <AppenderRef ref="Console"/>
        </Logger>

        <Root level="trace">
            <AppenderRef ref="Console"/>
            <Filters>
                <LevelRangeFilter minLevel="error" maxLevel="info" onMatch="ACCEPT" onMismatch="DENY" />
            </Filters>
        </Root>
    </Loggers>
</Configuration>
複製代碼

 

  • RootLogger 只能有 1 個,普通的 Logger 可以定義多個,可以細緻到給某個類定義;
  • 多個 Logger 配置重複了,在日誌文件中會重複;
  • 每一個 Logger 對應的 name 是包路徑,表示在 name 包下的類使用 AppenderRef 指向的日誌模板來輸出日誌;
  • 不同的 LogConfig 之間其實是有繼承關係的,子 LogConfig 會繼承 parent 的屬性,而所有 LogConfig 都繼承自 Root LogConfig。所以即使只配置了 root logger,你一樣可以在任何地方通過 LoggerFactory.getLogger 獲取一個 logger 對象,記錄日誌;
  • 先配置一個 Root,讓所有需要使用日誌的 logger 繼承,然後對有特別需要的 logger 進行特殊的配置,比如我們希望 org.springframework 包只記錄 error以及 warn 級別的 log,再比如,我們希望能顯示mybatis 執行的 sql 的日誌,都可以進行個性化的配置;

Logger 等級實驗

複製代碼
<logger name="org.springframework" level="INFO" additivity="true">
    <AppenderRef ref="InfoLog"/>
</logger>

<Root level="ERROR" additivity="true">
    <AppenderRef ref="Console"/>
    <AppenderRef ref="InfoLog"/>
    <AppenderRef ref="WarnLog"/>
    <AppenderRef ref="ErrorLog"/>
</Root>
複製代碼

 

  • ROOT 等級設爲 ERROR 時,org.springframework Logger 等級設爲 OFF 時,發現原來的 warn.log 和 info.log 文件中,都只有級別大於或等於 ERROR 的日誌信息了;
  • ROOT 等級設爲 ERROR 時,org.springframework Logger 等級設爲 INFO 時,發現info.log 文件中,增加了 org.springframework 包的相關 INFO 級別的日誌信息了;

總結:

  • Logger 日誌等級和 appender 日誌等級的關係:logger 日誌等級和 appender 日誌等級,誰「高」聽誰的;
  • 普通 Logger 的優先級高
應對軟件變化
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章