工作總結!日誌列印的11條建議

JavaPub發表於2024-03-16

前言

大家好,我是 JavaPub。日誌是我們定位問題的得力助手,也是我們團隊間協作溝通(甩鍋)、明確責任歸屬(撕B)的利器。沒有日誌的程式執行起來就如同脫韁的野🐎。列印日誌非常重要。今天我們來聊聊日誌列印的 N 個好建議~

image-20240314165434764

選擇合適的日誌等級

在開發中我們有常見的四種日誌列印等級,debug、info、warn、error,要選擇合適的等級列印,不要上來直接 info。

image-20240314214602647

  • error: 錯誤日誌,指比較嚴重的問題,會對系統和有業務造成傷害。運維監控重點關注

  • warn: 警告日誌,不會對系統執行造成大的影響,一般由開發人員關注

  • info: 關鍵日誌,為了保留系統執行關鍵指標,比如函式的入參、出參,時間等資訊。

  • debug: 開發日誌,在開發除錯階段,記錄物件資料在關鍵處理步驟中的變化情況、快速定位。

要列印函式的入參、出參

記錄日誌並不是要把所有資訊都記錄下來,那日誌儲存就要大到上天。我們只記錄關鍵有效的日誌,有效日誌才是 battle 🆚 時殺手鐧。

image-20240314214637143

哪些算是有效日誌?比如函式的入口處,列印入參,還包括使用者唯一標識 (uid)、鏈路標識 (traceId) 等。函式出口列印返回值及時間等。

    public String GetName(Request req, Integer id){
        log.debug("method start param: {}", req.UserID);
        
        String name = "JavaPub";
        log.debug("method end result: {}", name);
        return name;
    }

列印日誌物件要做判空處理,避免阻斷流程

image-20240314214712731

為了列印一行日誌,程式寫掛了。空指標異常在任何程式碼中都是最常見的異常之一。

反例:當 book 物件是 NULL 的話,這行日誌就會拋空指標異常。

public void doSome(Book book){
    log.info("do do and print log: {}". book.getName());
    // do something...
    ...
}

不要使用日誌系統的(Log4j、Logback),要使用 Slf4j

image-20240314214745191

Slf4j 是使用門面模式的日誌框架,可以解耦具體的日誌實現。可以在不修改程式碼的情況下,更換底層的日誌框架

正例:

import org.slf4j.Logger; 
import org.slf4j.LoggerFactory;

private static final Logger logger = LoggerFactory.getLogger(JavaPub.class);

對低階別的日誌輸出,必須進行日誌級別開關判斷

image-20240314214801114

對於 trace、debug、info 這些比較低的日誌級別,必須進行日誌級別開關。

正例:

開關判斷邏輯通常放在日誌工具類中。

public void doSomething(){
    User user = new User(1, "技術自媒體", "JavaPub");
    if (logger.isDebugEnabled()) {
        logger.debug("print debug log. 666 is {}", user.getName());
    }
}

反例:

public void doSth(){
    String name = "JavaPub";
    logger.trace("print debug log" + name);
    logger.debug("print debug log" + name);
    logger.info("print info log" + name);
    // 業務邏輯
    ...
}

當日志級別是 warn 時,以上日誌不會列印,但是會執行字串拼接操作,如果列印值是物件的話,還會執行 toString() 方法,浪費了系統資源,因此建議加上日誌開關判斷

不要用e.printStackTrace()列印日誌

反例:

public void doSomething(){
    try{
        // 業務程式碼
        ...
    } catch (Exception e){
        e.printStackTrace();
    }
}
  • e.printStackTrace() 列印出的日誌包含堆疊資訊,導致我們的日誌資訊不規整、增加定位問題的難度。如果使用 ELK 分析日誌也會非常困難。

  • e.printStackTrace() 語句產生的字串記錄的是堆疊資訊,如果資訊太長太多,字串常量池所在的記憶體塊沒有空間了,即記憶體滿了,系統請求也將被阻塞。

正例:

public void doSomething(){
    try{
        // 業務邏輯
        ...
    } catch (Exception e){
        log.error("程式異常 failed", e);
    }
}

列印全部的異常資訊,方便定位問題

image-20240314214823572

反例:

沒有列印系統異常 e,無法定位出現了什麼型別的異常。

public void doSth(){
    try{
        // 業務邏輯
        ...
    } catch (Exception e){
        log.error("發生了一個異常");
    }
}

不要列印重複日誌

image-20240314214836800

在巢狀邏輯程式碼中列印重複日誌,增加系統資源消耗佔用。

反例:

public void doSomething(String s){
    log.info("do something and print log: {}", s);
    doSubSomething(s);
}
​
private void doSubSomething(String s){
    log.info("do sub something and print log: {}", s);
    // 寫點業務邏輯
    ...
}

正例:

應該直接刪掉或者將為 debug 日誌級別。

日誌儘量使用英文

反例:

image-20240314155834611

建議:儘量在列印時日誌時輸出英文,防止中文編碼與終端不一致導致列印出現亂碼,對排查故障造成感染。

核心業務邏輯,在每個分支首行都列印日誌

在編寫核心業務邏輯程式碼時,遇到 if...else 或者 switch 這樣的分支條件,在行首列印日誌,透過日誌可以快速排查定位異常。

public void doSomething(){
    if(user.isVip()){
        log.info("該使用者是 JavaPub 會員,Id:{},開始處理會員邏輯",user,getUserId());
        // TODO 會員邏輯
    }else{
        log.info("該使用者是非會員,Id:{},開始處理非會員邏輯",user,getUserId())
        // TODO 非會員邏輯
    }
}

不要列印無意義的日誌(不攜帶上下文、日誌鏈路 id)

image-20240314214848607

反例:

不攜帶任何業務資訊的日誌,對故障排查意義不大。

public void doSomething(){
    log.info("do something and print log. i am NB");
    // TODO 業務邏輯
    ...
}

正例:

  • 日誌一定要攜帶業務資訊相關內容,有利於快速定位問題原因
public void doSomething(Request req, User user){
    log.info("do something and print log, id={}, trace_id={}", user.GetId, req.GetTraceId);
    // TODO 業務邏輯
    ...
}

如何列印日誌呢?總的來說不要讓你的程式在黑盒總執行,列印關鍵資訊、保證在出現異常時透過日誌快速定位到那裡就可以啦。

相關文章