歡迎光臨
每天分享高質量文章

Java 應用中的日誌

點擊上方“芋道原始碼”,選擇“置頂公眾號”

技術文章第一時間送達!

原始碼精品專欄

 

來源:https://mp.weixin.qq.com/s/MG0ZMMZe1REoPm847ORsiA

日誌在應用程式中是非常非常重要的,好的日誌信息能有助於我們在程式出現 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**3@gmail.com 僅對 @ 之前的郵箱名稱進行掩碼,掩碼部分不管多少位均輸出 ***
地址
隱號碼
上海市西藏北路 *** 號 *** 樓 *** 室

上述僅列取出部分資料的顯示要求,其他的顯示原則為通過掩碼後的資料無法得知原始資料。

實現瞭如上掩碼的工具類,參考:https://github.com/frankiegao123/mask-utils

異常堆棧

異常堆棧一般會出現在 ERROR 或者 WARN 級別的日誌中,異常堆棧含有方法呼叫鏈的系統,以及異常產生的根源。異常堆棧的日誌屬於上一行日誌的,在日誌收集時需要將其劃至上一行中。

日誌檔案

日誌檔案放置於固定的目錄中,按照一定的模板進行命名,推薦的日誌檔案名稱:

  • 當前正在寫入的日誌檔案名:[-].log

  • 已經滾入歷史的日誌檔案名:[-].log.

日誌配置

輸出

根據不同的環境配置不同的日誌輸出方式:

  • 本地除錯可以將日誌輸出到控制臺上

  • 測試環境或者生產環境輸出到檔案中,每天產生一個檔案,如果日誌量龐大可以每個小時產生一個日誌檔案

  • 生產環境中的檔案輸出,可以考慮使用異步檔案輸出,該種方式日誌並不會馬上掃清到檔案中去,會產生日誌延時,在停止應用時可能會導致一些還在記憶體中的日誌未能及時掃清到檔案中去而產生丟失,如果對於應用的要求並不是非常高的話,可暫不考慮異步日誌

logback 日誌工具可以在日誌檔案滾動後將前一檔案進行壓縮,以減少磁盤空間占用,若使用 logback 對於日誌量龐大的應用建議開啟該功能。




如果你對 Dubbo / Netty 等等原始碼與原理感興趣,歡迎加入我的知識星球一起交流。長按下方二維碼噢

目前在知識星球更新了《Dubbo 原始碼解析》目錄如下:

01. 除錯環境搭建
02. 專案結構一覽
03. 配置 Configuration
04. 核心流程一覽

05. 拓展機制 SPI

06. 執行緒池

07. 服務暴露 Export

08. 服務取用 Refer

09. 註冊中心 Registry

10. 動態編譯 Compile

11. 動態代理 Proxy

12. 服務呼叫 Invoke

13. 呼叫特性 

14. 過濾器 Filter

15. NIO 服務器

16. P2P 服務器

17. HTTP 服務器

18. 序列化 Serialization

19. 集群容錯 Cluster

20. 優雅停機

21. 日誌適配

22. 狀態檢查

23. 監控中心 Monitor

24. 管理中心 Admin

25. 運維命令 QOS

26. 鏈路追蹤 Tracing

… 一共 69+ 篇

目前在知識星球更新了《Netty 原始碼解析》目錄如下:

01. 除錯環境搭建
02. NIO 基礎
03. Netty 簡介
04. 啟動 Bootstrap

05. 事件輪詢 EventLoop

06. 通道管道 ChannelPipeline

07. 通道 Channel

08. 位元組緩衝區 ByteBuf

09. 通道處理器 ChannelHandler

10. 編解碼 Codec

11. 工具類 Util

… 一共 61+ 篇

目前在知識星球更新了《資料庫物體設計》目錄如下:


01. 商品模塊
02. 交易模塊
03. 營銷模塊
04. 公用模塊

… 一共 17+ 篇

原始碼不易↓↓↓

點贊支持老艿艿↓↓

赞(0)

分享創造快樂