1-前言
Spring框架本身提供了對日誌的集成,對logback的支持非常好,但是對log4j和log4j2的支持就沒那麼好。 在同步打印日誌的場景下logback擁有最高的日誌吞吐量《Logback Throughput Benchmark (qos.ch)》,正常情況下推薦使用logback作爲日誌實現層。 但如果請求處理鏈路的代碼中打印了大量日誌,又需要減少日誌打印對響應時間的影響,建議先優化日誌打印量或打印級別,其次再考慮換用log4j2並啓用完全異步日誌:
《Log4j – Performance (apache.org)》
異步日誌適用的場景
- 需要減少日誌打印造成的延遲:通常日誌打印只佔很低比例的代碼執行時間,如果不是追求極致的響應時間,或者打印日誌耗費時間佔比太高,就沒有使用異步日誌的必要;
- 需要更高的瞬時日誌打印吞吐量:異步日誌只能通過線程池平谷削峯,只是提高峯值日誌打印吞吐量,使用異步日誌可以減少突發流量場景下日誌對吞吐量的影響;但如果流量壓力一直很大、打印日誌壓力持續時間很長,異步日誌反而不適用。
異步日誌的代價
- 線程不安全問題:由於打印日誌和日誌實際處理的時機不同,對一些多路複用對象(比如Tomcat的Request/Response)的日誌打印,會帶來線程不安全問題,造成程序不能正常的工作;當然log4j2會盡量避免線程安全問題的發生,依賴於
Message
的實現但是並沒有提供保證,而通過提前處理等機制進一步抵消了異步帶來的好處; - 更高的資源消耗:線程同步、線程池、日誌隊列都會帶來更多的CPU時間和堆內存的消耗;
- 堆內存溢出風險:日誌隊列的配置不良、日誌內容不合理、不可控,都會大幅增加瞬時的堆內存消耗,帶來更高的堆內存溢出風險;
- 丟日誌風險:日誌異步提交成功不代表處理完成,提交的日誌有可能因爲隊列滿而被拋棄、因日誌異常或服務重啓而丟失;
- 日誌異常處理困難:如果業務邏輯依賴日誌的成功與否,則改造起來非常困難、且不是所有場景都能實現。
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.xml
和log4j2-spring.xml
:
log4j2-spring.xml
是由Spring託管的,與Spring框架集成得更好,可以組合Spring的(日誌級別等)配置能力:log4j2.xml
加載的生命週期早於Spring框架,加載不到Spring中配置的日誌文件名、日誌路徑等各種屬性,只能使用環境變量配置,在啓動腳本中管理配置;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
...