6000 字講明白大型網站架構技術細節:後端架構規整化 java 日誌框架
日誌
對於一個大型網站系統而言,日誌是後端應用程序必須要引入的模塊,日誌有利於追查 Bug 和記錄用戶操作。每個編程語言都有很多現成的日誌框架,而 Java 的日誌框架也有很多,如 log4j2、Logback、SLF4J 等,這些日誌框架都可以讓後端應用程序按照一定的規則輸出日誌文件。
在大型網站系統當中,僅僅把日誌記錄下來是遠遠不夠的。大型網站系統需要一個完整的日誌系統,日誌系統除了需要收集日誌並將其記錄下來以外,還需要做日誌篩選、用戶行爲記錄追溯及風險預警等工作。
不過,在項目中前期,或者單獨調試某個後端應用程序時,仍然需要使用日誌模塊。這裏以 log4j2 爲例,通過日誌模塊引入和規範化日誌記錄兩個方面對日誌模塊的使用進行介紹。
1. 日誌模塊引入
引入日誌模塊的具體操作步驟如下:
(1)引入 log4j2 依賴包。需要在工程配置文件(build.gradle)中添加 log4j2 依賴包,如代碼 4.32 所示。需要注意的是,如果不去除 Spring Boot 中原有日誌模塊的話,那麼新引入的日誌模塊與原有日誌模塊會產生衝突。
代碼 4.32 build.gradle 中添加 log4j2 依賴包
configurations {
// 去除 Spring Boot 中原有的日誌模塊
all*.exclude group: 'org.springframework.boot', module: 'spring-boot
starter-logging'
}
…
dependencies {
…
//dependencies 中添加 log4j2 的依賴包
implementation 'org.springframework.boot:spring-boot-starter-log4j2'
…}
(2)同步工程配置。修改完 build.gradle 文件後,log4j2 的依賴包在同步工程配置後纔會被下載和引入。在 IntelliJ IDEA 中單擊 “同步” 按鈕即可同步工程配置,如圖 4.59 所示。
圖 4.59 在 IntelliJ IDEA 中同步 build.gradle 配置
(3)創建 log4j2 的配置文件 log4j2.xml,其內容及設置說明如代碼 4.33 所示,更詳細的說明請參考官方說明(
https://logging.apache.org/log4j/2.x/manual/index.html)。另外,日誌配置文件一般與後端應用程序的配置文件放在一起,如圖 4.60 所示。
圖 4.60 日誌配置文件存放位置
說明:圖 4.60 中的 “配置文件目錄位置” 和“後端應用程序配置文件名”都不是默認設置。關於 “配置文件目錄位置” 和“後端應用程序配置文件名”的設置可參考前面小節中的講解。
代碼 4.33 log4j2.xml 文件的內容及其配置
<?xml version="1.0" encoding="UTF-8"?>
<!-- monitorInterval:檢查更新的時間間隔,單位爲 s。
在程序運行期間,log4j2 能夠自動檢測日誌配置文件是否有更新,如果有更新則自動加載新設
置 -->
<configuration monitorInterval="1800">
<!-- 配置變量,變量會被後續設置使用 -->
<properties>
<!-- 設置日誌格式的變量:%d:獲取日期時間;
%level:日誌等級;
%msg:日誌消息,如 ERROR、INFO、DEBUG 等
%n:換行符 -->
<property
value="[%d{yyyy-MM-dd}][%d{HH:mm:ss}][%level]%msg%n" />
<!-- 設置日誌存儲路徑的變量 -->
<property />
</properties>
<!-- 設置日誌輸出源,如設置日誌輸出格式、設置日誌文件名等 -->
<appenders>
<!-- 設置 Console(控制檯)輸出日誌格式,一般在開發工具調試時使用 -->
<console >
<!-- 輸出日誌的格式,採用 properties 中設置的 LOG_PATTERN 變量 -->
<patternLayout pattern="${LOG_PATTERN}"/>
</console>
<!-- 設置記錄 INFO 和 DEBUG 日誌等級的日誌文件,當符合存檔策略時在(<policies></policies > 中設置),則會自動壓縮並另存爲存檔文件。
fileName:日誌文件名,使用 properties 中設置的 FILE_PATH 變量。在此例中,輸出文件名爲
D:/logs/backend/demo/web-info.log。
immediateFlush:接收到日誌後,是否立即輸出到文件中。這個一般設置爲 false,設置爲 true 會嚴重影響接口的併發能力。
filePattern:存檔文件名,在此例中,歸檔文件名爲(以 2020-2-23 爲例)
D:/logs/backend/demo/web-info/web-info-2020-2-23_1.log.gz-->
<rollingFile ${FILE_PATH}/web
info.log"
immediateFlush="false"
filePattern="${FILE_PATH}/web-info/web-info-%d{yyyy-MM
dd}_%i.log.gz">
<!-- 輸出日誌的格式,採用 properties 中設置的 LOG_PATTERN 變量 -->
<patternLayout pattern="${LOG_PATTERN}"/>
<!-- 篩選接收的日誌等級,接收 INFO 和 DEBUG 等級的日誌 -->
<filters>
<thresholdFilter level="error" onMatch="DENY" onMismatch=
"NEUTRAL"/>
<thresholdFilter level="info" onMatch="ACCEPT" onMismatch=
"DENY"/>
<thresholdFilter level="debug" onMatch="ACCEPT" onMismatch=
"DENY"/>
</filters>
<!-- 設置存檔策略,此例爲:每天自動存檔,日誌文件超過 20MB 也會存檔 -->
<policies>
<!-- 設置時間的存檔策略,interval 的時間精度與 filePattern 的時間精
度一致,因爲 filePattern 只設置到日期,所以這裏的 interval="1" 指的是 1 天 -->
<timeBasedTriggeringPolicy interval="1"/><!-- 設置文件大小的存檔策略 -->
<sizeBasedTriggeringPolicy size="20MB"/>
</policies>
<!-- 設置保留多少個日誌文件,日誌文件個數超過 max 的值會自動覆蓋 -->
<defaultRolloverStrategy max="15"/>
</rollingFile>
<rollingFile ${FILE_PATH}/web
error.log"
immediateFlush="false"
filePattern="${FILE_PATH}/web-error/web-error-%d{yyyy-MM
dd}_%i.log.gz">
<thresholdFilter level="error" onMatch="ACCEPT" onMismatch=
"DENY"/>
-- 設置輸出日誌等級,默認情況下,不會輸出比該日誌等級低的日誌。
此例中,只輸出 FATAL、ERROR、WARN、INFO 的日誌。
志級別以及優先級排序爲 OFF > FATAL > ERROR > WARN > INFO > DEBUG >
TRACE > ALL,其中 OFF 是不輸出所有日誌 -->
(4)引入日誌配置文件。在後端應用程序配置文件中指定日誌配置文件路徑後,才能生效。在如圖 4.60 所示的工程目錄結構中,需要在 demo.properties 文件中添加日誌配置文件的路徑,如代碼 4.34 所示,其中,classpath:log4j2.xml 爲具體的路徑。
代碼 4.34 在後端應用程序的配置文件中添加日誌配置文件的路徑
#設置日誌配置文件的路徑
logging.config=classpath:log4j2.xml
(5)程序中記錄日誌,如代碼 4.35 所示。對應的日誌輸出結果如代碼 4.36 所示,其中,由於 log4j2.xml(日誌配置文件)設置的日誌輸出等級爲 INFO,所以 DEBUG 等級的日誌沒有被記錄下來。
說明:日誌配置文件的相關說明請參照代碼 4.33,其中,設置輸出日誌等級的位置爲 …。
代碼 4.35 代碼中記錄日誌
// 需要引入的日誌依賴類
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
public class XX{
…
// 獲取日誌對象
private static final Logger LOGGER = LogManager.getLogger();
…
public void Function(){
…
// 輸出 INFO 級別的日誌
LOGGER.info("info log test.");
// 輸出 ERROR 級別的日誌
LOGGER.error("error log test.");
// 輸出 DEBUG 級別的日誌
LOGGER.debug("debug log test.");
…
}
…
}
代碼 4.36 日誌輸出結果
[2020-04-20][17:46:17][INFO]info log test.
[2020-04-20][17:46:17][ERROR]error log test.
(6)如果實現了 4.3.3 小節中的後端應用程序與配置文件分離,那麼日誌配置文件也應該從後端應用程序中分離出來。按常理來說,只要在配置文件中設置日誌配置文件路徑就可以了(如 logging.config=
/home/tomcat/appconfig/log4j2.xml),但是由於某種衝突,這樣的配置是不起作用的。
因此,要想實現後端應用程序引用外部的日誌配置文件,需要通過 “添加啓動參數” 和“修改代碼(ServletInitializer.java)”才能實現。具體做法是,“添加啓動參數”需要在 Tomcat 目錄下的 / conf/catalina.properties 文件中添加如代碼 4.37 所示的設置,修改後的代碼如代碼 4.38 所示,其中,xxx_log4j2.xml 爲日誌配置文件名。最終,配置文件和日誌配置文件集中管理的效果如圖 4.61 所示。
圖 4.61 配置文件和日誌配置文件集中管理
代碼 4.37 設置日誌配置文件所在目錄
…
#後端應用程序的外部配置文件所在目錄,詳見 4.3.3 節的介紹
spring.config.location=${catalina.home}/appconfig/
#新增代碼,設置日誌配置文件所在目錄,一般與後端應用程序的外部配置文件目錄相同
logging.config=${catalina.home}/appconfig/
…
代碼 4.38 修改後的 ServletInitializer.java 文件
…
public class ServletInitializer extends SpringBootServletInitializer {
@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder
application) {
// 新增代碼,補全啓動參數(logging.config)的文件名
String loggingConfig = System.getProperty("logging.config");
if(!loggingConfig.isEmpty()) {
System.setProperty("logging.config", loggingConfig+"xxx_
log4j2.xml");
}
// 設置後端應用程序的外部配置文件名,詳見 4.3.3 小節的介紹
return application
.properties("spring.config.)
.sources(xxApplication.class);
// 初始工程的代碼,需要去除
//return application.sources(xxApplication.class);
}
}
2. 規範化日誌記錄
日誌記錄是一個十分開放的行爲,原則上,只要記錄的日誌能 “定位問題發生的位置” 和“記錄某些重要的用戶操作”就可以了。但是,在實際項目當中,由於日誌對功能的實現是不產生影響的,所以日誌通常都是通過一次次問題修復而得到補充的。而這種 “補丁型” 日誌,通常是混亂的。混亂的日誌對於 “定位問題發生的位置” 和“追查某些重要的用戶操作”都不能起到很好的作用,因此,日誌記錄需要規範化。
注意:日誌記錄的規則需要在項目前期定好,在開發過程中吸收規整化日誌的工作量。項目後期再整理日誌是很不理智的行爲,因爲後期整理日誌需要花費大量的時間去梳理和理解業務代碼,而這部分工作量是很難預估且十分枯燥的,最後整理日誌的結果往往也是不盡人意的。
規範化日誌記錄可以從限制日誌等級、明確日誌記錄位置、添加日誌跟蹤碼等方面進行考慮。
(1)限制日誌等級。日誌模塊一般都有等級劃分,以 log4j2 爲例,其日誌等級有 6 種,分別爲 TRACE(追蹤調試)、DEBUG(調試)、INFO(信息)、WARNING(警告)、ERROR(錯誤)和 FATAL(致命錯誤)。每個日誌等級看上去都有相對明確的分工和含義,但是在實際應用當中,這些日誌等級的具體用途其實相當模糊,很多時候,都很難界定一個日誌應該歸類爲哪一個等級。一旦出現這種模糊規則,就會出現一人一個樣的做法,最後導致 “五花八門” 的日誌等級劃分原則。
因此,規整化日誌需要限制日誌等級。一般情況下,後端應用程序使用 DEBUG、INFO 和 ERROR 三個日誌等級就足夠了,這三個日誌等級的分工和協助如圖 4.62 所示。
圖 4.62 DEBUG、INFO 和 ERROR 日誌等級的分工和協助
其中,DEBUG 日誌在運行時不生效,需要打開調試模式(修改日誌輸出等級)後,才能記錄 DEBUG 日誌。
(2)明確日誌記錄位置。在限制了日誌等級後,需要解決 “補丁式日誌” 的問題,避免日誌不夠全面的情況。而解決 “補丁式日誌” 的關鍵,是明確日誌記錄位置。但是,明確日誌記錄位置是一件很難實現的事情,因爲接口程序與接口程序之間很難找到共性。
不過,如果實現了 4.3.2 小節中介紹的 “限制函數調用層級”“公共模塊” 和“錯誤機制”,那麼接口程序會變成流水線式的處理方式。在流水線式的接口程序中明確日誌記錄位置是相對容易的,如圖 4.63 所示。
圖 4.63 明確日誌位置
其中,每個接口只需要在 “接收請求”“數據庫操作” 和“返回結果”這三部分添加日誌就可以了,其餘日誌都在公共模塊裏,而且公共模塊裏的日誌是一次添加全局有效的。
說明:Dao 層(數據庫操作)其實也可以做成一個公共模塊,這樣可以省掉一些日誌工作量。另外,雖然數據庫本身可以自動記錄日誌,但是數據庫自身的日誌不能包含用戶身份信息,即不能追溯用戶操作,所以 Dao 層(數據庫操作)的日誌是有必要記錄的。
(3)添加日誌跟蹤碼。即使日誌被記錄得十分詳細,分析日誌也是一件很麻煩的事情。同一時刻,後端應用程序可能會同時處理多個請求,以至於多個請求的日誌是混合在一起的,在不經過特殊處理的情況下,根本沒法分辨哪幾條日誌是屬於同一個請求的。像這種無法區分請求的日誌,被記錄下來也是浪費資源。
因此,需要在每條日誌中添加日誌跟蹤碼,標記同一請求的日誌。跟蹤碼的本質,就是同一請求輸出日誌時,都多加一個相同的字符串。如果使用的是 log4j2 日誌模塊,可以在不改變原有日誌輸出代碼的前提下,添加日誌跟蹤碼。
首先,需要修改日誌配置文件中的日誌輸出格式,如代碼 4.39 所示,其中 [%X{requestId}] 爲新增的跟蹤碼格式。
代碼 4.39 修改日誌輸出格式
…
<!-- 此段設置截取自代碼 4.33 的,單獨設置是不起作用的 -->
<!-- 設置日誌格式的變量:
%d:獲取日期時間;
%level:日誌等級;
%X{requestId}:跟蹤碼;
%msg:日誌消息,如 ERROR、INFO、DEBUG 等;
%n:換行符 -->
<property
value="[%d{yyyy-MM-dd}][%d{HH:mm:ss}][%level][%X{requestId}]
%msg%n" />
…
修改完日誌配置文件之後,需要在每個接口程序中添加 “生成跟蹤碼” 的代碼,如代碼 4.40 所示,其中,代碼中的函數爲 Controller 層中的接口的入口函數,requestId 對應代碼 4.39 中的跟蹤碼標識。
代碼 4.40 添加 “生成跟蹤碼” 的代碼
…
@Controller
…
@RequestMapping(value="…",method = RequestMethod.POST)
@ResponseBody
public JSONObject XXX(@RequestBody String requestParam, HttpServlet
Response response) {
// 在每個接口的入口函數都需要添加以下 “生成跟蹤碼” 代碼
ThreadContext.put("requestId", UUID.randomUUID().toString());
…
}
… 修改日誌跟蹤碼後,能清晰地識別不同請求的日誌,日誌輸出結果如代碼 4.41 所示,其中,
62e3300c-e0a0-40cd-be80-4320d40ddc2c 和 00000000-0000-0000-0000-000000000000 是日誌追蹤碼。
代碼 4.41 添加 “日誌跟蹤碼” 後的日誌輸出結果
[2020-04-20][17:46:17][INFO][62e3300c-e0a0-40cd-be80-4320d40ddc2c]info
log test_1.
[2020-04-20][17:46:17][INFO][00000000-0000-0000-0000-000000000000]info
log test_1.
[2020-04-20][17:46:17][INFO][00000000-0000-0000-0000-000000000000]info
log test_2.
[2020-04-20][17:46:17][INFO][62e3300c-e0a0-40cd-be80-4320d40ddc2c]info
log test_2.
[2020-04-20][17:46:17][INFO][62e3300c-e0a0-40cd-be80-4320d40ddc2c]info
log test_3.
來源:
https://www.toutiao.com/article/7171706087735853582/?log_from=a774e7cca8953_1669861559715
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/6ULQphuRs8davO4lM3dZyw