提高日志質量的 5 大技巧
最近涌現出各種各樣能幫助你理解日志的新工具,有類似 Scribe、Logstash 這樣的開源項目,也有類似 Splunk 的預付費工具,還有托管服務如 SumoLogic 和 PaperTrail。這些工具的共同點是對日志數據進行清洗,在大量日志中提取一些更有價值的文件。
但有一件事這些工具卻愛莫能助,因為它們完全依賴你實際投入的日志數據,而如何保證數據的質量和數量則需要用戶自行完成。因此,在關鍵時刻,如果你需要基于部分或者遺漏日志做代碼調試時,事情可能會變得非常棘手。
為了減少這種情況發生,在這里分享五個建議,在你記錄日志時最好能銘記于心:
正如 Ringo,線程名稱這個屬性是 Java 中最被低估的方法之一。其原因是線程名稱大部分是描述性的。然而問題同樣出現在這里,類似人們自己,起名時通常會被賦予一定的意義。而在多線程日志中,線程名同樣揮著關鍵作用。通常情況下,大多數日志框架會記錄當前所調用的線程名稱。可悲的是,我們通常會看到 http-nio-8080-exec-3 這種名字,簡單地由線程池或容器進行分配。
出于某種原因,我們曾不止一次地聽過這種誤解——線程名稱是不可變的。與之相反,在日志中,線程名稱占據基本主要地位,你應該確保能正確使用。比如將它與具體情境結合起來,例如 Servlet 的名字、任務相關,或者一些動態語境如用戶或消息 ID。
這樣的話,代碼接口應該是這樣:
更先進的版本將被加載到當前線程的線程局部變量,配置 log appender,并自動將其添加到日志條目。
當多個線程寫入服務器日志,但你需要集中在單一線程上時,這將會非常有用。如果你在一個分布式 /SOA 環境下運行,更能看到它得天獨厚的優勢。
在 SOA 或消息驅動的架構,任務執行很可能跨多臺機器。當處理這種環境下的故障時,連接相關機器和它們的狀態將是了解具體情況的關鍵。大多數日志分析器會將這些日志信息分組,假設你為它們提供了唯一標識,它們便可以作為實際日志消息的一部分。
從設計的角度出發,這意味著,從進入系統到操作完成,每一個入站操作應該有其唯一的 ID 對應。請注意,一個持久的標識符,如用戶 ID 可能不是一個好容器。在記錄日志文件的過程中,用戶可能有多個操作,這將使得隔離特定流更加困難。UUIDs 可能是個不錯的選擇。它的值可以被加載到實際線程名稱或者作為 TLS-thread 的局部儲存器。
很多時候,你會看到一段代碼在緊密的循環中運行,并執行相應的日志操作。基本假設是,該代碼運行的次數是有限的。
很可能運行情況非常良好。但是當代碼得到意外輸入時,循環可能并不會中斷。在這種情況下,你不只是處理一個無限循環「雖然這樣已經很糟糕了」,你正在處理的代碼正將無限量的數據寫到磁盤或網絡。
在單機場景中它可能會造成一臺服務器崩潰,而在分布式場景中,被影響的則是整個集群。因此如果可能,不要在緊密循環中記錄日志。捕獲錯誤時,這一點尤其如此。
下面這個例子,記錄了一個 while 循環中的異常:
while (hasNext()) {
try {readData();
}
catch {Exception e) {
// this isn’t recommendlogger.error(“error reading data“, e);
}
}
}
如果 readData 拋出異常,而 hasNext 返回值為 true,這里將會寫入無限量的日志數據。要解決這個問題的方法是確保不會記錄這一切:
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.
}
}
}
}
另一種方法是從循環中移除日志記錄,并保存第一/最后一個異常對象并在其它地方記錄。
Westeros 有最后一道防御墻,而你有 Thread.uncaughtExceptionHandler。因此,盡量使用它們。如果沒有安裝這些處理程序,在異常拋出時,你只能獲得很少有價值的上下文,同時你也無法控制在結束之前你已經將其記錄,并確定記錄的位置。
請注意,即使在未捕獲的異常處理程序,看起來你沒有任何辦法訪問線程中(已終止)的任何變量,你仍然可以獲得實際線程對象的引用。如果你堅持# 1步,你仍然會得到一個有意義的thread.getName()值可記錄。
每當調用一個外部的 API, JVM 異常的幾率將大大增加。這包括 Web 服務、 HTTP、 DB、 文件系統、操作系統和任何其他 JNI 調用。認真對待每個調用,因為它隨時會爆炸 「它很有可能發生在同樣的點」。
大多數情況下,外部 API 故障的原因是意外輸入,日志中對其記錄是修復代碼的關鍵。
在這一點上,你可以選擇不記錄錯誤,只是拋出異常也可以。在這種情況下,只要收集到調用的相關參數,并將其解析為異常錯誤信息。
只要確保異常被捕獲并記錄在更高級別的堆棧調用即可。
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);//you can also throw a nested exception here with err instead.
}