日誌打印的5點建議

日誌打印的5點建議

最近我們介紹了幾款日誌分析的工具,比如Scribe和LogStash這類的開源項目,以及Splunk這樣的企業級工具,還有像SumoLogic和PaperTrail這樣的託管服務。你可以使用這些工具從海量的日誌數據提取到一些有價值的信息。

不過還有一件事它們是幫不了你的。它們都依賴於你實際輸出到日誌文件裏面的數據。日誌數據保質保量的重任就落到你肩上了。因此萬一情況不妙,你又得在日誌文件不全的情況下自己去調試代碼,那估計你只好趕緊把原先訂好的晚餐取消掉了。

爲了減少出現類似的情況,我這裏想分享5點日誌打印的心得,希望能對你有所幫助:

  1. 線程名

就像Ringo一樣,線程名應該是Java裏最被低估的功能之一了。因爲其實它的表述性很強。那又怎樣?就像我們的名字一樣,我們會給它賦予一個含義。

線程名最有用的時候應該就是多線程的情況下了。許多日誌框架都會記錄當前方法調用所在線程的名字。不幸的是,一般看起來都是這樣的:“http-nio-8080-exec-3″,這是線程池或者容器自動分配的線程名。

我經常聽到有謠傳稱線程名是不可變的。當然不是。線程名就是你日誌中最優質的不動產,你得確保自己能正確的使用它們。通常給它賦值會帶上上下文的詳細信息,比如說Servlet或者任務的名字之類的,以及一些動態的上下文信息比如用戶ID。

這麼做的話,你的代碼看起來應該是這樣的:

Thread.currentThread().setName(ProcessTask.class.getName() + : + message.getID);

更高級的做法是引入一個ThreadLocal的變量,然後配置一個appender,自動把裏面的信息輸出到日誌中。

當多個線程同時在往文件中寫入日誌而你需要關注其中某個線程的時候,這個功能尤其有用。如果你在一個分佈式或者SOA環境中運行的話,這麼做還會有一個額外的好處,下面我們很快就會看到。

  1. 分佈式標識符

在SOA或者消息驅動的架構中,某個任務的執行可能會涉及到多臺機器。這種架構下如果出了錯要進行處理的話,要想知道到底發生了什麼,這裏所牽涉到的相關機器以及它們的狀態就顯得至關重要。很多日誌分析器只是幫你把這些日誌收集起來,它們假設你已經有一個唯一的標誌符,可以用它來進行過濾。

從設計的角度來看,這意味着系統中每一個入站操作都需要有一個唯一的ID,處理過程中會一直攜帶着這個ID直到處理結束。這裏如果使用持久性標識比如說用戶ID之類的可能並不適合,因爲在一個日誌文件中一個用戶可能會有多個請求在同時進行處理,這就很難提取出具體的某個處理流。UUID是個不錯的選擇,你可以把它存儲到線程名或者TLS——ThreadLocal Storage裏面。

  1. 不要使用循環

你經常會看到有在循環體中進行日誌打印,這麼做的前提是循環的次數是有限的。

如果不出什麼問題的話當然還好。不過如果代碼碰到一些異常的輸入導致循環無法退出的話,這就不妙了。這可不止是循環無法結束的問題了,你的程序還一直在往磁盤或者網絡中寫入數據。

如果只是寫到自己的設備中,結果可能就只是掛了一臺服務器,但如果是一個分佈式的環境,就可能就是一整個集羣都癱了。所以最好還是不要在循環裏面打印日誌,尤其是當涉及到異常處理的時候。

我們來看一個例子,這裏是在循環中來打印異常的信息:

void read() {
    while (hasNext()) {
        try {
            readData();
        } catch {Exception e) {
            // this isn’t recommend
            logger.error(error reading data, e);
        }
    }
}

如果readData()拋出異常並且hasNext()返回true,這段代碼就會不停在打印日誌。一個解決方法就是不要每次都打印出來:

void read() {
    int exceptionsThrown = 0;
    while (hasNext()) {
        try {
            readData();
        } catch {Exception e) {
            if (exceptionsThrown < THRESHOLD) {
                logger.error(error reading data", e);
                exceptionsThrown++;
            } else {
                // Now the error won’t choke the system.
            }
        }
    }
}

還有一個方法就是把日誌操作從循環中去掉,在另外的地方進行打印,只記錄第一個或者最後一個異常就好了。

  1. 未捕獲的異常

維斯特洛有一道最後的防禦牆,而你有Thread.uncaughtExceptionHandler。請確認你已經用上它們了。如果沒有的話,你的異常可能這麼沒了,而你只能拿到很少的一些上下文信息,同時這些異常在哪打印,是否打印,你也不好控制。

如果你的代碼出現異常卻沒有記錄下來,或者記錄下來了卻沒有相關的狀態信息,那真是非常失敗。

儘管在uncaughtExceptionHandler裏面看似已經訪問不了線程裏面的任何變量了(它已經掛了),但你至少還有一個當前線程的引用。如果結合剛纔提到的第一條建議的話,至少日誌中還能打印出一個有意義的thread.getName()的值。

  1. 捕獲外部調用的異常

只要你調用到了JVM以外的接口,那麼發生異常的概率就大大提升了。這包括WEB服務,HTTP,數據庫,文件系統,操作系統或者其它的一些JNI調用。你得把每一個調用都當成一個定時炸彈來處理。

大多數情況下,外部調用之所以會失敗是因爲傳入了錯誤的參數。爲了修復這些問題,把這些請求參數記錄到日誌中是非常有必要的。

你可能不想記錄錯誤信息,而是直接去拋出異常,這樣做也沒有問題。不過這麼做的話,你要儘可能把相關的參數都收集起來,放到異常信息裏面去。

你得確保在上一層調用中捕獲了異常並且記錄到了日誌裏。

try {
    return s3client.generatePresignedUrl(request);
} catch (Exception e) {
    String err = String.format(Error generating request: %s bucket: %s key: %s. method: %s", request, bucket, path, method);
    log.error(err, e); //這裏你也可以拋出一個異常,記得把ERR信息帶上。
}

原文出處:日誌打印5點意見

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章