程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> 關於JAVA >> Java日志管理的最佳實踐

Java日志管理的最佳實踐

編輯:關於JAVA

概述

對於現在的應用程序來說,日志的重要性是不言而喻的。很難想象沒有任何日志記錄功能的應用程序運行在生產環境中。日志所能提供的功能是多種多樣的,包括記錄程序運行時產生的錯誤信息、狀態信息、調試信息和執行時間信息等。在生產環境中,日志是查找問題來源的重要依據。應用程序運行時的產生的各種信息,都應該通過日志 API 來進行記錄。很多開發人員習慣於使用 System.out.println、System.err.println 以及異常對象的 printStrackTrace 方法來輸出相關信息。這些使用方式雖然簡便,但是所產生的信息在出現問題時並不能提供有效的幫助。這些使用方式都應該改為使用日志 API。使用日志 API 並沒有增加很多復雜度,但是所提供的好處是顯著的。

盡管記錄日志是應用開發中並不可少的功能,在 JDK 的最初版本中並不包含日志記錄相關的 API 和實現。相關的 API(java.util.logging 包,JUL)和實現,直到 JDK 1.4 才被加入。因此在日志記錄這一個領域,社區貢獻了很多開源的實現。其中比較流行的包括 log4j 及其後繼者 logback。除了真正的日志記錄實現之外,還有一類與日志記錄相關的封裝 API,如 Apache Commons Logging 和 SLF4J。這類庫的作用是在日志記錄實現的基礎上提供一個封裝的 API 層次,對日志記錄 API 的使用者提供一個統一的接口,使得可以自由切換不同的日志記錄實現。比如從 JDK 的默認日志記錄實現 JUL 切換到 log4j。這類封裝 API 庫在框架的實現中比較常用,因為需要考慮到框架使用者的不同需求。在實際的項目開發中則使用得比較少,因為很少有項目會在開發中切換不同的日志記錄實現。本文對於這兩類庫都會進行具體的介紹。

記錄日志只是有效地利用日志的第一步,更重要的是如何對程序運行時產生的日志進行處理和分析。典型的情景包括當日志中包含滿足特定條件的記錄時,觸發相應的通知機制,比如郵件或短信通知;還可以在程序運行出現錯誤時,快速地定位潛在的問題源。這樣的處理和分析的能力對於實際系統的維護尤其重要。當運行系統中包含的組件過多時,日志對於錯誤的診斷就顯得格外重要。

本文首先介紹關於日志 API 的基本內容。

Java 日志 API

從功能上來說,日志 API 本身所需求的功能非常簡單,只需要能夠記錄一段文本即可。API 的使用者在需要進行記錄時,根據當前的上下文信息構造出相應的文本信息,調用 API 完成記錄。一般來說,日志 API 由下面幾個部分組成:

記錄器(Logger):日志 API 的使用者通過記錄器來發出日志記錄請求,並提供日志的內容。在記錄日志時,需要指定日志的嚴重性級別。

格式化器(Formatter):對記錄器所記錄的文本進行格式化,並添加額外的元數據。

處理器(Handler):把經過格式化之後的日志記錄輸出到不同的地方。常見的日志輸出目標包括控制台、文件和數據庫等。

記錄器

當程序中需要記錄日志時,首先需要獲取一個日志記錄器對象。一般的日志記錄 API 都提供相應的工廠方法來創建記錄器對象。每個記錄器對象都是有名稱的。一般的做法是使用當前的 Java 類的名稱或所在包的名稱作為記錄器對象的名稱。記錄器的名稱通常是具有層次結構的,與 Java 包的層次結構相對應。比如 Java 類“com.myapp.web.IndexController”中使用的日志記錄器的名稱一般是“com.myapp.web.IndexController”或“com.myapp.web”。除了使用類名或包名之外,還可以根據日志記錄所對應的功能來進行劃分,從而選擇不同的名稱。比如用“security”作為所有與安全相關的日志記錄器的名稱。這樣的命名方式對於某些橫切的功能比較實用。開發人員一般習慣於使用當前的類名作為日志記錄器的名稱,這樣可以快速在日志記錄中定位到產生日志的 Java 類。使用有意義的其他名稱在很多情況下也是一個不錯的選擇。

