1. 日誌訊息的結構
讓我們把日誌想像成不只是程式的「意識流」,而是一份有價值的紀錄,過了一個月或一年之後,你或你的同事仍能從中找到問題的答案:「這裡到底發生了什麼事?」。為了做到這點,每一則日誌訊息都應該是有結構的。通常(這也是大多數函式庫的標準)每則訊息包含:
- 事件時間 — 何時發生。
- 層級 — 重要程度(INFO、ERROR 等)。
- logger 名稱 — 通常是類別或元件名稱。
- 訊息內容 — 具體發生了什麼。
- 堆疊追蹤(若有錯誤) — 用來了解發生位置與原因。
以下是格式良好的日誌行(以 Log4j/SLF4J 為例):
2024-06-16 18:42:07,123 INFO com.example.MainApp - 使用者已登入系統: username=vasya
若發生錯誤:
2024-06-16 18:42:10,456 ERROR com.example.LoginService - 使用者驗證錯誤: vasya
java.lang.IllegalArgumentException: 密碼不正確
at com.example.LoginService.checkPassword(LoginService.java:42)
...
為什麼這很重要?
當應用程式運作時間一長,日誌可能佔用數個 GB。若訊息沒有結構化,要找問題就會變成「從風扇噪音猜旋律」等級的難題。
2. 訊息格式化
為什麼不該這樣做:
logger.info("使用者 " + username + " 已登入系統");
看似簡單,但有個陷阱:即使目前的日誌層級設為 ERROR,括號內的字串仍會被組合(字串連接仍會執行),這會造成額外的資源消耗。在大型系統中,每秒上千行日誌,可能導致實際的延遲。
正確作法:樣板與參數
現代的函式庫(例如 SLF4J 與 Log4j 2)支援帶參數的樣板:
logger.info("使用者 {} 已登入系統", username);
這樣只有在日誌層級允許輸出時才會建立字串。例如現在層級是 WARN,則連字串的計算都不會發生——節省資源與心力。
加分: 若傳入多個參數,它們會依序套入:
logger.info("使用者 {} 對物件 {} 執行了動作 {}", username, action, objectId);
記錄例外(stacktrace)
如果你捕捉到例外,別手動把堆疊追蹤加到訊息裡:
// 不要這樣:
logger.error("錯誤: " + ex.getMessage() + "\n" + Arrays.toString(ex.getStackTrace()));
正確作法:
logger.error("處理請求時發生錯誤", ex);
SLF4J 和 Log4j 會自動將堆疊追蹤漂亮地加入日誌。
範例:兩種做法的比較
// 不佳(字串連接永遠會執行)
logger.debug("物件: " + expensiveToString(obj));
// 佳(延遲形成)
logger.debug("物件: {}", obj);
3. 選擇日誌層級
如果你的日誌全都在 ERROR 層級,那就不是日誌,而是一盞紅色警示燈;如果全都在 DEBUG,你會淹沒在細節中。來看看各層級應該何時使用。
| 層級 | 用途 | 訊息範例 |
|---|---|---|
|
關鍵性故障,導致系統運作不正常或完全無法運作 | 「資料庫連線錯誤」 |
|
重要但非致命的警告,需要留意 | 「找不到使用者,改用 guest」 |
|
反映應用程式正常運作的一般事件 | 「使用者已註冊:vasya」 |
|
除錯用的詳細資訊,生產環境不需要 | 「呼叫方法 checkPassword,參數為 ...」 |
|
最細的資訊,通常用於深入診斷 | 「開始處理迴圈:i=0」 |
常見訊息範例
- ERROR — 無法寫入檔案、捕捉到未處理的例外、服務不可用。
- WARN — 已淘汰的 API、使用者的可疑行為、超出嘗試次數上限。
- INFO — 使用者登入/登出、訂單處理完成、應用程式啟動。
- DEBUG — 請求參數、變數值、中間計算結果。
- TRACE — 進入/離開方法、內部迴圈、演算法的細節。
建議:
在生產環境通常只開啟 INFO 以上,有時包含 WARN 與 ERROR。DEBUG 與 TRACE 僅在追查棘手的 bug 時啟用。
4. Best practices(日誌記錄最佳實務)
不要記錄敏感資料
密碼、token、信用卡號——都不該出現在日誌中。即使你覺得「日誌檔只有我會看」,也請想想 GDPR 與那位可能不小心把日誌丟到群組聊天的同事。
// 不佳:
logger.info("使用者 {} 以密碼 {} 登入", username, password);
// 佳:
logger.info("使用者 {} 已登入系統", username);
不要濫用 ERROR 層級
如果你什麼都用 logger.error 記錄,真正發生災難時沒人會注意到——大家早就對「紅燈」麻木了。只有在應用程式真的無法繼續運作或商業邏輯被破壞時,才使用 ERROR。
記錄例外要包含完整堆疊
不要只寫 ex.getMessage(),否則你永遠不知道錯誤究竟在哪裡發生。請把例外當作第二個參數傳給 logger。
logger.error("處理請求時發生錯誤", ex);
使用唯一識別碼(事件關聯)
在大型系統中,替每個請求、使用者或操作指派唯一識別碼很有用。這能幫助你「串起」系統不同部分的事件。
logger.info("開始處理訂單: orderId={}", orderId);
logger.info("訂單已成功處理: orderId={}", orderId);
不要把所有東西都寫進日誌
如果日誌量太大——它就會變得毫無用處。不要記錄每一行程式,否則將無法找到真正需要的資訊。
讓訊息易於理解
請以半年後讀日誌的人也能理解的方式撰寫訊息。避免縮寫、難以理解的簡寫與「圈內人才懂的玩笑」。
5. 實作:設定日誌格式與層級
Log4j2(log4j2.xml)格式設定範例
<Configuration>
<Appenders>
<Console name="Console" target="SYSTEM_OUT">
<PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level %logger{36} - %msg%n"/>
</Console>
</Appenders>
<Loggers>
<Root level="info">
<AppenderRef ref="Console"/>
</Root>
</Loggers>
</Configuration>
這代表什麼?
- %d{...} — 事件時間。
- %-5level — 層級(ERROR、INFO 等)。
- %logger{36} — logger 名稱(通常是類別)。
- %msg — 訊息本身。
含不同日誌層級的程式碼範例(SLF4J)
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class LogDemo {
private static final Logger logger = LoggerFactory.getLogger(LogDemo.class);
public static void main(String[] args) {
logger.info("應用程式已啟動");
logger.debug("變數 x 的值: {}", 42);
try {
throw new IllegalArgumentException("哎呀!");
// ...
} catch (Exception ex) {
logger.error("啟動時發生錯誤", ex);
}
}
}
示範不同層級的差異
如果在 logger 設定中選擇 INFO 層級,那麼 DEBUG 與更低的訊息將不會輸出。試著在設定中把層級改成 debug —— 你會看到更詳細的資訊。
6. 常見錯誤
錯誤 №1: 在日誌中進行字串連接。 新手很常這樣寫:
logger.debug("使用者: " + user.getName() + ", 角色: " + user.getRole());
結果即使關閉 DEBUG 層級,這些字串仍會被建立,造成額外負擔。請改用參數化!
錯誤 №2: 記錄例外時沒有堆疊。 只寫訊息:
logger.error("錯誤: " + ex.getMessage());
結果日誌裡沒有發生位置的資訊。請把 exception 當作第二個參數傳入!
錯誤 №3: 把所有東西都記在 ERROR 層級。 如果一切都是紅色——就沒有真正的紅色。請依照用途選擇層級,否則重要錯誤會淹沒在「小事」之中。
錯誤 №4: 記錄敏感資料。 千萬不要把密碼、token、卡號寫入日誌。就算你覺得不會有人看到,現實常常出乎意料。
錯誤 №5: 訊息含糊不清。 如果日誌訊息看起來像「ERR42: fail」,一個月後你自己也記不得是什麼意思。請寫得清楚且詳盡。
錯誤 №6: 缺少唯一識別碼。 在複雜系統中,沒有 orderId、userId 等識別碼,你就無法「串起」事件並理解某個使用者或訂單究竟發生了什麼。
GO TO FULL VERSION