Spring優雅使用log4j2日誌

1-前言

Spring框架本身提供了對日誌的集成,對logback的支持非常好,但是對log4j和log4j2的支持就沒那麼好。 在同步打印日誌的場景下logback擁有最高的日誌吞吐量《Logback Throughput Benchmark (qos.ch)》,正常情況下推薦使用logback作爲日誌實現層。 但如果請求處理鏈路的代碼中打印了大量日誌,又需要減少日誌打印對響應時間的影響,建議先優化日誌打印量或打印級別,其次再考慮換用log4j2並啓用完全異步日誌:

Log4j – Performance (apache.org)

異步日誌適用的場景

  1. 需要減少日誌打印造成的延遲:通常日誌打印只佔很低比例的代碼執行時間,如果不是追求極致的響應時間,或者打印日誌耗費時間佔比太高,就沒有使用異步日誌的必要;
  2. 需要更高的瞬時日誌打印吞吐量:異步日誌只能通過線程池平谷削峯,只是提高峯值日誌打印吞吐量,使用異步日誌可以減少突發流量場景下日誌對吞吐量的影響;但如果流量壓力一直很大、打印日誌壓力持續時間很長,異步日誌反而不適用。

異步日誌的代價

  1. 線程不安全問題:由於打印日誌和日誌實際處理的時機不同,對一些多路複用對象(比如Tomcat的Request/Response)的日誌打印,會帶來線程不安全問題,造成程序不能正常的工作;當然log4j2會盡量避免線程安全問題的發生,依賴於Message的實現但是並沒有提供保證,而通過提前處理等機制進一步抵消了異步帶來的好處;
  2. 更高的資源消耗:線程同步、線程池、日誌隊列都會帶來更多的CPU時間和堆內存的消耗;
  3. 堆內存溢出風險:日誌隊列的配置不良、日誌內容不合理、不可控,都會大幅增加瞬時的堆內存消耗,帶來更高的堆內存溢出風險;
  4. 丟日誌風險:日誌異步提交成功不代表處理完成,提交的日誌有可能因爲隊列滿而被拋棄、因日誌異常或服務重啓而丟失;
  5. 日誌異常處理困難:如果業務邏輯依賴日誌的成功與否,則改造起來非常困難、且不是所有場景都能實現。

2-Spring配置屬性

Spring Boot支持的log4j2配置不多,比較實用的是日誌文件名、路徑,以包爲維度的日誌級別:Spring Boot配置屬性-日誌

配置項 描述 默認值
logging.charset.console 控制檯日誌輸出的字符集
logging.charset.file 文件日誌輸出的字符集
logging.config 自定義日誌配置文件的路徑。比如,Spring Boot針對log4j2的默認值是:classpath:log4j2-spring.xml
logging.exception-conversion-word 異常轉換標識符,參考log4j的PatternConverter接口實現,其中%wEx是Spring Boot給異常前後加分隔的實現WhitespaceThrowablePatternConverter %wEx
logging.file.name 日誌文件名,如:myapp.log,可經引用其它Spring變量,如:${spring.application.name}
logging.file.path 日誌路徑,如:/var/log
logging.group.* 日誌分組,*替換爲組名,用於批量設置一組日誌的級別,如:logging.group.db=org.hibernate,org.springframework.jdbc
logging.level.* 日誌級別,*替換爲日誌的包名,如:全局日誌級別設置:logging.level.root=WARN,spring框架日誌級別設置:logging.level.org.springframework=DEBUG.
logging.log4j2.config.override 當配置文件不止一個時使用此屬性添加更多log4j2配置文件

Spring Boot針對log4j2的文件日誌,配置了按文件大小的默認滾動策略,但是不會清理,見文件:spring-boot-2.7.18.jar!/org/springframework/boot/logging/log4j2/log4j2-file.xml

<RollingFile name="File" fileName="${sys:LOG_FILE}" filePattern="${sys:LOG_PATH}/$${date:yyyy-MM}/app-%d{yyyy-MM-dd-HH}-%i.log.gz">
    <PatternLayout pattern="${sys:FILE_LOG_PATTERN}" charset="${sys:FILE_LOG_CHARSET}"/>
    <Policies>
        <SizeBasedTriggeringPolicy size="10 MB" />
    </Policies>
</RollingFile>

3-改良log4j2的Spring配置屬性

3.1 支持文件的滾動刪除

3.1.1 爲什麼配置文件名要用log4j2-spring.xml

要想在Spring項目中配置自定義log4j2配置文件,官方推薦在項目中自定義一個名爲log4j2-spring.xml的配置文件。 對比log4j2.xmllog4j2-spring.xml

  1. log4j2-spring.xml是由Spring託管的,與Spring框架集成得更好,可以組合Spring的(日誌級別等)配置能力:
  2. log4j2.xml加載的生命週期早於Spring框架,加載不到Spring中配置的日誌文件名、日誌路徑等各種屬性,只能使用環境變量配置,在啓動腳本中管理配置;
  3. log4j2-spring.xml支持Spring配置中心的屬性,從而可同時支持環境變量、配置文件、配置中心等多種配置方式。

