Java 應(yīng)用中的日志

日志在應(yīng)用程序中是非常非常重要的,好的日志信息能有助于我們在程序出現(xiàn) BUG 時能快速進(jìn)行定位,并能找出其中的原因。

但是,很多介紹 AOP 的地方都采用日志來作為介紹,實際上日志要采用切面的話是極其不科學(xué)的!對于日志來說,只是在方法開始、結(jié)束、異常時輸出一些什么,那是絕對不夠的,這樣的日志對于日志分析沒有任何意義。如果在方法的開始和結(jié)束整個日志,那方法中呢?如果方法中沒有日志的話,那就完全失去了日志的意義!如果應(yīng)用出現(xiàn)問題要查找由什么原因造成的,也沒有什么作用。這樣的日志還不如不用!

希望藉以本文能讓應(yīng)用程序的開發(fā)人員能更加重視日志,能在應(yīng)用中輸出有意義的日志。

日志基本格式

日志輸出主要在文件中,應(yīng)包括以下內(nèi)容:

  • 時間
  • 日志級別主要使用
  • 調(diào)用鏈標(biāo)識(可選)
  • 線程名稱
  • 日志記錄器名稱
  • 日志內(nèi)容
  • 異常堆棧(不一定有)
11:44:44.827 WARN [93ef3E0120160803114444-1.2] [main] [ClassPathXmlApplicationContext] Exception encountered during context initialization - cancelling refresh attempt

日志時間

作為日志產(chǎn)生的日期和時間,這個數(shù)據(jù)非常重要,一般精確到毫秒。由于一般按天滾動日志文件,日期不需要放在這個時間中,使用 HH:mm:ss.SSS 格式即可。

日志級別

日志級別主要使用 DEBUG、INFO、WARN、ERROR。

DEBUG

DEUBG 級別的主要輸出調(diào)試性質(zhì)的內(nèi)容,該級別日志主要用于在開發(fā)、測試階段輸出。該級別的日志應(yīng)盡可能地詳盡,便于在開發(fā)、測試階段出現(xiàn)問題或者異常時,對其進(jìn)行分析。

INFO

INFO 級別的主要輸出提示性質(zhì)的內(nèi)容,該級別日志主要用于生產(chǎn)環(huán)境的日志輸出。該級別或更高級別的日志不要出現(xiàn)在循環(huán)中,可以在循環(huán)開始或者結(jié)束后輸出循環(huán)的次數(shù),以及一些其他重要的數(shù)據(jù)。

  • 應(yīng)用啟動時所加載的配置參數(shù)值(比如:連接參數(shù)、線程池參數(shù)、超時時間等,以及一些與環(huán)境相關(guān)的配置,或者是整個配置參數(shù))
  • 一些重要的依賴注入對象的類名
  • 方法(服務(wù)方法)的輸入?yún)?shù)值、返回值,由于一些方法入?yún)⒌闹捣浅6啵辉谌肟谔庉敵鲆淮尉涂梢粤?,在服?wù)方法內(nèi)部或者調(diào)用非服務(wù)方法時就不需要再輸出了
  • 方法中重要的部分,比如:從數(shù)據(jù)庫中所獲取較為重要的數(shù)據(jù),以及調(diào)用第三方接口的輸入?yún)?shù)值和接口返回值

INFO 級別日志原則是在生產(chǎn)環(huán)境中,通過 INFO 和更高級別的日志,可以了解系統(tǒng)的運行狀況,以及出現(xiàn)問題或者異常時,能快速地對問題進(jìn)行定位,還原當(dāng)時調(diào)用的上下文數(shù)據(jù),能重現(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)行分析,不要直接寫成:

不好的日志
log.warn( "name is null" );

除了輸出警告的原因之外,還需要將其他參數(shù)內(nèi)容都輸出,以便于有更多的信息供為日志分析的參考。

推薦的日志
log.warn( "[{}] name is null, ignore the method, arg0: {}, arg1: {}" , methodName , arg0 , arg1 );

ERROR