在通過日志記錄器對象記錄日志時,需要指定日志的嚴重性級別。根據每個記錄器對象的不同配置,低於某個級別的日志消息可能不會被記錄下來。該級別是日志 API 的使用者根據日志記錄中所包含的信息來自行決定的。不同的日志記錄 API 所定義的級別也不盡相同。日志記錄封裝 API 也會定義自己的級別並映射到底層實現中相對應的實際級別。比如 JDK 標准的日志 API 使用的級別包括 OFF、SEVERE、WARNING、INFO、CONFIG、FINE、FINER、FINEST 和 ALL 等,Log4j 使用的級別則包括 OFF、FATAL、ERROR、WARN、INFO、DEBUG、TRACE 和 ALL 等。一般情況下,使用得比較多的級別是 FATAL、ERROR、WARN、INFO、DEBUG 和 TRACE 等。這 6 個級別所對應的情況也有所不同:

FATAL:導致程序提前結束的嚴重錯誤。

ERROR:運行時異常以及預期之外的錯誤。

WARN:預期之外的運行時狀況,不一定是錯誤的情況。

INFO:運行時產生的事件。

DEBUG:與程序運行時的流程相關的詳細信息。

TRACE:更加具體的詳細信息。

在這 6 個級別中,以 ERROR、WARN、INFO 和 DEBUG 作為常用。

日志記錄 API 的使用者通過記錄器來記錄日志消息。日志消息在記錄下來之後只能以文本的形式保存。不過有的實現(如 Log4j)允許在記錄日志時使用任何 Java 對象。非 String 類型的對象會被轉換成 String 類型。由於日志記錄通常在出現異常時使用,記錄器在記錄消息時可以把產生的異常(Throwable 類的對象)也記錄下來。

每個記錄器對象都有一個運行時對應的嚴重性級別。該級別可以通過配置文件或代碼的方式來進行設置。如果沒有顯式指定嚴重性級別,則會根據記錄器名稱的層次結構關系往上進行查找,直到找到一個設置了嚴重性級別的名稱為止。比如名稱為“com.myapp.web.IndexController”的記錄器對象,如果沒有顯式指定其嚴重性級別,則會依次查找是否有為名稱“com.myapp.web”、“com.myapp”和“com”指定的嚴重性級別。如果仍然沒有找到,則使用根記錄器配置的值。

通過記錄器對象來記錄日志時,只是發出一個日志記錄請求。該請求是否會完成取決於請求和記錄器對象的嚴重性級別。記錄器使用者產生的低於記錄器對象嚴重性級別的日志消息不會被記錄下來。這樣的記錄請求會被忽略。除了基於嚴重性級別的過濾方式之外,日志記錄框架還支持其他自定義的過濾方式。比如 JUL 可以通過實現 java.util.logging.Filter 接口的方式來進行過濾。Log4j 可以通過繼承 org.apache.log4j.spi.Filter 類的方式來過濾。

格式化器

實際記錄的日志中除了使用記錄器對象時提供的消息之外,還包括一些元數據。這些元數據由日志記錄框架來提供。常用的信息包括記錄器的名稱、時間戳、線程名等。格式化器用來確定所有這些信息在日志記錄中的展示方式。不同的日志記錄實現提供各自默認的格式化方式和自定義支持。

JUL 中通過繼承 java.util.logging.Formatter 類來自定義格式化的方式,並提供了兩個標准實現 SimpleFormatter 類和 XMLFormatter 類。清單 1 中給出了 JUL 中自定義格式化器的實現方式,只需要繼承自 Formatter 類並實現 format 方法即可。參數 LogRecord 類的對象中包含了日志記錄中的全部信息。

清單 1. JUL 中自定義格式化器的實現