3.1.2 自定義log4j2配置文件

第一步 創建配置文件

在代碼根目錄創建名爲log4j2-spring.xml配置文件 以maven項目爲例,相對項目根目錄的路徑爲:src/main/resources/log4j2-spring.xml

第二步 複製Spring默認配置

從spring-boot配置文件中複製文件內容:spring-boot-2.7.18.jar!/org/springframework/boot/logging/log4j2/log4j2-file.xml中複製

第三步 修改默認配置

修改其中的RollingFile配置:

...
<Properties>
    ...
    <!-- 日誌文件名 -->
    <Property name="LOG_FILE_NAME">${spring:logging.file.name}</Property>
    <!-- 日誌路徑 -->
    <Property name="LOG_FILE_PATH">${spring:logging.file.path}</Property>
    <!-- 滾動文件大小,從spring配置中讀取配置,默認值100M -->
    <Property name="MAX_FILE_SIZE">${spring:logging.log4j2.rollingpolicy.max-file-size:-100m}</Property>
    <!-- 文件保留數量,從spring配置中讀取配置,默認值14 -->
    <Property name="MAX_HISTORY">${spring:logging.log4j2.rollingpolicy.max-history:-14}</Property>
    <!-- 文件保留時長,從spring配置中讀取配置,默認值14天 -->
    <Property name="RETAIN_TIME">${spring:logging.log4j2.rollingpolicy.retain-time:-14d}</Property>
    ...
</Properties>

<Appenders>
...
    <RollingFile name="File" fileName="${LOG_FILE_PATH}/${LOG_FILE_NAME}.log"
                 filePattern="${LOG_FILE_PATH}/${LOG_FILE_NAME}-%d{yyyy-MM-dd-HH}-%i.log.gz">
        <PatternLayout pattern="${sys:FILE_LOG_PATTERN}" charset="${sys:FILE_LOG_CHARSET}"/>
        <Policies>
            <SizeBasedTriggeringPolicy size="${MAX_FILE_SIZE}"/>
        </Policies>
        <!-- 保留14天的日誌,max="14",age="14d" 一個目錄下保留14個備份日誌文件 -->
        <DefaultRolloverStrategy max="${MAX_HISTORY}">
            <Delete basePath="${LOG_FILE_PATH}" maxDepth="1">
                <IfFileName glob="*.gz"/>
                <IfLastModified age="${RETAIN_TIME}"/>
            </Delete>
        </DefaultRolloverStrategy>
    </RollingFile>
</Appenders>
...

3.1.3 在Spring中修改配置

通過以上配置,我們的項目便具備了通過Spring配置文件或配置中心修改日誌配置的能力,比如:

logging:
  level:
    # 從環境變量中讀取全局日誌級別,未配置則使用級別warn
    root: ${QZD_SYS_LOG_LEVEL:warn}
    # 從環境變量中讀取業務日誌級別,未配置則使用級別info
    com:
      qizhidao: ${QZD_SYS_LOG_LEVEL:info}
  file:
    # 從環境變量中讀取日誌路徑,讀取不到則使用默認值
    path: ${QZD_LOG_PATH:/data/logs}/${APP_NAME:${spring.application.name:escommon-server}}
    # 從環境變量中讀取日誌名稱,取不到則使用默認值
    name: ${APP_NAME:${spring.application.name:escommon-server}}
  # 配置上一節自定義的日誌滾動策略
  log4j2:
    rollingpolicy:
      # 日誌文件大小不超過20MB
      max-file-size: 20m
      # 文件數量不超過30個
      max-history: 30
      # 文件最多保留7天
      retain-time: 7d

3.2 開啓完全異步日誌

之所以要使用log4j2,就是爲了使用完全異步日誌來提升打印日誌的性能。 參考《log4j2的異步日誌文檔》,要輸出異步日誌,需要以下配置:

第一步:關閉文件日誌的立即刷新

按log4j2的文檔要求,使用異步日誌要關閉立即刷新,否則會影響性能,異步日誌是批量提交的。 修改日誌配置文件log4j2-spring.xml,自動感知異步日誌

...
<Properties>
    ...
    <!-- 使用系統屬性仲裁器檢測是否啓用了異步日誌,從而自動開關立即刷新 -->
    <SystemPropertyArbiter propertyName="log4j2.contextSelector" propertyValue="org.apache.logging.log4j.core.async.AsyncLoggerContextSelector">
        <Property name="IMMEDIATE_FLUSH">false</Property>
    </SystemPropertyArbiter>
</Properties>

<Appenders>
    ...
    <!-- 設置日誌輸出的immediateFlush值 -->
    <RollingFile name="File" ... immediateFlush="${IMMEDIATE_FLUSH:-true}">
        ...
    </RollingFile>
</Appenders>
...

第二步:啓動時指定使用完全異步日誌

log4j2的完全異步日誌需要在啓動時通過JVM參數指定:

# Java啓動腳本增加JVM參數
java -Dlog4j2.contextSelector=org.apache.logging.log4j.core.async.AsyncLoggerContextSelector -classpath my-app.jar MyMain

