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 property | log4j2.formatMsgNoLookups | true |
Environment variable | LOG4J_FORMAT_MSG_NO_LOOKUPS | true |
參考資料:
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 度建立一個配置檔:
檔案內容:
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-encoder
既 LogstashEncoder
- 呢個係為左畀 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
totalSizeCap
- 所有舊既 log 檔既大小總數上限
- 一旦超出上限,會自動刪除日期最舊既 log 檔
- Logback 會先睇
maxHistory
,之後再睇 totalSizeCap
去決定刪除舊既 log 檔。
4.4 寫 Java code
我地需要喺每個需要做 logging 既 class 加上 Lombok 既 @Slf4j
annotation,然後用 Lombok 幫我地改好左叫 log
既 org.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 參考資料