ERROR 級別主要針對于一些不可預(yù)知的信息,諸如:錯誤、異常等,比如,在 catch 塊中抓獲的網(wǎng)絡(luò)通信、數(shù)據(jù)庫連接等異常,若異常對系統(tǒng)的整個流程影響不大,可以使用 WARN 級別日志輸出。在輸出 ERROR 級別的日志時,盡量多地輸出方法入?yún)?shù)、方法執(zhí)行過程中產(chǎn)生的對象等數(shù)據(jù),在帶有錯誤、異常對象的數(shù)據(jù)時,需要將該對象一并輸出:

推薦的日志
log.error( "Invoking com.service.UserService cause error, username: {}" , username , e );

不要寫成(下面這種會將 e 作為日志內(nèi)容參數(shù)中的一個,效果與使用 e.toString() 一致,不會輸出異常堆棧):

不好的日志
log.error( "Invoking com.service.UserService cause error, username: {}, e: {}" , username , e );

不要在日志中輸出下面這樣的日志,在異常 e 中本身就會輸出 e.getMessage() 的內(nèi)容,沒必要在日志行中輸出一遍,這樣的日志對于問題的追蹤毫無意義!

不好的日志
log.error( e.getMessage() , e );

調(diào)用鏈標(biāo)識

在分布式應(yīng)用中,用戶的一個請求會調(diào)用若干個服務(wù)完成,這些服務(wù)可能還是嵌套調(diào)用的,因此完成一個請求的日志并不在一個應(yīng)用的日志文件,而是分散在不同服務(wù)器上不同應(yīng)用節(jié)點的日志文件中。該標(biāo)識是為了串聯(lián)一個請求在整個系統(tǒng)中的調(diào)用日志。

調(diào)用鏈標(biāo)識格式:

  • 唯一字符串(trace ID)
  • 調(diào)用層級(span ID)

調(diào)用鏈標(biāo)識作為可選項,無該數(shù)據(jù)時只輸出 [] 即可。

線程名稱

輸出該日志的線程名稱,一般在一個應(yīng)用中一個同步請求由同一線程完成,輸出線程名稱可以在各個請求產(chǎn)生的日志中進(jìn)行分類,便于分清當(dāng)前請求上下文的日志。

日志記錄器名稱

日志記錄器名稱一般使用類名,日志文件中可以輸出簡單的類名即可,看實際情況是否需要使用包名。主要用于看到日志后到哪個類中去找這個日志輸出,便于定位問題所在。

日志內(nèi)容

注意事項

禁用 System.out.println

src/main 的代碼中嚴(yán)禁使用 System.out.println 進(jìn)行輸出,因為生產(chǎn)環(huán)境一般不會將標(biāo)準(zhǔn)輸出和錯誤輸出重定向到文件中去,如果代碼中使用該方式輸出日志,可能會導(dǎo)致該輸出丟失。

變參替換日志拼接

使用 slf4j 的 Logger 進(jìn)行處理,使用其變參功能進(jìn)行日志輸出,不要在日志中進(jìn)行字符串的拼接,比如:

推薦的日志
log.debug( "Load No.{} object, {}" , i , object );

不要寫成 log.debug( "Load No." + i + " object, " + object ); 這是因為將日志級別調(diào)至 INFO 或以上級別時,這樣會增加無畏的字符串拼接。

實現(xiàn) toString()

需要輸出日志的對象,應(yīng)在其類中實現(xiàn)快速的 toString 方法,以便于在日志輸出時輸出這個對象中所有的字段。toString 方法可以通過 IDE 自動功能的 toString 功能生成。toString 方法不建議通過反射或者一些 toString 工具類生成,也不要直接使用 JSON 序列化工具轉(zhuǎn)為 JSON 字符串,這兩者均使用反射進(jìn)行處理的,若僅用于輸出日志則較為影響應(yīng)用的性能。

預(yù)防空指針

不要在日志中調(diào)用對象的方法獲取值,除非確保該對象肯定不為 null 值,否則很有可能會因為日志的問題而導(dǎo)致應(yīng)用產(chǎn)生空指針異常。

不好的日志
log.debug( "Load student(id={}), name: {}" , id , student.getName() );

可以改為以下方法,即使 student 對象為 null 值時,也不會產(chǎn)生空指針異常:

推薦的日志
log.debug( "Load student(id={}), student: {}" , id , student );