點擊查看代碼清單

對於自定義的格式化器類,需要在 JUL 的配置文件中進行指定,如清單 2 所示。

清單 2. 在 JUL 配置文件中指定自定義的格式化器類

java.util.logging.ConsoleHandler.formatter = logging.jul.CustomFormatter

Log4j 在格式化器的實現上要簡單一些,由 org.apache.log4j.PatternLayout 類來負責完成日志記錄的格式化。在自定義時不需要創建新的 Java 類,而是通過配置文件指定所需的格式化模式。在格式化模式中,不同的占位符表示不同類型的信息。比如“%c”表示記錄器的名稱,“%d”表示日期,“%m”表示日志的消息文本,“%p”表示嚴重性級別,“%t”表示線程的名稱。清單 3 給出了 Log4j 配置文件中日志記錄的自定義方式。
清單 3. Log4j 中日志記錄的自定義方式

log4j.appender.stdout.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} [%p] %c - %m%n

日志處理器

日志記錄經過格式化之後,由不同的處理器來進行處理。不同的處理器有各自不同的處理方式。比如控制台處理器會把日志輸出到控制台中,文件處理器把日志寫入到文件中。除了這些之外,還有寫入到數據庫、通過郵件發送、寫入到 JMS 隊列等各種不同的處理方式。

日志處理器也可以配置所處理日志信息的最低嚴重性級別。低於該級別的日志不會被處理。這樣可以控制所處理的日志記錄數量。比如控制台處理器的級別一般設置為 INFO,而文件處理器則一般設置為 DEBUG。

日志記錄框架一般提供了比較多的日志處理器實現。開發人員也可以創建自定義的實現。

Java 日志封裝 API

除了 JUL 和 log4j 這樣的日志記錄庫之外,還有一類庫用來封裝不同的日志記錄庫。這樣的封裝庫中一開始以 Apache Commons Logging 框架最為流行,現在比較流行的是 SLF4J。這樣封裝庫的 API 都比較簡單,只是在日志記錄庫的 API 基礎上做了一層簡單的封裝,屏蔽不同實現之間的區別。由於日志記錄實現所提供的 API 大致上比較相似,封裝庫的作用更多的是達到語法上的一致性。

在 Apache Commons Logging 庫中,核心的 API 是 org.apache.commons.logging.LogFactory 類和 org.apache.commons.logging.Log 接口。LogFactory 類提供了工廠方法用來創建 Log 接口的實現對象。比如 LogFactory.getLog 可以根據 Java 類或名稱來創建 Log 接口的實現對象。Log 接口中為 6 個不同的嚴重性級別分別定義了一組方法。比如對 DEBUG 級別,定義了 isDebugEnabled()、debug(Object message) 和 debug(Object message, Throwable t) 三個方法。從這個層次來說,Log 接口簡化了對於日志記錄器的使用。

SLF4J 庫的使用方式與 Apache Commons Logging 庫比較類似。SLF4J 庫中核心的 API 是提供工廠方法的 org.slf4j.LoggerFactory 類和記錄日志的 org.slf4j.Logger 接口。通過 LoggerFactory 類的 getLogger 方法來獲取日志記錄器對象。與 Apache Commons Logging 庫中的 Log 接口類似,Logger 接口中的方法也是按照不同的嚴重性級別來進行分組的。Logger 接口中有同樣 isDebugEnabled 方法。不過 Logger 接口中發出日志記錄請求的 debug 等方法使用 String 類型來表示消息,同時可以使用包含參數的消息,如清單 4 所示。

清單 4. SLF4J 的使用方式
public class Slf4jBasic { 
   private static final Logger LOGGER = LoggerFactory.getLogger(Slf4jBasic.class); 
   public void logBasic() { 
       if (LOGGER.isInfoEnabled()) { 
           LOGGER.info("My log message for %s", "Alex"); 
       } 
   } 
}

MDC