3.3 將錯誤日誌與其它日誌分離

有時我們想將錯誤日誌與其它日誌分別輸出到不同的文件,以方便問題的分析,可以修改log4j2-spring.xml的配置:

...
<RollingFile name="File" fileName="${LOG_FILE_PATH}/${LOG_FILE_NAME}-info.log"
             filePattern="${LOG_FILE_PATH}/${LOG_FILE_NAME}-%d{yyyy-MM-dd-HH}-%i-info.log.gz"
             immediateFlush="${IMMEDIATE_FLUSH:-true}">
    <!-- 錯誤日誌的過濾策略是:拒絕錯誤日誌,接受非錯誤日誌 -->
    <ThresholdFilter level="error" onMatch="DENY" onMismatch="ACCEPT"/>
    ...
    </RollingFile>
    <!-- 增加錯誤日誌輸出 -->
    <RollingFile name="ErrorFile" fileName="${LOG_FILE_PATH}/${LOG_FILE_NAME}-error.log"
                 filePattern="${LOG_FILE_PATH}/${LOG_FILE_NAME}-%d{yyyy-MM-dd-HH}-%i-info.log.gz"
                 immediateFlush="${IMMEDIATE_FLUSH:-true}">
        <!-- 錯誤日誌的過濾策略是:接受錯誤日誌,拒絕非錯誤日誌 -->
        <ThresholdFilter level="error" onMatch="ACCEPT" onMismatch="DENY"/>
    ...
    </RollingFile>
</Appenders>

<Loggers>
...
    <Root level="info">
        ...
        <!-- 根日誌附加錯誤日誌文件輸出 -->
        <Appender-ref ref="ErrorFile"/>
    </Root>
</Loggers>

4-高級技巧

4.1 使用Spring Profile配置不輸出日誌

對開發人員的本地環境來說,日誌屬於垃圾文件,我們並不想輸出日誌,此時可以配合Spring Profile和log4j2的SpringProfile仲裁器:

第一步:增加log4j2-spring-boot支持組件的依賴

在pom.xml文件中添加依賴:

    <dependency>
        <groupId>org.apache.logging.log4j</groupId>
        <artifactId>log4j-spring-boot</artifactId>
    </dependency>

第二步:改造log4j2-spring.xml配置文件,支持profile控制

修改log4j2-spring.xml配置文件:

<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN">
    ...
    <Appenders>
        ...
        <!-- 在未開啓Profile[close-file-log]時,纔打印文件日誌 -->
        <SpringProfile name="!close-file-log">
            <RollingFile name="File" ...>
                ...
            </RollingFile>
            <RollingFile name="ErrorFile" ...>
                ...
            </RollingFile>
        </SpringProfile>
    </Appenders>

    <Loggers>
        ...
        <Root level="info">
            ...
            <!-- 在未開啓Profile[close-file-log]時,纔打印文件日誌 -->
            <SpringProfile name="!close-file-log">
                <Appender-ref ref="File"/>
                <Appender-ref ref="ErrorFile"/>
            </SpringProfile>
        </Root>
    </Loggers>
</Configuration>

第三步:通過配置關閉文件日誌

方式一:JVM參數方式:
java -Dspring.profiles.active=close-file-log -classpath my-app.jar MyMain
方式二:通過配置文件:
spring:
  profiles:
    active: close-file-log

這裏主要是爲了演示SpringProfileArbiter,你也可以使用SystemPropertyArbiter來實現類似能力。

4.2 調試日誌

修改配置後,我們往往需要看生效的日誌配置是什麼,可以通過配置文件來開啓log4j2的調試功能: 同樣還是修改log4j2-spring.xml配置文件,將根結點的status改爲DEBUG:

<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="DEBUG">
    ...
</Configuration>

修改後再啓動應用程序,控制檯輸出效果如下:

2024-05-22 17:39:05,974 main DEBUG Loaded configuration from /Users/chentao/Develop/Qizhidao/Code/wzdata/wzdata_escommon/target/classes/log4j2-spring.xml
2024-05-22 17:39:05,975 main DEBUG Starting LoggerContext[name=Default, org.apache.logging.log4j.core.LoggerContext@2eae8e6e] with configuration XmlConfiguration[location=/Users/chentao/Develop/Qizhidao/Code/wzdata/wzdata_escommon/target/classes/log4j2-spring.xml]...
2024-05-22 17:39:05,975 main DEBUG Apache Log4j Core 2.17.2 initializing configuration XmlConfiguration[location=/Users/chentao/Develop/Qizhidao/Code/wzdata/wzdata_escommon/target/classes/log4j2-spring.xml]
2024-05-22 17:39:05,976 main DEBUG PluginManager 'Core' found 128 plugins
2024-05-22 17:39:05,976 main DEBUG PluginManager 'Level' found 0 plugins
2024-05-22 17:39:05,978 main DEBUG Building Plugin[name=Arbiter, class=org.apache.logging.log4j.core.config.arbiters.SystemPropertyArbiter].
2024-05-22 17:39:05,981 main DEBUG PluginManager 'TypeConverter' found 26 plugins
...

5-參考文檔

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