➜ Old React website
Chung Cheuk Hang MichaelJava Web Developer
用 Markdown 寫 docJava 測試(三):Mockito

Java Logging

Table of contents

1 Logging 簡介

一個程式運行既時候係咩狀態、以前處理過啲咩、有冇曾經出 exceptions,如果我地唔做 logging 就冇辦法知道,然後就唔可能有系統地去 troubleshoot 或者 debug 到個程式。
Java 既基礎班有教我地用 System.out 或者 System.err(同為 PrintStream type)既 println(...) 或者 printf(...) 去令個程式喺 runtime 時候喺 console 度顯示到 log,但問題係 console 既 log 只能喺果個 console session 仍然存在既時候 keep 到紀錄,而如果 console session 完結左,我地係冇辦法睇得返啲 log。
因此,我地必須將啲 log save 落 file 度。除此之外,用 console 去做 logging 仲有唔少問題,所以就要用啲 logging libraries 去做 logging,亦需要用啲工具去幫手管理啲 log。

2 如何做好 logging

好既 logging practices 至少包括以下幾點:
  • 可以自行配置格式
    • 包括由邊一個 Java class 既邊一行生產出黎、生產既 timestamp 資料
    • 包括額外數據(例如 microservice architecture 既 web applications 之間如果用 per-HTTP-request trace ID 溝通,日後就可以用 trace ID 追蹤到成個 flow,知道先後 call 左邊啲 microservices)。
    • 一個 Java 程式可以產生多個 threads,如果係 web application 既程式而 log entry 度又冇加到 per-HTTP-request 既 trace ID 或者 session ID,我地就只能以 thread name 黎分返開啲 log,從而串連啲 log 去跟住個 flow debug。
    • 可以做到 log message 只係一個 log entry
  • 根據日期、檔案大小拆檔,並且識自動管理
    • 假如我地個程式有好多野 log,亦有好多 users 用我地個 web service,咁我地個 web application 程式就可以生產到勁多 log。咁既情況下,最好係我地可以將啲 log 根據日期自動開新檔案儲存啲 log,甚或再根據檔案大小拆分檔案。
  • 可以配合搜尋工具
    • 如果我地既程式係一個提供 web service 既 web application,而我地又得一個或者一堆 log files,但冇工具去管理佢地,咁我地係好難可以搵返特定既 log 出黎,所以我地亦要用啲專門管理 log 既工具。
      • Open source 既工具有 Elastic 既 ELK stack
        • Elasticsearch - 儲存 log、搜尋引擎
        • Logstash - 讀 log、處理 log
        • Kibana - 搜尋 log 既用戶介面、視覺化或者圖像化搜尋結果
      • 收費 cloud 既工具有 Sumo Logic

3 Logging libraries

Logging 可以分為 API 以及實現既 libraries。我地可以喺程式裡面用唔同既配搭:
  • Log4j2 Core(API)、Log4j2 API(實現)
  • Slf4j(API)、Logback(實現)
  • Apache Commons Logging(API)、Log4j2(實現)
    • 需要使用 Commons Logging Bridge

3.1 Log4j2 漏洞

3.1.1 JNDI lookup 遠程代碼執行漏洞(2021 年 12 月)

喺 2021 年 12 月,Log4j2 畀人發現有遠程代碼執行漏洞(Log4Shell/CVE-2021-44228)。呢個漏洞係利用 Log4j 既一個 JNDI lookup 功能。
通常啲 logging libraries 都可以畀我地將 object value substitute 落帶有 {} placeholder 既 log message format 裡面,而如果呢個 object 係一個 JNDI URL 既 string,因為 Log4j2 本來默認左 JNDI lookup 既關係,Log4j2 就會連接到呢個 JNDI URL。如果個 JNDI URL 係一個上載喺黑客 server 上面既一個惡意 Java class 既網址,咁就有可能令到 Log4j2 call 左果個惡意網址去下載同埋執行果個惡意 Java class,從而達到最危險既資訊安全漏洞——遠程代碼執行漏洞(remote code execution)。
受影響既版本由 2.0-beta-9 至到 2.14.1,然後 Apache 推出左 2.15.0 去解決呢個漏洞。
如果冇辦法升級到最新版本,只要用既版本係 2.10.0 或以上,就可以能用呢啲參數:
參數類別參數名稱參數值
System propertylog4j2.formatMsgNoLookupstrue
Environment variableLOG4J_FORMAT_MSG_NO_LOOKUPStrue
參考資料:

3.1.2 Infinite recursion DoS 漏洞(2021 年 12 月)

不過之後又有人發現 Log4j2 仲有 DoS 漏洞(CVE-2021-45105),情況就好似 iPhone 既 iMessage 輸入特定文字有機會令到個程式 crash 咁。要解決呢個問題,就要升級 Log4j2 到 2.17.0(到底有完沒完?)。
參考資料:

4 動手寫

4.1 普通 Java 程式(非 Spring)

我地會用到 Lombok、Slf4j 以及 Logback。

4.1.1 Maven dependencies