對于一些一定需要進(jìn)行拼接字符串,或者需要耗費時間、浪費內(nèi)存才能產(chǎn)生的日志內(nèi)容作為日志輸出時,應(yīng)使用 log.isXxxxxEnable() 進(jìn)行判斷后再進(jìn)行拼接處理,比如:

推薦的代碼
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 );
}

信息安全

切記不要在日志中記錄密碼及個人信息相關(guān)的內(nèi)容!為了便于進(jìn)行問題定位,以下是涉及敏感信息日志輸出時最為寬松(明文顯示的數(shù)據(jù)只能更少,不能更多)的要求:

類型 要求 示例 說明
密碼 不輸出 ****** 登錄密碼、支付密碼等各種類型的密碼
信用卡 CVV2 不輸出 ***
信用卡有效期 不輸出 ****
驗證碼 不輸出 ****** 圖形驗證碼、短信驗證碼、郵件驗證碼等
密鑰、鹽 不輸出 ****** 用于加解密算法的密鑰,消息摘要的鹽,以及數(shù)字簽名及簽名驗證算法所使用的公私鑰對等
? 會話 ID
? 設(shè)備指紋 (ID)
? 指紋 token
密文數(shù)據(jù) 前 5 后 5 7SuA8***TtslB 主要有以下類型:
1. 應(yīng)用的會話標(biāo)識,比如:Web、APP、H5 等用于識別會話狀態(tài)信息的標(biāo)識
2. APP 標(biāo)識設(shè)備的設(shè)備指紋或者設(shè)備 ID
3. APP 用于指紋驗證的 token
4. 密文數(shù)據(jù)指的是加密后的數(shù)據(jù)
被掩碼的字符無論多少位都輸出 3 個 *
銀行卡卡號 前 6 后 4 622666******0831 銀行卡卡號最多 19 位數(shù)字
手機(jī)號 前 3 后 4 137****9574 定長 11 位數(shù)字
身份證號 前 1 后 1 3******X 定長 18 位
姓名 隱姓 *世仁 將姓氏隱藏
IP 地址 前 1 后 1 10.*.*.27 隱藏 IP 地址的第 2、第 3 段
郵箱地址 前 1 后 1 w***3@gmail.com 僅對 @ 之前的郵箱名稱進(jìn)行掩碼,掩碼部分不管多少位均輸出 ***
地址 隱號碼 上海市西藏北路 *** 號 *** 樓 *** 室

上述僅列取出部分?jǐn)?shù)據(jù)的顯示要求,其他的顯示原則為通過掩碼后的數(shù)據(jù)無法得知原始數(shù)據(jù)。

實現(xiàn)了如上掩碼的工具類,參考:https://github.com/frankiegao123/mask-utils。

異常堆棧

異常堆棧一般會出現(xiàn)在 ERROR 或者 WARN 級別的日志中,異常堆棧含有方法調(diào)用鏈的系統(tǒng),以及異常產(chǎn)生的根源。異常堆棧的日志屬于上一行日志的,在日志收集時需要將其劃至上一行中。

日志文件

日志文件放置于固定的目錄中,按照一定的模板進(jìn)行命名,推薦的日志文件名稱:

  • 當(dāng)前正在寫入的日志文件名:<應(yīng)用名>[-<功能名>].log
  • 已經(jīng)滾入歷史的日志文件名:<應(yīng)用名>[-<功能名>].log.<yyyy-MM-dd>

日志配置

輸出

根據(jù)不同的環(huán)境配置不同的日志輸出方式:

  • 本地調(diào)試可以將日志輸出到控制臺上
  • 測試環(huán)境或者生產(chǎn)環(huán)境輸出到文件中,每天產(chǎn)生一個文件,如果日志量龐大可以每個小時產(chǎn)生一個日志文件

生產(chǎn)環(huán)境中的文件輸出,可以考慮使用異步文件輸出,該種方式日志并不會馬上刷新到文件中去,會產(chǎn)生日志延時,在停止應(yīng)用時可能會導(dǎo)致一些還在內(nèi)存中的日志未能及時刷新到文件中去而產(chǎn)生丟失,如果對于應(yīng)用的要求并不是非常高的話,可暫不考慮異步日志。

logback 日志工具可以在日志文件滾動后將前一文件進(jìn)行壓縮,以減少磁盤空間占用,若使用 logback 對于日志量龐大的應(yīng)用建議開啟該功能。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容