系統(tǒng)日志規(guī)范及最佳實踐

打印日志是一門藝術(shù),日志信息是開發(fā)人員排查線上問題最主要的手段之一,但規(guī)范打日志被開發(fā)同學(xué)經(jīng)常所忽視。日志就像保險,平時正常的時候用不上,但是一旦出問題就都想看有沒有保險可以用。一條良好的日志,是我們向外部證明的材料。
1. 概要
1.1 什么是日志?
日志,維基百科中對其的定義是一個或多個由服務(wù)器自動創(chuàng)建和維護的日志文件,其中包含其所執(zhí)行活動的列表。
一個打印良好的日志文件可為開發(fā)人員提供精確的系統(tǒng)記錄,可輔助開發(fā)人員定位到系統(tǒng)錯誤發(fā)生的詳情及根源。在Java應(yīng)用程序中,通常使用日志文件來記錄應(yīng)用程序運行過程中的重要邏輯參數(shù)及異常錯誤,輔之日志采集系統(tǒng)(ELK、DTM)構(gòu)建系統(tǒng)監(jiān)控體系。
1.2 為什么要記錄日志?
上文中提到日志可以提供精準(zhǔn)的系統(tǒng)記錄方便根因分析,那為什么要記錄日志,記錄日志有哪些作用呢?
打印調(diào)試:用日志來記錄變量或者某一段邏輯,記錄程序運行的流程,即程序運行了哪些代碼,方便排查邏輯問題。
問題定位:程序出異常或者出故障時快速的定位問題,方便后期解決問題。因為線上生產(chǎn)環(huán)境無法debug,在測試環(huán)境去模擬一套生產(chǎn)環(huán)境費時費力。所以依靠日志記錄的信息定位問題,這點非常重要。
監(jiān)控告警 & 用戶行為審計:格式化后日志可以通過相關(guān)監(jiān)控系統(tǒng)(AntMonitor)配置多維度的監(jiān)控視圖,讓我們可以掌握系統(tǒng)運行情況或者記錄用戶的操作行為并對日志采集分析,用于建設(shè)業(yè)務(wù)大盤使用。
1.3 什么時候記錄日志?
上文說了日志的重要性,那么什么時候需要記錄日志。
代碼初始化時或進(jìn)入邏輯入口時:系統(tǒng)或者服務(wù)的啟動參數(shù)。核心模塊或者組件初始化過程中往往依賴一些關(guān)鍵配置,根據(jù)參數(shù)不同會提供不一樣的服務(wù)。務(wù)必在這里記錄INFO日志,打印出參數(shù)以及啟動完成態(tài)服務(wù)表述。
編程語言提示異常:這類捕獲的異常是系統(tǒng)告知開發(fā)人員需要加以關(guān)注的,是質(zhì)量非常高的報錯。應(yīng)當(dāng)適當(dāng)記錄日志,根據(jù)實際結(jié)合業(yè)務(wù)的情況使用WARN或者ERROR級別。
業(yè)務(wù)流程預(yù)期不符:項目代碼中結(jié)果與期望不符時也是日志場景之一,簡單來說所有流程分支都可以加入考慮。取決于開發(fā)人員判斷能否容忍情形發(fā)生。常見的合適場景包括外部參數(shù)不正確,數(shù)據(jù)處理問題導(dǎo)致返回碼不在合理范圍內(nèi)等等。
系統(tǒng)/業(yè)務(wù)核心邏輯的關(guān)鍵動作:系統(tǒng)中核心角色觸發(fā)的業(yè)務(wù)動作是需要多加關(guān)注的,是衡量系統(tǒng)正常運行的重要指標(biāo),建議記錄INFO級別日志。
第三方服務(wù)遠(yuǎn)程調(diào)用:微服務(wù)架構(gòu)體系中有一個重要的點就是第三方永遠(yuǎn)不可信,對于第三方服務(wù)遠(yuǎn)程調(diào)用建議打印請求和響應(yīng)的參數(shù),方便在和各個終端定位問題,不會因為第三方服務(wù)日志的缺失變得手足無措。
2. 基本規(guī)范
2.1?日志記錄原則
隔離性:日志輸出不能影響系統(tǒng)正常運行;
安全性:日志打印本身不能存在邏輯異?;蚵┒?,導(dǎo)致產(chǎn)生安全問題;
數(shù)據(jù)安全:不允許輸出機密、敏感信息,如用戶聯(lián)系方式、身份證號碼、token等;
可監(jiān)控分析:日志可以提供給監(jiān)控進(jìn)行監(jiān)控,分析系統(tǒng)進(jìn)行分析;
可定位排查:日志信息輸出需有意義,需具有可讀性,可供日常開發(fā)同學(xué)排查線上問題。
2.2?日志等級設(shè)置規(guī)范
在我們?nèi)粘i_發(fā)中有四種比較常見的日志打印等級,不同的等級適合在不同的時機下打印日志。
主要使用的有以下四個等級:
DEBUG
DEUBG級別的主要輸出調(diào)試性質(zhì)的內(nèi)容,該級別日志主要用于在開發(fā)、測試階段輸出。該級別的日志應(yīng)盡可能地詳盡,開發(fā)人員可以將各類詳細(xì)信息記錄到DEBUG里,起到調(diào)試的作用,包括參數(shù)信息,調(diào)試細(xì)節(jié)信息,返回值信息等等,便于在開發(fā)、測試階段出現(xiàn)問題或者異常時,對其進(jìn)行分析。
INFO
INFO級別的主要記錄系統(tǒng)關(guān)鍵信息,旨在保留系統(tǒng)正常工作期間關(guān)鍵運行指標(biāo),開發(fā)人員可以將初始化系統(tǒng)配置、業(yè)務(wù)狀態(tài)變化信息,或者用戶業(yè)務(wù)流程中的核心處理記錄到INFO日志中,方便日常運維工作以及錯誤回溯時上下文場景復(fù)現(xiàn)。建議在項目完成后,在測試環(huán)境將日志級別調(diào)成INFO,然后通過INFO級別的信息看看是否能了解這個應(yīng)用的運用情況,如果出現(xiàn)問題后是否這些日志能否提供有用的排查問題的信息。
WARN
WARN級別的主要輸出警告性質(zhì)的內(nèi)容,這些內(nèi)容是可以預(yù)知且是有規(guī)劃的,比如,某個方法入?yún)榭栈蛘咴搮?shù)的值不滿足運行該方法的條件時。在WARN級別的時應(yīng)輸出較為詳盡的信息,以便于事后對日志進(jìn)行分析。
ERROR
ERROR級別主要針對于一些不可預(yù)知的信息,諸如:錯誤、異常等,比如,在catch塊中抓獲的網(wǎng)絡(luò)通信、數(shù)據(jù)庫連接等異常,若異常對系統(tǒng)的整個流程影響不大,可以使用WARN級別日志輸出。在輸出ERROR級別的日志時,盡量多地輸出方法入?yún)?shù)、方法執(zhí)行過程中產(chǎn)生的對象等數(shù)據(jù),在帶有錯誤、異常對象的數(shù)據(jù)時,需要將該對象一并輸出。
? 如何選擇WARN/ERROR
當(dāng)方法或者功能出現(xiàn)非正常邏輯執(zhí)行情況時,需要打印WARN或者ERROR級別日志,那如何區(qū)分出現(xiàn)異常時打印WARN級別還是ERROR級別呢,我們可以從以下兩個方面進(jìn)行分析:

? 常見的WARN級別異常
用戶輸入?yún)?shù)錯誤
非核心組件初始化失敗
后端任務(wù)處理最終失?。ㄈ绻兄卦嚽抑卦嚦晒?,就不需要WARN)
數(shù)據(jù)插入冪等
? 常見的ERROR級別異常
程序啟動失敗
核心組件初始化失敗
連不上數(shù)據(jù)庫
核心業(yè)務(wù)訪問依賴的外部系統(tǒng)持續(xù)失敗
OOM
不要濫用ERROR級別日志。一般來說在配置了告警的系統(tǒng)中,WARN級別一般不會告警,ERROR級別則會設(shè)置監(jiān)控告警甚至電話報警,ERROR級別日志的出現(xiàn)意味著系統(tǒng)中發(fā)生了非常嚴(yán)重的問題,必須有人立即處理。
錯誤的使用ERROR級別日志,不區(qū)分問題的重要程度,只要是問題就采用ERROR級別日志,這是極其不負(fù)責(zé)任的表現(xiàn),因為大部分系統(tǒng)中的告警配置都是根據(jù)單位時間內(nèi)ERROR級別日志出現(xiàn)的數(shù)量來定的,隨意打ERROR日志將會造成極大的告警噪音,造成重要問題遺漏。
2.3?常見日志格式
? 摘要日志
摘要日志是格式化的標(biāo)準(zhǔn)日志文件,可用于監(jiān)控系統(tǒng)進(jìn)行監(jiān)控配置和離線日志分析的日志,通常系統(tǒng)對外提供的服務(wù)以及集成的第三方服務(wù)都需要打印對應(yīng)的服務(wù)摘要日志,摘要日志格式一般需包含以下幾類關(guān)鍵信息:
調(diào)用時間
日志鏈路id(traceId、rpcId)
線程名
接口名
方法名
調(diào)用耗時
調(diào)用是否成功(Y/N)
錯誤碼
系統(tǒng)上下文信息(調(diào)用系統(tǒng)名、調(diào)用系統(tǒng)ip、調(diào)用時間戳、是否壓測(Y/N))
? 詳細(xì)日志
詳細(xì)日志是用于補充摘要日志中的一些業(yè)務(wù)參數(shù)的日志文件,用于問題排查。詳細(xì)日志一般包含以下幾類信息:
調(diào)用時間
日志鏈路id(traceId、rpcId)
線程名
接口名
方法名
調(diào)用耗時
調(diào)用是否成功(Y/N)
系統(tǒng)上下文信息(調(diào)用系統(tǒng)名、調(diào)用系統(tǒng)ip、調(diào)用時間戳、是否壓測(Y/N))
請求入?yún)?/p>
請求出參
? 業(yè)務(wù)執(zhí)行日志
業(yè)務(wù)執(zhí)行日志就是系統(tǒng)執(zhí)行過程中輸出的日志,一般沒有特定格式,是開發(fā)人員用于跟蹤代碼執(zhí)行邏輯而打印的日志,個人看來在摘要日志、詳細(xì)日志、錯誤日志齊全的情況下,需要打印系統(tǒng)執(zhí)行日志的地方比較少。如果一定要打印業(yè)務(wù)執(zhí)行日志,需要關(guān)注以下幾個點:
這個日志是否一定要打印?如果不打印是否會影響后續(xù)問題排查,如果打印這個日志后續(xù)輸出頻率是否會太高,造成線上日志打印過多。
日志格式是否辨識度高?如果后續(xù)對該條日志進(jìn)行監(jiān)控或清洗,是否存在無法與其他日志區(qū)分或者每次打印的日志格式都不一致的問題?
輸出當(dāng)前執(zhí)行的關(guān)鍵步驟和描述,明確的表述出打印該條日志的作用,方便后續(xù)維護人員閱讀。
日志中需包含明確的打印意義,當(dāng)前執(zhí)行步驟的關(guān)鍵參數(shù)。
建議格式:[日志場景][日志含義]帶業(yè)務(wù)參數(shù)的具體信息
3. 日志最佳實踐
3.1?強制
1. 打印日志的代碼不允許失敗,阻斷流程!
一定要確保不會因為日志打印語句拋出異常造成業(yè)務(wù)流程中斷,如下圖所示,shop為null的會導(dǎo)致拋出NPE。
2. 禁止使用System.out.println()輸出日志
反例:
分析:
通過分析System.out.println源碼可知,System.out.println是一個同步方法,在高并發(fā)的情況下,大量執(zhí)行println方法會嚴(yán)重影響性能。
不能實現(xiàn)日志按等級輸出。具體來說就是不能和日志框架一樣,有 debug,info,error等級別的控制。
System.out、System.error打印的日志都并沒有打印在日志文件中,而是直接打印在終端,無法對日志進(jìn)行收集。
正例:
在日常開發(fā)或者調(diào)試的過程中,盡量使用標(biāo)準(zhǔn)日志記錄系統(tǒng)log4j2或者logback(但不要直接使用其中的API),異步的進(jìn)行日志統(tǒng)一收集。
3. 禁止直接使用日志系統(tǒng)(Log4j、Logback)中的API
應(yīng)用中不可直接使用日志系統(tǒng)(Log4j、Logback)中的API,而應(yīng)依賴使用日志框架 (SLF4J、JCL--Jakarta Commons Logging)中的API。
分析:
直接使用Log4j或者Logback中的API會導(dǎo)致系統(tǒng)代碼實現(xiàn)強耦合日志系統(tǒng),后續(xù)需要切換日志實現(xiàn)時會產(chǎn)生比較大的改造成本,統(tǒng)一使用SLF4J或者JCL等日志框架的API,其是使用門面模式的日志框架,可以做到解耦具體日志實現(xiàn)的作用,有利于后續(xù)維護和保證各個類的日志處理方式統(tǒng)一。
正例:
4. 聲明日志工具對象Logger應(yīng)聲明為private static final
分析:
聲明為private防止logger對象被其他類非法使用。
聲明為static是為了防止重復(fù)new出logger對象;防止logger被序列化,導(dǎo)致出現(xiàn)安全風(fēng)險;處于資源考慮,logger的構(gòu)造方法參數(shù)是Class,決定了logger是根據(jù)類的結(jié)構(gòu)來進(jìn)行區(qū)分日志,所以一個類只要一個logger,故static。
聲明為final是因為在類的生命周期無需變更logger,避免程序運行期對logger進(jìn)行修改。
正例:
5. 對于trace/debug/info級別的日志輸出,必須進(jìn)行日志級別的開關(guān)判斷
反例:
分析:
如果配置的日志級別是warn的話,上述日志不會打印,但是會執(zhí)行字符串拼接操作,如果name是對象, 還會執(zhí)行toString()方法,浪費了系統(tǒng)資源,執(zhí)行了上述操作,最終日志卻沒有打印,因此建議加日志開關(guān)判斷。
正例:
在trace、debug、info級別日志打印前加上對應(yīng)級別的日志開關(guān)判斷,通??梢詫㈤_關(guān)判斷邏輯包裝在日志工具類中,統(tǒng)一實現(xiàn)。
6. 捕獲異常后不要使用e.printStackTrace()打印日志
反例:
分析:
e.printStackTrace()打印出的堆棧日志跟業(yè)務(wù)代碼日志是交錯混合在一起的,通常排查異常日志不太方便。
e.printStackTrace()語句產(chǎn)生的字符串記錄的是堆棧信息,如果信息太長太多,字符串常量池所在的內(nèi)存塊沒有空間了,即內(nèi)存滿了,系統(tǒng)請求將被阻塞。
正例:
7. 打印異常日志一定要輸出全部錯誤信息
反例:
沒有打印異常e,無法定位出現(xiàn)什么類型的異常
沒有記錄詳細(xì)的堆棧異常信息,只記錄錯誤基本描述信息,不利于排查問題。
正例:
一般日志框架中的warn、error級別均有存在傳遞Throwable異常類型的API,可以直接將拋出的異常傳入日志API中。
8. 日志打印時禁止直接用JSON工具將對象轉(zhuǎn)換成String
反例:
分析:
fastjson等序列化組件是通過調(diào)用對象的get方法將對象進(jìn)行序列化,如果對象里某些get方法被覆寫,存在拋出異常的情況,則可能會因為打印日志而影響正常業(yè)務(wù)流程的執(zhí)行。
打日志過程中對一些對象的序列化過程也是比較耗性能的。首先序列化過程本身時一個計算密集型過程,費cpu。其次這個過程會產(chǎn)生很多中間對象,對內(nèi)存也不太友好。
正例:
可以使用對象的toString()方法打印對象信息,如果代碼中沒有對toString()有定制化邏輯的話,可以使用apache的ToStringBulider工具。
9. 不要打印無意義(無業(yè)務(wù)上下文、無關(guān)聯(lián)日志鏈路id)的日志
反例:
不帶任何業(yè)務(wù)信息的日志,對排查故障毫無意義。
對于無異常分支的代碼打印日志,一般流程下,異常分支都會打日志,如果沒有出現(xiàn)異常,那就說明正常執(zhí)行了。
正例:
日志一定要帶相關(guān)的業(yè)務(wù)信息,有利于排查問題快速定位到原因。
對于打印過多的無意義日志,可以直接干掉或者以debug級別打印。
10. 不要在循環(huán)中打印INFO級別日志
反例:
11. 不要打印重復(fù)的日志
反例:
分析:
在每一個嵌套環(huán)節(jié)都打印了重復(fù)的日志。
不要記錄日志后又拋出異常。拋出去的異常,一般外層會處理。如果不處理,那為什么還要拋出去?原則是,無論是否發(fā)生異常,都不要在不同地方重復(fù)記錄針對同一事件的日志消息。
正例:
直接干掉或者將日志降級成debug級別日志
12. 避免敏感信息輸出
13. 日志單行大小必須不超過200K
3.2?推薦
1. 日志語言盡量使用英文

建議:盡量在打印日志時輸出英文,防止中文編碼與終端不一致導(dǎo)致打印出現(xiàn)亂碼的情況,對故障定位和排查存在一定的干擾。
2. 重要方法可以記錄調(diào)用日志
建議在重要方法入口記錄方法調(diào)用日志,出口打印出參,對于排查問題會有很大的幫助。
3. 在核心業(yè)務(wù)邏輯中遇到if...else等條件,盡量每個分支首行都打印日志
在編寫核心業(yè)務(wù)邏輯代碼時,如遇到if...else...或者switch這樣的條件,可以在分支的首行就打印日志,這樣排查問題時,就可以通過日志,確定進(jìn)入了哪個分支,代碼邏輯更清晰,也更方便排查問題了。
建議:
4. 建議只打印必要的參數(shù),不要整個對象打印
反例:
分析:
首先分析下自己是否必須把所有對象里的字段打印出來?如果對象中有50個字段,但只需其中兩個參數(shù)就可以定位具體的原因,那么全量打印字段將浪費內(nèi)容空間且因為字段過多,影響根因排查。
正例:
使用這個種方法需及時防止npe,并考慮是否核心場景,核心場景建議還是打全,避免漏打、少打影響線上問題定位&排查。