java看log技巧_Log日志框架的学习五.正确使用日志的10个技巧
做一個苦逼的Java攻城師, 我們除了關心系統的架構這種high level的問題, 還需要了解一些語言的陷阱, 異常的處理, 以及日志的輸出, 這些"雞毛蒜皮"的細節. 這篇文章是JCP成員, Tomasz Nurkiewicz( http://nurkiewicz.blogspot.com/ )總結的10條如何正確使用日志的技巧(參見 原文). 跟那篇" java編程最差實踐"一樣, 也是針對一些細節的, 因為日志是我們排查問題, 了解系統狀況的重要線索. 我覺得對我們平常coding非常有借鑒意義. 所以轉換成中文, 加深一下印象, 也作為自己工作的一個參考.
1)選擇正確的Log開源框架
在代碼中為了知道程序的行為的狀態, 我們一般會打印一條日志:
Java代碼 ?
log.info("Happy?and?carefree?logging");
在所有的日志框架中, 我認為最好的是SLF4J. 比如在Log4J中我們會這樣寫:
Java代碼 ?
log.debug("Found?"?+?records?+?"?records?matching?filter:?'"?+?filter?+?"'");
而在SLF4J中我們會這樣寫:
Java代碼 ?
log.debug("Found?{}?records?matching?filter:?'{}'",?records,?filter);
從可讀性和系統效率來說, SLF4J( http://logback.qos.ch/ )比Log4J都要優秀(Log4J涉及到字符串連接和toString()方法的調用). 這里的{}帶來的另一個好處, 我們在盡量不損失性能的情況, 不必為了不同的日志輸出級別, 而加上類似isDebugEnabled()判斷.
SLF4J只是各種日志實現的一個接口抽象(facade), 而最佳的實現是Logback, 相對于Log4J的同門兄弟(皆出自Ceki Gülcü之手), 它在開源社區的活躍度更高.
最后要推薦的是Perf4J( http://perf4j.codehaus.org/ ). 用一句話來概括就是:
如果把log4j看做System.out.println()的話, 那么Perf4J就是System.currentTimeMillis()
Perf4J可以幫助我們更方便的輸出系統性能的日志信息. 然后借助其他報表工具將日志以圖表的形式加以展現, 從而方便我們分析系統的性能瓶頸. 關于Perf4J的使用可以參考它的開發者指南( http://perf4j.codehaus.org/devguide.html).
下面是一份關于日志jar包依賴的pom.xml參考模板:
Xml代碼 ?
Version99
Version?99?Does?Not?Exist?Maven?repository
default
http://no-commons-logging.zapto.org/mvn2
org.slf4j
slf4j-api
1.5.11
ch.qos.logback
logback-classic
0.9.20
org.slf4j
jul-to-slf4j
1.5.11
org.slf4j
log4j-over-slf4j
1.5.11
org.slf4j
jcl-over-slf4j
1.5.11
commons-logging
commons-logging
99.0-does-not-exist
下面是測試代碼:
Java代碼 ?
SLF4JBridgeHandler.install();
org.apache.log4j.Logger.getLogger("A").info("Log4J");
java.util.logging.Logger.getLogger("B").info("java.util.logging");
org.apache.commons.logging.LogFactory.getLog("C").info("commons-logging");
org.slf4j.LoggerFactory.getLogger("D").info("Logback?via?SLF4J");
上面的代碼, 無論你采用哪個log框架輸出日志, 底層采用的都是logback, 至于為什么, 可以看這里( http://www.slf4j.org/legacy.html), 另外這里為了在classpath里面不引入common-logging, 用了一個小技巧, 就是將依賴版本設置為99.0-does-not-exist, 關于這種用法的說明可以看這里( http://day-to-day-stuff.blogspot.com/2007/10/announcement-version-99-does-not-exist.html), 不過log4j的作者認為最簡單的做法就是直接去掉對common-logging的依賴, 相關內容可以看這里的說明( http://www.slf4j.org/faq.html#excludingJCL)
2) 理解正確的日志輸出級別
很多程序員都忽略了日志輸出級別, 甚至不知道如何指定日志的輸出級別. 相對于System.out來說, 日志框架有兩個最大的優點就是可以指定輸出類別(category)和級別(level). 對于日志輸出級別來說, 下面是我們應該記住的一些原則:
ERROR:系統發生了嚴重的錯誤, 必須馬上進行處理, 否則系統將無法繼續運行. 比如, NPE, 數據庫不可用等.
WARN:系統能繼續運行, 但是必須引起關注. 對于存在的問題一般可以分為兩類: 一種系統存在明顯的問題(比如, 數據不可用), 另一種就是系統存在潛在的問題, 需要引起注意或者給出一些建議(比如, 系統運行在安全模式或者訪問當前系統的賬號存在安全隱患). 總之就是系統仍然可用, 但是最好進行檢查和調整.
INFO:重要的業務邏輯處理完成. 在理想情況下, INFO的日志信息要能讓高級用戶和系統管理員理解, 并從日志信息中能知道系統當前的運行狀態. 比如對于一個機票預訂系統來說, 當一個用戶完成一個機票預訂操作之后, 提醒應該給出"誰預訂了從A到B的機票". 另一個需要輸出INFO信息的地方就是一個系統操作引起系統的狀態發生了重大變化(比如數據庫更新, 過多的系統請求).
DEBUG:主要給開發人員看, 下面會進一步談到.
TRACE: 系統詳細信息, 主要給開發人員用, 一般來說, 如果是線上系統的話, 可以認為是臨時輸出, 而且隨時可以通過開關將其關閉. 有時候我們很難將DEBUG和TRACE區分開, 一般情況下, 如果是一個已經開發測試完成的系統, 再往系統中添加日志輸出, 那么應該設為TRACE級別.
以上只是建議, 你也可以建立一套屬于你自己的規則. 但是一套良好的日志系統, 應該首先是能根據情況快速靈活的調整日志內容的輸出.
最后要提到的就是"臭名昭著"的is*Enabled()條件, 比如下面的寫法:
Java代碼 ?
if(log.isDebugEnabled())
log.debug("Place?for?your?commercial");
這種做法對性能的提高幾乎微乎其微(前面在提到SLF4J的時候已經說明), 而且是一種過度優化的表現. 極少情況下需要這樣寫, 除非構造日志信息非常耗性能. 最后必須記住一點: 程序員應該專注于日志內容, 而將日志的輸出的決定權交給日志框架去非處理.
3) 你真的知道log輸出的內容嗎?
對于你輸出的每一條log信息, 請仔細檢查最終輸出的內容是否存在問題, 其中最重要的就是避免NPE, 比如想下面這樣:
Java代碼 ?
log.debug("Processing?request?with?id:?{}",?request.getId());
這里我們能否保證request不為null? 除了NPE之外, 有時候我們可能還需要考慮, 是否會導致OOM? 越界訪問? 線程饑餓(log是同步的)? 延遲初始化異常? 日志打爆磁盤等等. 另外一個問題就是在日志中輸出集合(collection), 有時候我們輸出的集合內容可能是由Hibernate從數據庫中取出來的, 比如下面這條日志信息:
Java代碼 ?
log.debug("Returning?users:?{}",?users);
這里最佳的處理方式是僅僅輸出domain對象的id或者集合的大小(size), 而對Java來說, 不得不要吐槽幾句, 要遍歷訪問集合中每一個元素的getId方法非常繁瑣. 這一點Groovy就做的非常簡單(users*.id), 不過我們可以借助Commons Beanutils工具包來幫我們簡化:
Java代碼 ?
log.debug("Returning?user?ids:?{}",?collect(users,?"id"));
這里的collect方法的實現如下:
Java代碼 ?
public?static?Collection?collect(Collection?collection,?String?propertyName)?{
return?CollectionUtils.collect(collection,?new?BeanToPropertyValueTransformer(propertyName));
}
不過不幸的是, 在給Commons Beanutils提了一個patch(BEANUTILS-375 https://issues.apache.org/jira/browse/BEANUTILS-375)之后, 并沒有被接受:(
最后是關于toString()方法. 為了讓日志更容易理解, 最好為每一個類提供合適的toString()方法. 這里可以借助ToStringBuilder工具類. 另外一個就是關于數組和某些集合類型. 因為數組是使用的默認的toString方法. 而某些集合沒有很好的實現toString方法. 對于數組我們可以使用JDK的Arrays.deepToString()方法( http://docs.oracle.com/javase/6/docs/api/java/util/Arrays.html#deepToString%28java.lang.Object[]%29).
4) 小心日志的副作用
有時候日志或多或少的會影響系統的行為, 比如最近碰到的一個情況就是在某些條件下, Hibernate會拋出LazyInitializationException. 這是因為某些輸出日志導致延遲初始化的集合在session建立時被加載. 而在某些場景下當提高日志輸出級別時, 問題就會消失.
另一個副作用就是日志導致系統運行越來越慢. 比如不恰當的使用toString方法或者字符串連接, 使得系統出現性能問題, 曾經碰到的一個現象, 某個系統每隔15分鐘重啟一次, 這個主要是執行log輸出出現線程饑餓導致, 一般情況下, 如果一個系統一小時內生成的日志有幾百MB的時候, 就要小心了.
而如果因為日志輸出本身的問題導致正常的業務邏輯被中斷, 那就更嚴重了. 比如下面這種代碼, 最好不要這樣寫:
Java代碼 ?
try?{
log.trace("Id="?+?request.getUser().getId()?+?"?accesses?"?+?manager.getPage().getUrl().toString())
}?catch(NullPointerException?e)?{}
5) 日志信息應該簡潔且可描述
一般, 每一條日志數據會包括描述和上下文兩部分, 比如下面的日志:
Java代碼 ?
log.debug("Message?processed");
log.debug(message.getJMSMessageID());
log.debug("Message?with?id?'{}'?processed",?message.getJMSMessageID());
第一條只有描述, 第二條只有上下文, 第三條才算完整的一條日志, 還有下面這種日志:
Java代碼 ?
if(message?instanceof?TextMessage)
//...
else
log.warn("Unknown?message?type");
在上面的警告日志中沒有包含實際的message type, message id等信息, 只是表明程序有問題, 那么是什么導致了問題呢? 上下文是什么? 我們什么都不知道.
另外一個問題就是在日志中加上一個莫名其妙的內容, 即所謂的"magic log". 比如有些程序員會在日志中隨手敲上"&&&!#"這樣一串字符, 用來幫助他們定位.
一個日志文件中的內容應該易讀, 清晰, 可描述. 而不要使用莫名其妙的字符, 日志要給出當前操作做了什么, 使用的什么數據. 好的日志應該被看成文檔注釋的一部分.
最后一點, 切記不要在日志中包含密碼和個人隱私信息!
6) 正確的使用輸出模式
log輸出模式可以幫助我們在日志中增加一些清晰的上下文信息. 不過對添加的信息還是要多加小心. 比如說, 如果你是每小時輸出一個文件, 這樣你的日志文件名中已經包含了部分日期時間信息, 因此就沒必要在日志中再包含這些信息. 另外在多線程環境下也不要在自己在日志中包含線程名稱, 因為這個也可以在模式中配置.
根據我的經驗, 一個理想的日志模式將包含下列信息:
當前時間(不需要包含日志, 精確到毫秒)
日志級別(如果你關心這個)
線程名稱
簡單的日志名(非全限定名的那種)
日志描述信息
比如像下面這個logback配置:
Xml代碼 ?
%d{HH:mm:ss.SSS}?%-5level?[%thread][%logger{0}]?%m%n
千萬不要在日志信息中包含下列內容:
文件名
類名(我想這個應該是全限定名吧)
代碼行號
還有下面這種寫法也是要避免的:
Java代碼 ?
log.info("");
因為程序員知道, 在日志模式中會指定行號, 因此他就可以根據日志輸的行號出判斷指定的方法是否被調用了(比如這里可能是authenticate()方法, 進而判斷登錄的用戶已經經過了驗證). 另外, 大家也要清楚一點, 在日志模式中指定類名, 方法名以及行號會帶來嚴重的性能問題. 下面是我針對這個做的一個簡單的測試, 配置如下:
Xml代碼 ?
%d{HH:mm:ss.SSS}?%-5level?[%thread][%class{0}.%method\(\):%line][%logger{0}]?%m%n
%d{HH:mm:ss.SSS}?%-5level?[%thread][LoggerTest.testClassInfo\(\):30][%logger{0}]?%m%n
下面是測試代碼:
Java代碼 ?
import?org.junit.Test;
import?org.perf4j.StopWatch;
import?org.perf4j.slf4j.Slf4JStopWatch;
import?org.slf4j.Logger;
import?org.slf4j.LoggerFactory;
public?class?LoggerTest?{
private?static?final?Logger?log?=?LoggerFactory.getLogger(LoggerTest.class);
private?static?final?Logger?classInfoLog?=?LoggerFactory.getLogger("CLASS_INFO");
private?static?final?Logger?noClassInfoLog?=?LoggerFactory.getLogger("NO_CLASS_INFO");
private?static?final?int?REPETITIONS?=?15;
private?static?final?int?COUNT?=?100000;
@Test
public?void?testClassInfo()?throws?Exception?{
for?(int?test?=?0;?test?
testClassInfo(COUNT);
}
private?void?testClassInfo(final?int?count)?{
StopWatch?watch?=?new?Slf4JStopWatch("Class?info");
for?(int?i?=?0;?i?
classInfoLog.info("Example?message");
printResults(count,?watch);
}
@Test
public?void?testNoClassInfo()?throws?Exception?{
for?(int?test?=?0;?test?
testNoClassInfo(COUNT?*?20);
}
private?void?testNoClassInfo(final?int?count)?{
StopWatch?watch?=?new?Slf4JStopWatch("No?class?info");
for?(int?i?=?0;?i?
noClassInfoLog.info("Example?message");
printResults(count,?watch);
}
private?void?printResults(int?count,?StopWatch?watch)?{
log.info("Test?{}?took?{}ms?(avg.?{}?ns/log)",?new?Object[]{
watch.getTag(),
watch.getElapsedTime(),
watch.getElapsedTime()?*?1000?*?1000?/?count});
}
}
在上面的測試代碼中, CLASS_INFO日志輸出了1500萬次, 而NO_CLASS_INFO輸出了3億次. 后者采用一個靜態的文本來取代日志模式中的動態類信息.
從下面的對比圖可以看出, 直接在日志模式中指定類名的日志比使用反射動態獲取類名的要快13倍(平均輸出每條日志耗時:8.8 vs 115微秒). 對于一個java程序員來說, 一條日志耗時100微秒是可以接受的. 這也就是說, 一個后臺應用其中1%的時間消耗在了輸出日志上. 因此我們有時候也需要權衡一下, 每秒100條日志輸出是否是合理的.
最后要提到的是日志框架中比較高級的功能: Mapped Diagnostic Context. MDC( http://www.slf4j.org/api/org/slf4j/MDC.html )主要用來簡化基于thread-local的map參數管理. 你可以往這個map中增加任何key-value內容, 然后在隨后的日志輸出中作為模式的一部分, 與當前線程一起輸出.
7) 給方法的輸入輸出加上日志
當我們在開發過程中發現了一個bug, 一般我們會采用debug的方式一步步的跟蹤, 直到定位到最終的問題位置(如果能通過寫一個失敗的單元測試來暴露問題, 那就更帥了^_^). 但是如果實際情況不允許你debug時, 比如在客戶的系統上幾天前出現的bug. 如果你沒有詳細的日志的話, 你能找到問題的根源么?
如果你能根據一些簡單的規則來輸出每個方法的輸入和輸出(參數和返回值). 你基本上可以扔掉調試器了. 當然針對每一個方法加上日志必須是合理的: 訪問外部資源(比如數據庫), 阻塞, 等待等等, 這些地方可以考慮加上日志. 比如下面的代碼:
Java代碼 ?
public?String?printDocument(Document?doc,?Mode?mode)?{
log.debug("Entering?printDocument(doc={},?mode={})",?doc,?mode);
String?id?=?//Lengthy?printing?operation
log.debug("Leaving?printDocument():?{}",?id);
return?id;
}
因為在方法調用前后加上了日志, 我們可以非常方便的發現代碼的性能問題, 甚至找出死鎖和線程饑餓(starvation)等嚴重問題:這種情況下都只有輸入(entering)日志, 不會有輸出(leaving)日志. 如果方法名類名使用得當, 那么輸出的日志信息也將會非常賞心悅目. 因為你可以根據日志完整了解系統的運行情況, 因此分析問題的時候, 也將變得更加輕而易舉. 為了減少日志代碼, 也可以采用簡單的AOP來做日志輸出. 但是也要小心, 這種做法可能產生大量的日志.
對于這種日志, 一般采用DEBUG/TRACE級別. 當某些方法的調用非常頻繁, 那么大量的日志輸出將會影響到系統的性能, 此時我們可以提高相關類的日志級別或者干脆去掉日志輸出. 不過一般情況下, 還是建議大家多輸出一些日志. 另外也可以將日志看成一種單元測試. 輸出的日志將像單元測試一樣, 會覆蓋到整個方法的執行過程. 沒有日志的系統是不可想象的. 因此通過觀察日志的輸出將是我們了解系統是在正確的運行還是掛了的唯一方式.
8) 用日志檢查外部系統
這里是針對前面的一種場景: 如果你的系統需要和其他系統通信, 那么需要考慮是否需要用日志記錄這種交互. 一般情況下, 如果一個應用需要與多個系統進行集成, 那么診斷和分析問題將非常困難. 比如在最近的一個項目中, 由于我們在Apache CXF web服務上完整的記錄了消息數據(包括SOAP和HTTP頭信息), 使得我們在系統集成和測試階段非常happy.
如果通過ESB的方式來多個系統進行集成, 那么可以在總線(bus)上使用日志來記錄請求和響應. 這里可以參考Mules( http://www.mulesoft.org/)的( http://www.mulesoft.org/documentation/display/MULE2USER/Configuring+Components).
有時候與外部系統進行通信產生的大量日志可能讓我們無法接受. 另一方面, 我們希望打開日志臨時進行一下測試, 或者在系統出現問題的時候我們希望打開短暫的輸出日志. 這樣我們可以在輸出日志和保證系統性能之間取得一個平衡. 這里我們需要借助日志日別. 比如像下面的這樣做:
Java代碼 ?
Collection?requestIds?=?//...
if(log.isDebugEnabled())
log.debug("Processing?ids:?{}",?requestIds);
else
log.info("Processing?ids?size:?{}",?requestIds.size());
在上面的代碼中, 如果日志級別配置為DEBUG, 那么將但應所有的requestIds信息. 但是如果我們配置INFO級別, 那么只會輸出requestId的數量. 不過就像我們前面提到的日志的副作用那樣, 如果在INFO級別下requestIds為null將產生NullPointerException.
9) 正確輸出異常日志
對于日志輸出的第一條原則就是不要用日志輸出異常, 而是讓框架或者容器去處理. 記錄異常本不應該是logger的工作. 而許多程序員都會用日志輸出異常, 然后可能返回一個默認值(null, 0或者空字符串). 也可能在將異常包裝一下再拋出. 比如像下面的代碼這樣:
Java代碼 ?
log.error("IO?exception",?e);
throw?new?MyCustomException(e);
這樣做的結果可能會導致異常信息打印兩次(拋出的地方打一次, 捕獲處理的地方再打一次).
但是有時候我們確實希望打印異常, 那么應該如何處理呢? 比如下面對NPE的處理:
Java代碼 ?
try?{
Integer?x?=?null;
++x;
}?catch?(Exception?e)?{
log.error(e);????????//A
log.error(e,?e);????????//B
log.error(""?+?e);????????//C
log.error(e.toString());????????//D
log.error(e.getMessage());????????//E
log.error(null,?e);????????//F
log.error("",?e);????????//G
log.error("{}",?e);????????//H
log.error("{}",?e.getMessage());????????//I
log.error("Error?reading?configuration?file:?"?+?e);????????//J
log.error("Error?reading?configuration?file:?"?+?e.getMessage());????????//K
log.error("Error?reading?configuration?file",?e);????????//L
}
上面的代碼, 正確輸出異常信息的只有G和L, A和B甚至不能在SLF4J中編譯通過, 其他的都會丟失異常堆棧信息或者打印了不恰當的信息. 這里只要記住一條, 在日志中輸出異常信息, 第一個參數一定是一個字符串, 一般都是對問題的描述信息, 而不能是異常message(因為堆棧里面會有), 第二個參數才是具體的異常實例.
注: 對于遠程調用類型的服務拋出的異常,一定要注意實現序列化, 否則在客戶端將拋出NoClassDefFoundError異常, 而掩蓋了真實的異常信息
10) 讓日志易讀,易解析
對日志感興趣的可以分為兩類:
人(比如程序員)
機器(系統管理員寫的shell腳本)
日志的內容必須照顧到這兩個群體. 引用鮑勃大叔"Clean Code( http://www.amazon.com/Clean-Code-Handbook-Software-Craftsmanship/dp/0132350882)"一書的話來說:日志應該像代碼一樣可讀并且容易理解.
另一方面, 如果一個系統每小時要輸出幾百MB甚至上G的日志, 那么我們需要借助grep, sed以及awk來分析日志. 如果可能, 我們應該讓日志盡可能的被人和機器理解. 比如, 避免格式化數字, 使用日志模式則可以方便用正則表達式進行識別. 如果無法兼顧, 那么可以將數據用兩種格式輸出, 比如像下面這樣:
Java代碼 ?
log.debug("Request?TTL?set?to:?{}?({})",?new?Date(ttl),?ttl);
//?Request?TTL?set?to:?Wed?Apr?28?20:14:12?CEST?2010?(1272478452437)
final?String?duration?=?DurationFormatUtils.formatDurationWords(durationMillis,?true,?true);
log.info("Importing?took:?{}ms?({})",?durationMillis,?duration);
//Importing?took:?123456789ms?(1?day?10?hours?17?minutes?36?seconds)
上面的日志, 既照顧了計算機("ms after 1970 epoch"這種時間格式), 又能更好的讓人能理解("1 day 10 hours 17 minutes 36 seconds") . 另外, 這里順便廣告一下DurationFormatUtils( http://commons.apache.org/lang/api-release/org/apache/commons/lang/time/DateFormatUtils.html), 一個非常不錯的工具:)
與50位技術專家面對面20年技術見證,附贈技術全景圖總結
以上是生活随笔為你收集整理的java看log技巧_Log日志框架的学习五.正确使用日志的10个技巧的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: java 反射 new区别_JAVA的n
- 下一篇: java数组编译后_Java中数组和集合