MDC(Mapped Diagnostic Context,映射調試上下文)是 log4j 和 logback 提供的一種方便在多線程條件下記錄日志的功能。某些應用程序采用多線程的方式來處理多個用戶的請求。在一個用戶的使用過程中,可能有多個不同的線程來進行處理。典型的例子是 Web 應用服務器。當用戶訪問某個頁面時,應用服務器可能會創建一個新的線程來處理該請求,也可能從線程池中復用已有的線程。在一個用戶的會話存續期間,可能有多個線程處理過該用戶的請求。這使得比較難以區分不同用戶所對應的日志。當需要追蹤某個用戶在系統中的相關日志記錄時,就會變得很麻煩。

一種解決的辦法是采用自定義的日志格式,把用戶的信息采用某種方式編碼在日志記錄中。這種方式的問題在於要求在每個使用日志記錄器的類中,都可以訪問到用戶相關的信息。這樣才可能在記錄日志時使用。這樣的條件通常是比較難以滿足的。MDC 的作用是解決這個問題。

MDC 可以看成是一個與當前線程綁定的哈希表,可以往其中添加鍵值對。MDC 中包含的內容可以被同一線程中執行的代碼所訪問。當前線程的子線程會繼承其父線程中的 MDC 的內容。當需要記錄日志時,只需要從 MDC 中獲取所需的信息即可。MDC 的內容則由程序在適當的時候保存進去。對於一個 Web 應用來說,通常是在請求被處理的最開始保存這些數據。清單 5 中給出了 MDC 的使用示例。

清單 5. MDC 使用示例

public class MdcSample { 
   private static final Logger LOGGER = Logger.getLogger("mdc"); 
   public void log() { 
       MDC.put("username", "Alex"); 
       if (LOGGER.isInfoEnabled()) { 
           LOGGER.info("This is a message."); 
       } 
   } 
}

清單 5 中,在記錄日志前,首先在 MDC 中保存了名稱為“username”的數據。其中包含的數據可以在格式化日志記錄時直接引用,如清單 6 所示,“%X{username}”表示引用 MDC 中“username”的值。
清單 6. 使用 MDC 中記錄的數據

log4j.appender.stdout.layout.ConversionPattern=%X{username} %d{yyyy-MM-dd HH:mm:ss} [%p] %c - %m%n

日志記錄最佳實踐

下面主要介紹一些在記錄日志時的比較好的實踐。
檢查日志是否可以被記錄

當日志記錄器收到一個日志記錄請求時,如果請求的嚴重性級別低於記錄器對象的實際有效級別,則該請求會被忽略。在日志記錄方法的實現中會首先進行這樣的檢查。不過推薦的做法是在調用 API 進行記錄之前,首先進行相應的檢查,這樣可以避免不必要的性能問題,如清單 7 所示。
清單 7. 檢查日志是否可以被記錄

if (LOGGER.isDebugEnabled()) {
   LOGGER.debug("This is a message.");
}

清單 7 中的做法的作用在於避免了構造日志記錄消息所帶來的開銷。日志消息中通常包含與當前上下文相關的信息。為了獲取這些信息並構造相應的消息文本,不可避免會產生額外的開銷。尤其對於 DEBUG 和 TRACE 級別的日志消息來說,它們所出現的頻率很高,累加起來的開銷比較大。因此在記錄 INFO、DEBUG 和 TRACE 級別的日志時,首先進行相應的檢查是一個好的實踐。而 WARN 及其以上級別的日志則一般不需要進行檢查。
日志中包含充分的信息

日志中所包含的信息應該是充分的。在記錄日志消息時應該盡可能多的包含當前上下文中的各種信息,以方便在遇到問題時可以快速的獲取到所需的信息。比如在網上支付功能中,與支付相關的日志應該完整的包含當前用戶、訂單以及支付方式等全部信息。一種比較常見的做法是把相關的日志記錄分散在由不同日志記錄器所記錄的日志中。當出現問題之後,需要手工查找並匹配相關的日志來定位問題,所花費的時間和精力會更多。因此,應該盡可能在一條日志記錄中包含足夠多的信息。
使用合適的日志記錄器名稱

