前言
一個在生產環境里運行的程序如果沒有日志是很讓維護者提心吊膽的,有太多雜亂又無意義的日志也是令人傷神。程序出現問題時候,從日志里如果發現不了問題可能的原因是很令人受挫的。本文想討論的是如何在Java程序里寫好日志。
一般來說日志分為兩種:業務日志和異常日志,使用日志我們希望能達到以下目標:
- 對程序運行情況的記錄和監控;
- 在必要時可詳細了解程序內部的運行狀態;
- 對系統性能的影響盡量小
Java日志框架
Java的日志框架太多了。。。
- Log4j 或 Log4j 2 - Apache的開源項目,通過使用Log4j,我們可以控制日志信息輸送的目的地是控制台、文件、GUI組件、甚至是套接口服務器、NT的事件記錄器、UNIX Syslog守護進程等;用戶也可以控制每一條日志的輸出格式;通過定義每一條日志信息的級別,用戶能夠更加細致地控制日志的生成過程。這些可以通過一個配置文件(XML或Properties文件)來靈活地進行配置,而不需要修改程序代碼。Log4j 2則是前任的一個升級,參考了Logback的許多特性;
- Logback - Logback是由log4j創始人設計的又一個開源日記組件。logback當前分成三個模塊:logback-core,logback- classic和logback-access。logback-core是其它兩個模塊的基礎模塊。logback-classic是log4j的一個改良版本。此外logback-classic完整實現SLF4J API使你可以很方便地更換成其它日記系統如log4j或JDK14 Logging;
- java.util.logging - JDK內置的日志接口和實現,功能比較簡;
- Slf4j - SLF4J是為各種Logging API提供一個簡單統一的接口),從而使用戶能夠在部署的時候配置自己希望的Logging API實現;
- Apache Commons Logging - Apache Commons Logging (JCL)希望解決的問題和Slf4j類似。
選項太多了的后果就是選擇困難症,我的看法是沒有最好的,只有最合適的。在比較關注性能的地方,選擇Logback或自己實現高性能Logging API可能更合適;在已經使用了Log4j的項目中,如果沒有發現問題,繼續使用可能是更合適的方式;我一般會在項目里選擇使用Slf4j, 如果不想有依賴則使用java.util.logging或框架容器已經提供的日志接口。
3.Java日志最佳實踐
定義日志變量
日志變量往往不變,最好定義成final static,變量名用大寫。
日志分級
Java的日志框架一般會提供以下日志級別,缺省打開info級別,也就是debug,trace級別的日志在生產環境不會輸出,在開發和測試環境可以通過不同的日志配置文件打開debug級別。
- fatal - 嚴重的,造成服務中斷的錯誤;
- error - 其他錯誤運行期錯誤;
- warn - 警告信息,如程序調用了一個即將作廢的接口,接口的不當使用,運行狀態不是期望的但仍可繼續處理等;
- info - 有意義的事件信息,如程序啟動,關閉事件,收到請求事件等;
- debug - 調試信息,可記錄詳細的業務處理到哪一步了,以及當前的變量狀態;
- trace - 更詳細的跟蹤信息;
在程序里要合理使用日志分級:
1 LOGGER.debug("entering getting content"); 2 String content =CacheManager.getCachedContent(); 3 if(content == null){ 4 5 //使用warn,因為程序還可以繼續執行,但類似警告太多可能說明緩存服務不可用了,值得注意 6 LOGGER.warn("Got empty content from cache,need perform database lookup"); 7 8 Connection conn = ConnectionFactory.getConnection(); 9 if (conn=null) { 10 LOGGER.error("Can't get database connection, failed to return content");//盡量提供詳細的信息,知道錯誤的原因,而不能簡單的寫logger.error("failed") 11 }else{ 12 try{ 13 content = conn.query(...); 14 }catch ( IOException e ){ 15 //異常要記錄錯誤堆棧 16 LOGGER.error("Failed to perform database lookup", e ); 17 }finally{ 18 ConnectionFactory.releaseConnection(conn); 19 } 20 } 21 } 22 //調試的時候可以知道方法的返回了 23 LOGGER.debug("returning content: "+ content); 24 return content;
基本的Logger編碼規范
1.在一個對象中通常只使用一個Logger對象,Logger應該是static final的,只有在少數需要在構造函數中傳遞logger的情況下才使用private final。
static final logger_LOG=loggerFactory.getLogger(Main.class);
2.輸出Exceptions的全部Throwable信息,因為logger.error(msg)和logger.error(msg,e.getMessage())這樣的日志輸出方法會丟失掉最重要的StackTrace信息。
void foo(){ try { // do something... }catch ( Exception e ){ _LOG.error(e.getMessage()); // 錯誤 _LOG.error("Bad things : ",e.getMessage()); // 錯誤 _LOG.error("Bad things : ",e); // 正確 } }
3.不允許記錄日志后又拋出異常,因為這樣會多次記錄日志,只允許記錄一次日志。
void foo() throws LogException{ try{ // do something... }catch ( Exception e ){ _LOG.error("Bad things : ", e); throw new LogException("Bad things : ",e); } }
4.不允許出現System print(包括System.out.println和System.error.println)語句。
void foo() { try{ // do something... }catch( Exception e ){ System.out.println(e.getMessage()); // 錯誤 System.err.println(e.getMessage()); // 錯誤 _LOG.error("Bad things : ",e ); // 正確 } }
5.不允許出現printStackTrace。
void foo() { try { // do something... }catch ( Exception e ) { e.printStackTrace(); // 錯誤 _LOG.error("Bad things : ", e ); //正確 } }
6.日志性能的考慮,如果代碼為核心代碼,執行頻率非常高,則輸出日志建議增加判斷,尤其是低級別的輸出<debug、info、warn>。
debug日志太多后可能會影響性能,有一種改進方法是:
if (LOGGER.isDebugEnabled ()) { LOGGER.debug("returning content: "+ content); }
但更好的方法是Slf4j提供的最佳實踐:
LOGGER.debug("returning content: {}", content);
一方面可以減少參數構造的開銷,另一方面也不用多寫兩行代碼。
7.有意義的日志
通常情況下在程序日志里記錄一些比較有意義的狀態數據:程序啟動,退出的時間點;程序運行消耗時間;耗時程序的執行進度;重要變量的狀態變化。
除此之外,在公共的日志里規避打印程序的調試或者提示信息。
FAQ:參考