我地會用到 Lombok 既 @Slf4j annotation 去 generate construct logger object 既 code。
1<dependencies> 2 3 <dependency> 4 <groupId>org.projectlombok</groupId> 5 <artifactId>lombok</artifactId> 6 <version>1.18.24</version> 7 <scope>provided</scope> 8 </dependency> 9 10 <dependency> 11 <groupId>org.slf4j</groupId> 12 <artifactId>slf4j-api</artifactId> 13 <version>1.7.36</version> 14 </dependency> 15 16 <dependency> 17 <groupId>ch.qos.logback</groupId> 18 <artifactId>logback-classic</artifactId> 19 <version>1.2.11</version> 20 </dependency> 21 22</dependencies>

4.2 Spring Boot web application

我地會用到 Spring Boot、Lombok、Slf4j 以及 Logback。

4.2.1 Maven dependencies

我地會用到 Lombok 既 @Slf4j annotation 去 generate create construct object 既 code。
1<parent> 2 <groupId>org.springframework.boot</groupId> 3 <artifactId>spring-boot-starter-parent</artifactId> 4 <version>2.6.1</version> 5</parent> 6 7<dependencies> 8 9 <dependency> 10 <groupId>org.projectlombok</groupId> 11 <artifactId>lombok</artifactId> 12 </dependency> 13 14 <dependency> 15 <groupId>org.springframework.boot</groupId> 16 <artifactId>spring-boot-starter-web</artifactId> 17 </dependency> 18 19 <dependency> 20 <groupId>net.logstash.logback</groupId> 21 <artifactId>logstash-logback-encoder</artifactId> 22 <version>7.0.1</version> 23 </dependency> 24 25</dependencies>

4.3 Logback 配置

喺我地既 Maven project 度建立一個配置檔:
  • main/resources
    • logback.xml
檔案內容:
1<?xml version="1.0" encoding="UTF-8"?> 2<configuration> 3 <appender name="console" class="ch.qos.logback.core.ConsoleAppender"> 4 <encoder> 5 <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} (%file:%line\) - %msg%n</pattern> 6 <charset>UTF-8</charset> 7 </encoder> 8 </appender> 9 10 <appender name="stash" class="ch.qos.logback.core.rolling.RollingFileAppender"> 11 <file>logs/app.log</file> 12 13 <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy"> 14 <fileNamePattern>logs/archived/app.%d{yyyy-MM-dd}.%i.log</fileNamePattern> 15 <maxHistory>30</maxHistory> 16 <maxFileSize>100MB</maxFileSize> 17 <totalSizeCap>1GB</totalSizeCap> 18 </rollingPolicy> 19 20 <encoder class="net.logstash.logback.encoder.LogstashEncoder" /> 21 </appender> 22 23 <root level="INFO"> 24 <appender-ref ref="console" /> 25 <appender-ref ref="stash" /> 26 </root> 27</configuration>
解釋:
  • 呢度配置左 2 個 log appenders
    • 當有 log 出現,每個 appender 都會接收到相同既 log
    • Console
      • 仍然會喺 console 度出到 log,方便 local 啟動個 web application 既時候喺 IDE 或者 OS 自帶既 console(Windows 既 Command Prompt 或者 macOS 既 Terminal)睇到 log
    • Logstash
      • Encoder 會指定用現成黎自 logstash-logback-encoderLogstashEncoder
      • 呢個係為左畀 Elastic 既 ELK stack 裡面既 Logstash 工具睇得明我地既 log
      • 每一行都係 JSON 格式
  • rollingPolicy 可以畀我地配置點樣拆分檔案,到底係根據日期(每月一個或每日一個),又或者日期 + 檔案大小。
    • Logback 提供既 implementations
      • TimeBasedRollingPolicy
      • SizeAndTimeBasedRollingPolicy
    • fileNamePattern
      • TimeBasedRollingPolicy 既話,可以係以月拆分(用 {yyyy-MM})或者以日拆分(用 {yyyy-MM-dd})。
      • SizeAndTimeBasedRollingPolicy 既話,需要再加上 %i(第幾個分檔)。
    • maxHistory
      • 儲存最近幾耐既 log 檔
      • 如果 log 檔以月拆分,呢個數字就以月計(想一個月就寫 1
      • 如果 log 檔以日拆分,呢個數字就以日計(想一個月就寫 30
    • maxFileSize
      • 每個 log 檔既大小上限
    • totalSizeCap
      • 所有舊既 log 檔既大小總數上限
      • 一旦超出上限,會自動刪除日期最舊既 log 檔
    • Logback 會先睇 maxHistory,之後再睇 totalSizeCap 去決定刪除舊既 log 檔。

4.4 寫 Java code

我地需要喺每個需要做 logging 既 class 加上 Lombok 既 @Slf4j annotation,然後用 Lombok 幫我地改好左叫 logorg.slf4j.Logger object 黎 log 我地既 log message。
1@Slf4j 2@Service 3public class MyService { 4 5 public void foo() { 6 log.debug("This is DEBUG log."); 7 log.info("This is INFO log."); 8 log.trace("This is TRACE log."); 9 10 try { 11 throw new RuntimeException(); 12 } catch (Exception e) { 13 log.warn("This is WARN log.", e); 14 log.error("This is ERROR log.", e); 15 } 16 } 17}
註:如果係喺有 exception 既情況下要 log,又想包括埋 stack trace,可以喺任何一個出 log 既 method call 既最後一個 argument 畀個 Throwable object 佢。
用左 @Slf4j 就可以取代一般寫法:
@Service public class MyService { private Logger log = LoggerFactory.getLogger(getClass()); }

5 參考資料