一般的日志記錄實踐是使用當前 Java 類的全名作為其使用的日志記錄器的名稱。這樣做可以得到一個與 Java 類和包的層次結構相對應的日志記錄器的層次結構。可以很方便的按照不同的模塊來設置相應的日志記錄級別。不過對於某些全局的或是橫切的功能,如安全和性能等,則推薦使用功能相關的名稱。比如程序中可能包含用來提供性能剖析信息的日志記錄。對於這樣的日志記錄,應該使用同一名稱的日志記錄器,如類似“performance”或“performance.web”。這樣當需要啟用和禁用性能剖析時,只需要配置這些名稱的記錄器即可。
使用半結構化的日志消息

在介紹日志記錄 API 中的格式化器時提到過,日志記錄中除了基本的日志消息之外,還包括由日志框架提供的其他元數據。這些數據按照給定的格式出現在日志記錄中。這些半結構化的格式使得可以通過工具提取日志記錄中的相關信息進行分析。在使用日志 API 進行記錄時,對於日志消息本身,也推薦使用半結構化的方式來組織。

比如一個電子商務的網站,當用戶登錄之後,該用戶所產生的不同操作所對應的日志記錄中都可以包含該用戶的用戶名,並以固定的格式出現在日志記錄中,如清單 8 所示。
清單 8. 使用半結構化的日志消息

[user1] 用戶登錄成功。
[user1] 用戶成功購買產品 A。
[user2] 訂單 003 付款失敗。

當需要通過日志記錄來排查某個用戶所遇到的問題時,只需要通過正則表達就可以很快地查詢到用戶相關的日志記錄。

日志聚合與分析

在程序中正確的地方輸出合適的日志消息,只是合理使用日志的第一步。日志記錄的真正作用在於當有問題發生時,能夠幫助開發人員很快的定位問題所在。不過一個實用的系統通常由很多個不同的部分組成。這其中包括所開發的程序本身,也包括所依賴的第三方應用程序。以一個典型的電子商務網站為例,除了程序本身,還包括所依賴的底層操作系統、應用服務器、數據庫、HTTP 服務器和代理服務器和緩存等。當一個問題發生時,真正的原因可能來自程序本身,也可能來自所依賴的第三方程序。這就意味著開發人員可能需要檢查不同服務器上不同應用程序的日志來確定真正的原因。

日志聚合的作用就在於可以把來自不同服務器上不同應用程序產生的日志聚合起來,存放在單一的服務器上,方便進行搜索和分析。在日志聚合方面,已經有不少成熟的開源軟件可以很好的滿足需求。本文中要介紹的是 logstash,一個流行的事件和日志管理開源軟件。logstash 采用了一種簡單的處理模式:輸入 -> 過濾器 -> 輸出。logstash 可以作為代理程序安裝到每台需要收集日志的機器上。logstash 提供了非常多的插件來處理不同類型的數據輸入。典型的包括控制台、文件和 syslog 等;對於輸入的數據,可以使用過濾器來進行處理。典型的處理方式是把日志消息轉換成結構化的字段;過濾之後的結果可以被輸出到不同的目的地,比如 ElasticSearch、文件、電子郵件和數據庫等。

Logstash 在使用起來很簡單。從官方網站下載 jar 包並運行即可。在運行時需要指定一個配置文件。配置文件中定義了輸入、過濾器和輸出的相關配置。清單 9 給出了一個簡單的 logstash 配置文件的示例。

查看本欄目

清單 9. logstash 配置文件示例

input { 
 file { 
   path => [ "/var/log/*.log", "/var/log/messages", "/var/log/syslog" ] 
   type => 'syslog'
 } 
} 
    
output { 
 stdout { 
debug => true
debug_format => "json"
 } 
}

