對於如今的應用程序來講,日誌的重要性是不言而喻的。很難想象沒有任何日誌記錄功能的應用程序運行在生產環境中。日誌所能提供的功能是多種多樣的,包括記錄程序運行時產生的錯誤信息、狀態信息、調試信息和執行時間信息等。在生產環境中,日誌是查找問題來源的重要依據。應用程序運行時的產生的各類信息,都應該經過日誌 API 來進行記錄。不少開發人員習慣於使用 System.out.println、System.err.println 以及異常對象的 printStrackTrace 方法來輸出相關信息。這些使用方式雖然簡便,可是所產生的信息在出現問題時並不能提供有效的幫助。這些使用方式都應該改成使用日誌 API。使用日誌 API 並無增長不少複雜度,可是所提供的好處是顯著的。 html
儘管記錄日誌是應用開發中並不可少的功能,在 JDK 的最第一版本中並不包含日誌記錄相關的 API 和實現。相關的 API(java.util.logging 包,JUL)和實現,直到 JDK 1.4 才被加入。所以在日誌記錄這一個領域,社區貢獻了不少開源的實現。其中比較流行的包括 log4j 及其後繼者 logback。除了真正的日誌記錄實現以外,還有一類與日誌記錄相關的封裝 API,如 Apache Commons Logging 和 SLF4J。這類庫的做用是在日誌記錄實現的基礎上提供一個封裝的 API 層次,對日誌記錄 API 的使用者提供一個統一的接口,使得能夠自由切換不一樣的日誌記錄實現。好比從 JDK 的默認日誌記錄實現 JUL 切換到 log4j。這類封裝 API 庫在框架的實現中比較經常使用,由於須要考慮到框架使用者的不一樣需求。在實際的項目開發中則使用得比較少,由於不多有項目會在開發中切換不一樣的日誌記錄實現。本文對於這兩類庫都會進行具體的介紹。 java
記錄日誌只是有效地利用日誌的第一步,更重要的是如何對程序運行時產生的日誌進行處理和分析。典型的情景包括當日志中包含知足特定條件的記錄時,觸發相應的通知機制,好比郵件或短信通知;還能夠在程序運行出現錯誤時,快速地定位潛在的問題源。這樣的處理和分析的能力對於實際系統的維護尤爲重要。當運行系統中包含的組件過多時,日誌對於錯誤的診斷就顯得格外重要。 web
本文首先介紹關於日誌 API 的基本內容。 正則表達式
從功能上來講,日誌 API 自己所需求的功能很是簡單,只須要可以記錄一段文本便可。API 的使用者在須要進行記錄時,根據當前的上下文信息構造出相應的文本信息,調用 API 完成記錄。通常來講,日誌 API 由下面幾個部分組成: shell
當程序中須要記錄日誌時,首先須要獲取一個日誌記錄器對象。通常的日誌記錄 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 個級別所對應的狀況也有所不一樣: apache
在這 6 個級別中,以 ERROR、WARN、INFO 和 DEBUG 做爲經常使用。 json
日誌記錄 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 類的對象中包含了日誌記錄中的所有信息。
public class CustomFormatter extends Formatter {
public String format(LogRecord record) {
return String.format("<%s> [%s] : %s", new Date(record.getMillis()), record.getLoggerName(), record.getMessage());
}
}
java.util.logging.ConsoleHandler.formatter = logging.jul.CustomFormatter
Log4j 在格式化器的實現上要簡單一些,由 org.apache.log4j.PatternLayout 類來負責完成日誌記錄的格式化。在自定義時不須要建立新的 Java 類,而是經過配置文件指定所需的格式化模式。在格式化模式中,不一樣的佔位符表示不一樣類型的信息。好比「%c」表示記錄器的名稱,「%d」表示日期,「%m」表示日誌的消息文本,「%p」表示嚴重性級別,「%t」表示線程的名稱。清單 3 給出了 Log4j 配置文件中日誌記錄的自定義方式。
log4j.appender.stdout.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} [%p] %c - %m%n
日誌記錄通過格式化以後,由不一樣的處理器來進行處理。不一樣的處理器有各自不一樣的處理方式。好比控制檯處理器會把日誌輸出到控制檯中,文件處理器把日誌寫入到文件中。除了這些以外,還有寫入到數據庫、經過郵件發送、寫入到 JMS 隊列等各類不一樣的處理方式。
日誌處理器也能夠配置所處理日誌信息的最低嚴重性級別。低於該級別的日誌不會被處理。這樣能夠控制所處理的日誌記錄數量。好比控制檯處理器的級別通常設置爲 INFO,而文件處理器則通常設置爲 DEBUG。
日誌記錄框架通常提供了比較多的日誌處理器實現。開發人員也能夠建立自定義的實現。
除了 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 所示。
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(Mapped Diagnostic Context,映射調試上下文)是 log4j 和 logback 提供的一種方便在多線程條件下記錄日誌的功能。某些應用程序採用多線程的方式來處理多個用戶的請求。在一個用戶的使用過程當中,可能有多個不一樣的線程來進行處理。典型的例子是 Web 應用服務器。當用戶訪問某個頁面時,應用服務器可能會建立一個新的線程來處理該請求,也可能從線程池中複用已有的線程。在一個用戶的會話存續期間,可能有多個線程處理過該用戶的請求。這使得比較難以區分不一樣用戶所對應的日誌。當須要追蹤某個用戶在系統中的相關日誌記錄時,就會變得很麻煩。
一種解決的辦法是採用自定義的日誌格式,把用戶的信息採用某種方式編碼在日誌記錄中。這種方式的問題在於要求在每一個使用日誌記錄器的類中,均可以訪問到用戶相關的信息。這樣纔可能在記錄日誌時使用。這樣的條件一般是比較難以知足的。MDC 的做用是解決這個問題。
MDC 能夠當作是一個與當前線程綁定的哈希表,能夠往其中添加鍵值對。MDC 中包含的內容能夠被同一線程中執行的代碼所訪問。當前線程的子線程會繼承其父線程中的 MDC 的內容。當須要記錄日誌時,只須要從 MDC 中獲取所需的信息便可。MDC 的內容則由程序在適當的時候保存進去。對於一個 Web 應用來講,一般是在請求被處理的最開始保存這些數據。清單 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」的值。
log4j.appender.stdout.layout.ConversionPattern=%X{username} %d{yyyy-MM-dd HH:mm:ss} [%p] %c - %m%n
下面主要介紹一些在記錄日誌時的比較好的實踐。
當日志記錄器收到一個日誌記錄請求時,若是請求的嚴重性級別低於記錄器對象的實際有效級別,則該請求會被忽略。在日誌記錄方法的實現中會首先進行這樣的檢查。不過推薦的作法是在調用 API 進行記錄以前,首先進行相應的檢查,這樣能夠避免沒必要要的性能問題,如清單 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 所示。
[user1] 用戶登陸成功。 [user1] 用戶成功購買產品 A。 [user2] 訂單 003 付款失敗。
當須要經過日誌記錄來排查某個用戶所遇到的問題時,只須要經過正則表達就能夠很快地查詢到用戶相關的日誌記錄。
在程序中正確的地方輸出合適的日誌消息,只是合理使用日誌的第一步。日誌記錄的真正做用在於當有問題發生時,可以幫助開發人員很快的定位問題所在。不過一個實用的系統一般由不少個不一樣的部分組成。這其中包括所開發的程序自己,也包括所依賴的第三方應用程序。以一個典型的電子商務網站爲例,除了程序自己,還包括所依賴的底層操做系統、應用服務器、數據庫、HTTP 服務器和代理服務器和緩存等。當一個問題發生時,真正的緣由可能來自程序自己,也可能來自所依賴的第三方程序。這就意味着開發人員可能須要檢查不一樣服務器上不一樣應用程序的日誌來肯定真正的緣由。
日誌聚合的做用就在於能夠把來自不一樣服務器上不一樣應用程序產生的日誌聚合起來,存放在單一的服務器上,方便進行搜索和分析。在日誌聚合方面,已經有很多成熟的開源軟件能夠很好的知足需求。本文中要介紹的是 logstash,一個流行的事件和日誌管理開源軟件。logstash 採用了一種簡單的處理模式:輸入 -> 過濾器 -> 輸出。logstash 能夠做爲代理程序安裝到每臺須要收集日誌的機器上。logstash 提供了很是多的插件來處理不一樣類型的數據輸入。典型的包括控制檯、文件和 syslog 等;對於輸入的數據,可使用過濾器來進行處理。典型的處理方式是把日誌消息轉換成結構化的字段;過濾以後的結果能夠被輸出到不一樣的目的地,好比 ElasticSearch、文件、電子郵件和數據庫等。
Logstash 在使用起來很簡單。從官方網站下載 jar 包並運行便可。在運行時須要指定一個配置文件。配置文件中定義了輸入、過濾器和輸出的相關配置。清單 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 所示。
//Apache 訪問日誌 49.50.214.136 GET /index.html 200 1150 "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.17 (KHTML, like Gecko) Chrome/24.0.1312.57 Safari/537.17" //grok 提取模式 %{IP:client} %{WORD:method} %{URIPATHPARAM:request} %{NUMBER:status} %{NUMBER:bytes} %{QS:useragent}
//Apache 訪問日誌 49.50.214.136 GET /index.html 200 1150 "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.17 (KHTML, like Gecko) Chrome/24.0.1312.57 Safari/537.17" //grok 提取模式 %{IP:client} %{WORD:method} %{URIPATHPARAM:request} %{NUMBER:status} %{NUMBER:bytes} %{QS:useragent}
在通過上面 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 中所示便可。
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。本文也給出了一些記錄日誌時應該採用的最佳實踐。最後介紹瞭如何使用開源工具對日誌進行聚合和分析。經過本文,開發人員能夠了解如何在開發中有效 的使用日誌。