java應用中的日誌介紹
日誌在應用程式中是非常非常重要的,好的日誌資訊能有助於我們在程式出現 BUG 時能快速進行定位,並能找出其中的原因。
但是,很多介紹 AOP 的地方都採用日誌來作為介紹,實際上日誌要採用切面的話是極其不科學的!對於日誌來說,只是在方法開始、結束、異常時輸出一些什麼,那是絕對不夠的,這樣的日誌對於日誌分析沒有任何意義。如果在方法的開始和結束整個日誌,那方法中呢?如果方法中沒有日誌的話,那就完全失去了日誌的意義!如果應用出現問題要查詢由什麼原因造成的,也沒有什麼作用。這樣的日誌還不如不用!
希望藉以本文能讓應用程式的開發人員能更加重視日誌,能在應用中輸出有意義的日誌。
日誌基本格式
日誌輸出主要在檔案中,應包括以下內容:
-
時間
-
日誌級別主要使用
-
呼叫鏈標識(可選)
-
執行緒名稱
-
日誌記錄器名稱
-
日誌內容
-
異常堆疊(不一定有)
11
:
44
:
44.827
WARN [93ef3E0120160803114444-
1.2
] [main] [ClassPathXmlApplicationContext] Exception encountered during context initialization - cancelling refresh attempt
日誌時間
作為日誌產生的日期和時間,這個資料非常重要,一般精確到毫秒。由於一般按天滾動日誌檔案,日期不需要放在這個時間中,使用 HH:mm:ss.SSS 格式即可。
日誌級別
日誌級別主要使用 DEBUG、INFO、WARN、ERROR。
DEBUG
DEUBG 級別的主要輸出除錯性質的內容,該級別日誌主要用於在開發、測試階段輸出。該級別的日誌應儘可能地詳盡,便於在開發、測試階段出現問題或者異常時,對其進行分析。
INFO
INFO 級別的主要輸出提示性質的內容,該級別日誌主要用於生產環境的日誌輸出。該級別或更高級別的日誌不要出現在迴圈中,可以在迴圈開始或者結束後輸出迴圈的次數,以及一些其他重要的資料。
-
應用啟動時所載入的配置引數值(比如:連線引數、執行緒池引數、超時時間等,以及一些與環境相關的配置,或者是整個配置引數)
-
一些重要的依賴注入物件的類名
-
方法(服務方法)的輸入引數值、返回值,由於一些方法入參的值非常多,只在入口處輸出一次就可以了,在服務方法內部或者呼叫非服務方法時就不需要再輸出了
-
方法中重要的部分,比如:從資料庫中所獲取較為重要的資料,以及呼叫第三方介面的輸入引數值和介面返回值
INFO 級別日誌原則是在生產環境中,通過 INFO 和更高級別的日誌,可以瞭解系統的執行狀況,以及出現問題或者異常時,能快速地對問題進行定位,還原當時呼叫的上下文資料,能重現問題。
建議在專案完成後,在測試環境將日誌級別調成 INFO,然後通過 INFO 級別的資訊看看是否能瞭解這個應用的運用情況,如果出現問題後是否這些日誌能否提供有用的排查問題的資訊。
WARN
WARN 級別的主要輸出警告性質的內容,這些內容是可以預知且是有規劃的,比如,某個方法入參為空或者該引數的值不滿足執行該方法的條件時。在 WARN 級別的時應輸出較為詳盡的資訊,以便於事後對日誌進行分析,不要直接寫成:
不好的日誌
log.warn(
"name is null"
);
除了輸出警告的原因之外,還需要將其他引數內容都輸出,以便於有更多的資訊供為日誌分析的參考。
推薦的日誌
log.warn(
"[{}] name is null, ignore the method, arg0: {}, arg1: {}"
, methodName , arg0 , arg1 );
ERROR
ERROR 級別主要針對於一些不可預知的資訊,諸如:錯誤、異常等,比如,在 catch 塊中抓獲的網路通訊、資料庫連線等異常,若異常對系統的整個流程影響不大,可以使用 WARN 級別日誌輸出。在輸出 ERROR 級別的日誌時,儘量多地輸出方法入引數、方法執行過程中產生的物件等資料,在帶有錯誤、異常物件的資料時,需要將該物件一併輸出:
推薦的日誌
log.error( "Invoking com.service.UserService cause error, username: {}" , username , e );
不要寫成(下面這種會將 e 作為日誌內容引數中的一個,效果與使用 e.toString() 一致,不會輸出異常堆疊):
不好的日誌
log.error(
"Invoking com.service.UserService cause error, username: {}, e: {}"
, username , e );
不要在日誌中輸出下面這樣的日誌,在異常堆疊 e 中本身就會輸出 e.getMessage 的內容,沒必要在日誌行中輸出一遍,這樣的日誌對於問題的追蹤毫無意義!
不好的日誌
log.error( e.getMessage() , e );
呼叫鏈標識
在分散式應用中,使用者的一個請求會呼叫若干個服務完成,這些服務可能還是巢狀呼叫的,因此完成一個請求的日誌並不在一個應用的日誌檔案,而是分散在不同伺服器上不同應用節點的日誌檔案中。該標識是為了串聯一個請求在整個系統中的呼叫日誌。
呼叫鏈標識格式:
-
唯一字串(trace ID)
-
呼叫層級(span ID)
呼叫鏈標識作為可選項,無該資料時只輸出 []
即可。
執行緒名稱
輸出該日誌的執行緒名稱,一般在一個應用中一個同步請求由同一執行緒完成,輸出執行緒名稱可以在各個請求產生的日誌中進行分類,便於分清當前請求上下文的日誌。
日誌記錄器名稱
日誌記錄器名稱一般使用類名,日誌檔案中可以輸出簡單的類名即可,看實際情況是否需要使用包名。主要用於看到日誌後到哪個類中去找這個日誌輸出,便於定位問題所在。
日誌內容
注意事項
禁用 System.out.println
src/main 的程式碼中嚴禁使用 System.out.println 進行輸出,因為生產環境一般不會將標準輸出和錯誤輸出重定向到檔案中去,如果程式碼中使用該方式輸出日誌,可能會導致該輸出丟失。
變參替換日誌拼接
使用 slf4j 的 Logger 進行處理,使用其變參功能進行日誌輸出,不要在日誌中進行字串的拼接,比如:
推薦的日誌
log.debug( "Load No.{} object, {}" , i , object );
不要寫成 log.debug ( "Load No." + i + " object, " + object );
這是因為將日誌級別調至 INFO 或以上級別時,這樣會增加無畏的字串拼接。
實現 toString()
需要輸出日誌的物件,應在其類中實現快速的 toString 方法,以便於在日誌輸出時僅輸出這個物件類名和 hashCode。該 toString 方法應該處理類中所有的欄位。toString 方法可以通過 IDE 的自動功能 toString 功能生成。toString 方法建議不要通過反射或者一些 toString 工具類生成,也不要直接使用 JSON 序列化工具轉為 JSON 字串,這兩者均使用反射進行處理的,僅為了輸出日誌較為影響應用的效能。
預防空指標
不要在日誌中呼叫物件的方法獲取值,除非確保該物件肯定不為 null,否則很有可能會因為日誌的問題而導致應用產生空指標異常。
不好的日誌
log.debug( "Load student(id={}), name: {}" , id , student.getName() );
可以改為(當 student 為 null 時,這樣也不會產生空指標異常):
推薦的日誌
log.debug( "Load student(id={}), student: {}" , id , student );
對於一些一定需要進行拼接字串,或者需要耗費時間、浪費記憶體才能產生的日誌內容作為日誌輸出時,應使用 log.isXxxxxEnable() 進行判斷後再進行拼接處理,比如:
推薦的程式碼
if ( log.isDebugEnable() ) {
StringBuilder builder = new StringBuilder();
for ( Student student : students ) {
builder.append( "student: " ).append( student );
}
builder.append( "value: " ).append( JSON.toJSONString(object) );
log.debug( "debug log example, detail: {}" , builder );
}
|
資訊保安
切記不要 log 密碼及個人資訊相關的內容!為了便於進行問題定位,以下是涉及敏感資訊日誌輸出時最為寬鬆(明文顯示的資料只能更少,不能更多)的要求:
型別 |
要求 |
示例 |
說明 |
密碼 | 不輸出 | ****** | 登入密碼、支付密碼等各種型別的密碼 |
信用卡 CVV2 | 不輸出 | *** | |
信用卡有效期 | 不輸出 | **** | |
驗證碼 | 不輸出 | ****** | 圖形驗證碼、簡訊驗證碼、郵件驗證碼等 |
金鑰、鹽 | 不輸出 | ****** | 用於加解密演算法的金鑰,訊息摘要的鹽,以及數字簽名及簽名驗證演算法所使用的公私鑰對等 |
會話 ID 裝置指紋 (ID) 指紋 token 密文資料 |
前 5 後 5 | 7SuA8***TtslB | 主要有以下型別: 1. 應用的會話標識,比如:Web、APP、H5 等用於識別會話狀態資訊的標識 2. APP 標識裝置的裝置指紋或者裝置 ID 3. APP 用於指紋驗證的 token 4. 密文資料指的是加密後的資料 被掩碼的字元無論多少位都輸出 3 個 * |
銀行卡卡號 | 前 6 後 4 | 622666******0831 | 銀行卡卡號最多 19 位數字 |
手機號 | 前 3 後 4 | 137****9574 | 定長 11 位數字 |
身份證號 | 前 1 後 1 | 3******X | 定長 18 位 |
姓名 | 隱姓 | *世仁 | 將姓氏隱藏 |
IP 地址 | 前 1 後 1 | 10.*.*.27 | 隱藏 IP 地址的第 2、第 3 段 |
郵箱地址 | 前 1 後 1 | w**[email protected] | 僅對 @ 之前的郵箱名稱進行掩碼,掩碼部分不管多少位均輸出 *** |
地址 | 隱號碼 | 上海市西藏北路 *** 號 *** 樓 *** 室 |
上述僅列取出部分資料的顯示要求,其他的顯示原則為通過掩碼後的資料無法得知原始資料。
實現瞭如上掩碼的工具類,參考:https://github.com/frankiegao123/mask-utils
異常堆疊
異常堆疊一般會出現在 ERROR 或者 WARN 級別的日誌中,異常堆疊含有方法呼叫鏈的系統,以及異常產生的根源。異常堆疊的日誌屬於上一行日誌的,在日誌收集時需要將其劃至上一行中。
日誌檔案
日誌檔案放置於固定的目錄中,按照一定的模板進行命名,推薦的日誌檔名稱:
-
當前正在寫入的日誌檔名:<應用名>[-<功能名>].log
-
已經滾入歷史的日誌檔名:<應用名>[-<功能名>].log.<yyyy-MM-dd>
日誌配置
輸出
根據不同的環境配置不同的日誌輸出方式:
-
本地除錯可以將日誌輸出到控制檯上
-
測試環境或者生產環境輸出到檔案中,每天產生一個檔案,如果日誌量龐大可以每個小時產生一個日誌檔案
-
生產環境中的檔案輸出,可以考慮使用非同步檔案輸出,該種方式日誌並不會馬上重新整理到檔案中去,會產生日誌延時,在停止應用時可能會導致一些還在記憶體中的日誌未能及時重新整理到檔案中去而產生丟失,如果對於應用的要求並不是非常高的話,可暫不考慮非同步日誌
logback 日誌工具可以在日誌檔案滾動後將前一檔案進行壓縮,以減少磁碟空間佔用,若使用 logback 對於日誌量龐大的應用建議開啟該功能。