清單 9 中定義了 logstash 收集日志時的輸入(input)和輸出(output)的相關配置。輸入類型是文件(file)。每種類型輸入都有相應的配置。對於文件來說,需要配置的是文件的路徑。對每種類型的輸入,都需要指定一個類型(type)。該類型用來區分來自不同輸入的記錄。代碼中使用的輸出是控制台。配置文件完成之後,通過“java -jar logstash-1.1.13-flatjar.jar agent -f logstash-simple.conf”就可以啟動 logstash。

在日志分析中,比較重要的是結構化的信息。而日志信息通常只是一段文本,其中的不同字段表示不同的含義。不同的應用程序產生的日志的格式並不相同。在分析時需要關注的是其中包含的不同字段。比如 Apache 服務器會產生與用戶訪問請求相關的日志。在日志中包含了訪問者的各種信息,包括 IP 地址、時間、HTTP 狀態碼、響應內容的長度和 User Agent 字符串等信息。在 logstash 收集到日志信息之後,可以根據一定的規則把日志信息中包含的數據提取出來並命名。logstash 提供了 grok 插件可以完成這樣的功能。grok 基於正則表達式來工作,同時提供了非常多的常用類型數據的提取模式,如清單 10 所示。

清單 10. 使用 grok 提取日志記錄中的內容

點擊查看代碼清單

在經過上面 grok 插件的提取之後,Apache 訪問日志被轉換成包含字段 client、method、request、status、bytes 和 useragent 的格式化數據。可以根據這些字段來進行搜索。這對於分析問題和進行統計都是很有幫助的。

當日志記錄通過 logstash 進行收集和處理之後,通常會把這些日志記錄保存到數據庫中進行分析和處理。目前比較流行的方式是保存到 ElasticSearch 中,從而可以利用 ElasticSearch 提供的索引和搜索能力來分析日志。已經有不少的開源軟件在 ElasticSearch 基礎之上開發出相應的日志管理功能,可以很方便的進行搜索和分析。本文中介紹的是 Graylog2。

Graylog2 由服務器和 Web 界面兩部分組成。服務器負責接收日志記錄並保存到 ElasticSearch 之中。Web 界面則可以查看和搜索日志,並提供其他的輔助功能。logstash 提供了插件 gelf,可以把 logstash 收集和處理過的日志記錄發送到 Graylog2 的服務器。這樣就可以利用 Graylog2 的 Web 界面來進行查詢和分析。只需要把清單 9 中的 logstash 的配置文件中的 output 部分改成清單 11 中所示即可。

清單 11. 配置 logstash 輸出到 Graylog2
output { 
 gelf { 
   host => '127.0.0.1'
 } 
}

在安裝 Graylog2 時需要注意,一定要安裝與 Graylog2 的版本相對應的版本的 ElasticSearch,否則會出現日志記錄無法保存到 ElasticSearch 的問題。本文中使用的是 Graylog2 服務器 0.11.0 版本和 ElasticSearch 0.20.4 版本。

除了 Graylog2 之外,另外一個開源軟件 Kibana 也比較流行。Kibana 可以看成是 logstash 和 ElasticSearch 的 Web 界面。Kibana 提供了更加豐富的功能來顯示和分析日志記錄。與代碼清單中的 logstash 的配置相似,只需要把輸出改為 elasticsearch 就可以了。Kibana 可以自動讀取 ElasticSearch 中包含的日志記錄並顯示。

小結

日志記錄是應用程序開發中的重要一環。不過這一環比較容易被開發人員忽視,因為它所產生的影響在程序運行和維護時。對於一個生產系統來說,日志記錄的重要性是不言而喻的。本文首先以 java.util.logging 包和 log4j 為例介紹了 Java 日志 API 的主要組成部分和使用方式,同時也介紹了 Apache Commons Logging 和 SLF4J 兩種日志封裝 API。本文也給出了一些記錄日志時應該采用的最佳實踐。最後介紹了如何使用開源工具對日志進行聚合和分析。通過本文,開發人員可以了解如何在開發中有效的使用日志。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved