日志
對于一個大型網站系統而言,日志是後端應用程式必須要引入的子產品,日志有利于追查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 name="LOG_PATTERN"
value="[%d{yyyy-MM-dd}][%d{HH:mm:ss}][%level]%msg%n" />
<!-- 設定日志存儲路徑的變量 -->
<property name="FILE_PATH" value="D:/logs/backend/demo" />
</properties>
<!-- 設定日志輸出源,如設定日志輸出格式、設定日志檔案名等 -->
<appenders>
<!-- 設定Console(控制台)輸出日志格式,一般在開發工具調試時使用 -->
<console name="Console" target="SYSTEM_OUT">
<!--輸出日志的格式,采用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 name="RollingFileInfo" fileName="${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>
<!-- 設定記錄error日志等級的日志檔案,配置格式與上面“設定記錄INFO和DEBUG
日志等級的日志檔案”相同,這裡不展開介紹。
在此例子中,ERROR日志的日志檔案名為D:/logs/backend/demo/web-error.log,歸檔檔案名為(以2020-2-23為例)
D:/logs/backend/demo/web-error/web-error-2020-2-23_1.log.gz-->
<rollingFile name="RollingFileError" fileName="${FILE_PATH}/web
error.log"
immediateFlush="false"
filePattern="${FILE_PATH}/web-error/web-error-%d{yyyy-MM
dd}_%i.log.gz">
<patternLayout pattern="${LOG_PATTERN}"/>
<filters>
<thresholdFilter level="error" onMatch="ACCEPT" onMismatch=
"DENY"/>
</filters>
<policies>
<timeBasedTriggeringPolicy interval="1"/>
<sizeBasedTriggeringPolicy size="20MB"/>
</policies>
<defaultRolloverStrategy max="15"/>
</rollingFile>
</appenders>
<!-- 設定日志源,需要在這裡關聯日志輸出源(<appenders></appenders>)才能輸出到對
應檔案當中 -->
<loggers>
<!-- 設定輸出日志等級,預設情況下,不會輸出比該日志等級低的日志。
在此例中,隻輸出FATAL、ERROR、WARN、INFO的日志。
日志級别以及優先級排序為OFF > FATAL > ERROR > WARN > INFO > DEBUG >
TRACE > ALL,其中OFF是不輸出所有日志-->
<root level="info">
<!-- 關聯輸出源,其中ref中的值需要與<rollingFile></rollingFile>中
的name對應 -->
<!-- 在非調試環境下,需要關閉控制台的日志輸出(去掉下面第一行就可以了) -->
<AppenderRef ref="Console" />
<AppenderRef ref="RollingFileInfo" />
<AppenderRef ref="RollingFileError" />
</root>
</loggers>
</configuration>
(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,其中,設定輸出日志等級的位置為<root level="info">…</root>。
代碼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.name=xxx")
.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 name="LOG_PATTERN"
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.
本文給大家講解的内容是大型網站架構的技術細節:後端架構規整化java日志架構
- 下篇文章給大家講解的内容是大型網站架構的技術細節:後端架構規整化自研架構Once
- 感謝大家的支援!