Java日誌終極指南

ImportNew發表於2015-07-20

Java日誌基礎

Java使用了一種自定義的、可擴充套件的方法來輸出日誌。雖然Java通過java.util.logging包提供了一套基本的日誌處理API,但你可以很輕鬆的使用一種或者多種其它日誌解決方案。這些解決方案儘管使用不同的方法來建立日誌資料,但它們的最終目標是一樣的,即將日誌從你的應用程式輸出到目標地址。

在這一節中,我們會探索Java日誌背後的原理,並說明如何通過日誌來讓你成為一個更好的Java開發人員。

Java日誌元件

Java日誌API由以下三個核心元件組成:

  • Loggers:Logger負責捕捉事件並將其傳送給合適的Appender。
  • Appenders:也被稱為Handlers,負責將日誌事件記錄到目標位置。在將日誌事件輸出之前,Appenders使用Layouts來對事件進行格式化處理。
  • Layouts:也被稱為Formatters,它負責對日誌事件中的資料進行轉換和格式化。Layouts決定了資料在一條日誌記錄中的最終形式。

當Logger記錄一個事件時,它將事件轉發給適當的Appender。然後Appender使用Layout來對日誌記錄進行格式化,並將其傳送給控制檯、檔案或者其它目標位置。另外,Filters可以讓你進一步指定一個Appender是否可以應用在一條特定的日誌記錄上。在日誌配置中,Filters並不是必需的,但可以讓你更靈活控制日誌訊息的流動。

Java日誌終極指南

日誌框架

在Java中,輸出日誌需要使用一個或者多個日誌框架,這些框架提供了必要的物件、方法和配置來傳輸訊息。Java在java.util.logging包中提供了一個預設的框架除此之外,還有很多其它第三方框架,包括Log4jLogback以及tinylog。還有其它一些開發包,例如SLF4JApache Commons Logging,它們提供了一些抽象層,對你的程式碼和日誌框架進行解耦,從而允許你在不同的日誌框架中進行切換。

如何選擇一個日誌解決方案,這取決於你的日誌需求的複雜度、和其它日誌解決方案的相容性、易用性以及個人喜好。Logback基於log4j之前的版本開發(版本1),因此它們的功能集合都非常類似。然而,Log4j在最新版本(版本2)中引用了一些改進,例如支援多API,並提升了在用Disruptor庫的效能。而tinylog,由於缺少了一些功能,執行特別快,非常適合小專案。

另外一個考慮因素是框架在基於Java的各種不同專案上的支援程度。例如Android程式只能使用Log4jLogback或者第三方包來記錄日誌, Apache Tomcat可以使用Log4j來記錄內部訊息,但只能使用版本1的Log4j。

抽象層

諸如SLF4J這樣的抽象層,會將你的應用程式從日誌框架中解耦。應用程式可以在執行時選擇繫結到一個特定的日誌框架(例如java.util.logging、Log4j或者Logback),這通過在應用程式的類路徑中新增對應的日誌框架來實現。如果在類路徑中配置的日誌框架不可用,抽象層就會立刻取消呼叫日誌的相應邏輯。抽象層可以讓我們更加容易地改變專案現有的日誌框架,或者整合那些使用了不同日誌框架的專案。

配置

儘管所有的Java日誌框架都可以通過程式碼進行配置,但是大部分配置還是通過外部配置檔案完成的。這些檔案決定了日誌訊息在何時通過什麼方式進行處理,日誌框架可以在執行時載入這些檔案。在這一節中提供的大部分配置示例都使用了配置檔案。

java.util.logging

預設的Java日誌框架將其配置儲存到一個名為 logging.properties 的檔案中。在這個檔案中,每行是一個配置項,配置項使用點標記(dot notation)的形式。Java在其安裝目錄的lib資料夾下面安裝了一個全域性配置檔案,但在啟動一個Java程式時,你可以通過指定 java.util.logging.config.file 屬性的方式來使用一個單獨的日誌配置檔案,同樣也可以在個人專案中建立和儲存 logging.properties 檔案。

下面的示例描述瞭如何在全域性的logging.properties檔案中定義一個Appender:

# default file output is in user's home directory.
java.util.logging.FileHandler.pattern = %h/java%u.log
java.util.logging.FileHandler.limit = 50000
java.util.logging.FileHandler.count = 1
java.util.logging.FileHandler.formatter = java.util.logging.XmlFormatter

Log4j

Log4j版本1使用的語法和 java.util.logging 的語法很類似。使用了Log4j的程式會在專案目錄中尋找一個名為 log4j.properties 的檔案。預設情況下,Log4j配置會將所有日誌訊息輸出到控制檯上。Log4j同樣也支援XML格式的配置檔案,對應的配置資訊會儲存到 log4j.xml 檔案中。

Log4j版本2支援XML、JSON和YAML格式的配置,這些配置會分別儲存到 log4j2.xml、log4j2.json 和 log4j2.yaml 檔案中。和版本1類似,版本2也會在工程目錄中尋找這些檔案。你可以在每個版本的文件中找到相應的配置檔案示例。

Logback

對於Logback來說,大部分配置都是在 logback.xml 檔案中完成的,這個檔案使用了和Log4j類似的XML語法。Logback同時也支援通過Groovy語言的方式來進行配置,配置資訊會儲存到 logback.groovy 檔案中。你可以通過每種型別配置檔案的連結找到對應的配置檔案示例。

Loggers

Loggers是用來觸發日誌事件的物件,在我們的Java應用程式中被建立和呼叫,然後Loggers才會將事件傳遞給Appender。一個類中可以包含針對不同事件的多個獨立的Loggers,你也可以在一個Loggers裡面內嵌一個Loggers,從而建立一種Loggers層次結構

建立新Logger

在不同的日誌框架下面建立新Logger過程大同小異,儘管呼叫的具體方法名稱可能不同。在使用 java.util.logging 時,你可以通過 Logger.getLogger().getLogger() 方法建立新Logger,這個方法接收一個string引數,用於指定Logger的名字。如果指定名字的Logger已經存在,那麼只需要返回已經存在的Logger;否則,程式會建立一個新Logger。通常情況下,一種好的做法是我們在當前類下使用 class.getName() 作為新Logger的名字。

Logger logger = Logger.getLogger(MyClass.class.getName());

記錄日誌事件

Logger提供了幾種方法來觸發日誌事件。然而在你記錄一個事件之前,你還需要設定級別。日誌級別用來確定日誌的嚴重程度,它可以用來過濾日誌事件或者將其傳送給不同的Appender(想了解更多資訊,請參考“日誌級別”一節),Logger.log() 方法除了日誌訊息以外,還需要一個日誌級別作為引數:

logger.log(Level.WARNING, “This is a warning!”);

大部分日誌框架都針對輸出特定級別日誌提供了快捷方式。例如,下面語句的作用和上面語句的作用是一樣的:

logger.warning(“This is a warning!”);

你還可以阻止Logger輸出低於指定日誌級別的訊息下面的示例中,Logger只能輸出高於WARNING級別的日誌訊息,並丟棄日誌級別低於WARNING的訊息:

logger.setLevel(Level.WARNING);

我們還有另外一些方法可以用來記錄額外的資訊。logp()(精確日誌)可以讓你指定每條日誌記錄的源類(source class)和方法,而 logrb()(使用資源繫結的日誌)可以讓你指定用於提取日誌訊息的資源。entering() 和 exiting() 方法可以讓你記錄方法呼叫資訊,從而追蹤程式的執行過程。

Appenders

Appenders將日誌訊息轉發給期望的輸出它負責接收日誌事件,使用Layout格式化事件,然後將其傳送給對應的目標。對於一個日誌事件,我們可以使用多個Appenders來將事件傳送到不同的目標位置。例如,我們可以在控制檯上顯示一個簡單的日誌事件的同時,將其通過郵件的方式傳送給指定的接收者。

請注意,在java.util.logging中,Appenders被稱作Handlers。

增加Appender

大部分日誌框架的Appender都會執行類似的功能,但在實現方面大相徑庭。如果使用 java.util.logging,你可以使用 Logger.addHandler() 方法將Appender新增到Logger中。例如,下面的程式碼新增了一個新的ConsoleHandler,它會將日誌輸出到控制檯:

logger.addHandler(new ConsoleHandler());

一種更常用的新增Appender的方式是使用配置檔案如果使用 java.util.logging,Appenders會定義一個以逗號隔開的列表,下面的示例將日誌事件輸出到控制檯和檔案:

handlers=java.util.logging.ConsoleHandler, java.util.logging.FileHandler

如果使用基於XML的配置檔案,Appenders會被新增到<Appenders>元素下面,如果使用Log4j,我們可以很容易地新增一個新ConsoleAppender來將日誌訊息傳送到System.out:

<Console name="console" target="SYSTEM_OUT">
  <PatternLayout pattern="[%p] %t: %m%n" />
</Console>

Appenders型別

這一節描述了一些更通用的Appenders,以及它們在各種日誌框架中是如何實現的。

ConsoleAppender

ConsoleAppender是最常用的Appenders之一,它只是將日誌訊息顯示到控制檯上。許多日誌框架都將其作為預設的Appender,並且在基本的配置中進行預配置。例如,在Log4j中ConsoleAppender的配置引數如下所示。

引數 描述
filter 用於決定是否需要使用該Appender來處理日誌事件
layout 用於決定如何對日誌記錄進行格式化,預設情況下使用“%m%n”,它會在每一行顯示一條日誌記錄
follow 用於決定Appender是否需要了解輸出(system.out或者system.err)的變化,預設情況是不需要跟蹤這種變化
name 用於設定Appender的名字
ignoreExceptions 用於決定是否需要記錄在日誌事件處理過程中出現的異常
target 用於指定輸出目標位置,預設情況下使用SYSTEM_OUT,但也可以修改成SYSTEM_ERR

一個完整的Log4j2的配置檔案如下所示:

<?xml version="1.0" encoding="UTF-8"?>
 <Configuration status="warn" name="MyApp">
   <Appenders>
     <Console name="MyAppender" target="SYSTEM_OUT">
       <PatternLayout pattern="%m%n"/>
     </Console>
   </Appenders>
   <Loggers>
     <Root level="error">
       <AppenderRef ref="MyAppender"/>
     </Root>
   </Loggers>
 </Configuration>

這個配置檔案建立了一個名為MyAppender的ConsoleAppender,它使用PatternLayout來對日誌事件進行格式化,然後再將其輸出到System.out。<Loggers>元素對定義在程式程式碼中的Loggers進行了配置。在這裡,我們只配置了一個LoggerConfig,即名為Root的Logger,它會接收哪些日誌級別在ERROR以上的日誌訊息。如果我們使用logger.error()來記錄一個訊息,那麼它就會出現在控制檯上,就像這樣:

An unexpected error occurred.

你也可以使用Logback實現完全一樣的效果:

<configuration>
  <appender name="MyAppender" class="ch.qos.Logback.core.ConsoleAppender">
    <encoder>
      <pattern>%m%n</pattern>
    </encoder>
  </appender>
  <root level="error">
    <appender-ref ref="MyAppender" />
  </root>
</configuration>

FileAppenders

FileAppenders將日誌記錄寫入到檔案中,它負責開啟、關閉檔案,向檔案中追加日誌記錄,並對檔案進行加鎖,以免資料被破壞或者覆蓋

在Log4j中,如果想建立一個FileAppender,需要指定目標檔案的名字,寫入方式是追加還是覆蓋,以及是否需要在寫入日誌時對檔案進行加鎖:

...
<Appenders>
  <File name="MyFileAppender" fileName="myLog.log" append="true" locking="true">
    <PatternLayout pattern="%m%n"/>
  </File>
</Appenders>
...

這樣我們建立了一個名為MyFileAppender的FileAppender,並且在向檔案中追加日誌時會對檔案進行加鎖操作。

如果使用Logback,你可以同時啟用prudent模式來保證檔案的完整性。雖然Prudent模式增加了寫入檔案所花費的時間,但它可以保證在多個FileAppender甚至多個Java程式向同一個檔案寫入日誌時,檔案的完整性。

...
<appender name="FileAppender" class="ch.qos.Logback.core.FileAppender">
  <file>myLog.log</file>
  <append>true</append>
  <prudent>true</prudent>
  <encoder>
    <pattern>%m%n</pattern>
  </encoder>
</appender>
...

SyslogAppender

SyslogAppenders將日誌記錄傳送給本地或者遠端系統的日誌服務。syslog是一個接收日誌事件服務,這些日誌事件來自作業系統、程式、其它服務或者其它裝置。事件的範圍可以從診斷資訊到使用者登入硬體失敗等。syslog的事件按照裝置進行分類,它指定了正在記錄的事件的型別。例如,auth facility表明這個事件是和安全以及認證有關。

Log4j和Logback都內建支援SyslogAppenders在Log4j中,我們建立SyslogAppender時,需要指定syslog服務監聽的主機號、埠號以及協議。下面的示例演示瞭如何設定裝置

...
<Appenders>
  <Syslog name="SyslogAppender" host="localhost" port="514" protocol="UDP" facility="Auth" />
</Appenders>
...

在Logback中,我們可以實現同樣的效果:

...
<appender name="SyslogAppender" class="ch.qos.Logback.classic.net.SyslogAppender">
  <syslogHost>localhost</syslogHost>
  <port>514</port>
  <facility>Auth</facility>
</appender>
...

其它Appender

我們已經介紹了一些經常用到的Appenders,還有很多其它Appender它們新增了新功能或者在其它的一些Appender基礎上實現了新功能。例如,Log4j中的RollingFileAppender擴充套件了FileAppender,它可以在滿足特定條件時自動建立新的日誌檔案;SMTPAppender會將日誌內容以郵件的形式傳送出去;FailoverAppender會在處理日誌的過程中,如果一個或者多個Appender失敗,自動切換到其他Appender上。

如果想了解更多關於其他Appender的資訊,可以檢視Log4j Appender參考以及Logback Appender參考

Layouts

Layouts將日誌記錄的內容從一種資料形式轉換成另外一種。日誌框架為純文字、HTML、syslog、XML、JSON、序列化以及其它日誌提供了Layouts。

請注意在java.util.logging中Layouts也被稱為Formatters。

例如,java.util.logging提供了兩種Layouts:SimpleFormatter和XMLFormatter。預設情況下,ConsoleHandlers使用SimpleFormatter,它輸出的純文字日誌記錄就像這樣:

Mar 31, 2015 10:47:51 AM MyClass main
SEVERE: An exception occurred.

而預設情況下,FileHandlers使用XMLFormatter,它的輸出就像這樣:

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE log SYSTEM "logger.dtd">
<log>
<record>
  <date>2015-03-31T10:47:51</date>
  <millis>1427903275893</millis>
  <sequence>0</sequence>
  <logger>MyClass</logger>
  <level>SEVERE</level>
  <class>MyClass</class>
  <method>main</method>
  <thread>1</thread>
  <message>An exception occurred.</message>
</record>
</log>

配置Layout

我們通常使用配置檔案對Layouts進行配置從Java 7開始,我們也可以使用system property來配置SimpleFormatter。

例如,在Log4j和Logback中最常用的Layouts是PatternLayout。它可以讓你決定日誌事件中的哪些部分需要輸出,這是通過轉換模式(Conversion Pattern)完成的,轉換模式在每一條日誌事件的資料中扮演了“佔位符”的角色。例如,Log4j預設的PatternLayout使用瞭如下轉換模式:

<PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/>

%d{HH:mm:ss.SSS} 將日期轉換成時、分、秒和毫秒的形式,%level顯示日誌事件的嚴重程度,%C顯示生成日誌事件的類的名字,%t顯示Logger的當前執行緒,%m顯示時間的訊息,最後,%n為下一個日誌事件進行了換行。

改變Layouts

如果在java.util.logging中使用一個不同的Layout,需要將Appender的formatter屬性設定成你想要的Layout在程式碼中,你可以建立一個新的Handler,呼叫setFormatter方法,然後通過logger.AddHandler()方法將Handler放到Logger上面。下面的示例建立了一個ConsoleAppender,它使用XMLFormatter來對日誌進行格式化,而不是使用預設的SimpleFormatter:

Handler ch = new ConsoleHandler();
ch.setFormatter(new XMLFormatter());
logger.addHandler(ch);

這樣Logger會將下面的資訊輸出到控制檯上:

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE log SYSTEM "logger.dtd">
<log>
<record>
  <date>2015-03-31T10:47:51</date>
  <millis>1427813271000</millis>
  <sequence>0</sequence>
  <logger>MyClass</logger>
  <level>SEVERE</level>
  <class>MyClass</class>
  <method>main</method>
  <thread>1</thread>
  <message>An exception occurred.</message>
</record>

如果想了解更多資訊,你可以檢視Log4j Layouts參考以及Logback Layouts參考

使用自定義Layouts

自定義Layouts可以讓你指定Appender應該如何輸出日誌記錄。從Java SE 7開始,儘管你可以調整SimpleLogger的輸出,但有一個限制,即只能夠調整簡單的純文字訊息。對於更高階的格式,例如HTML或者JSON,你需要一個自定義Layout或者一個單獨的框架。

如果想了解更多使用java.util.logging建立自定義Layouts的資訊,你可以檢視Jakob Jenkov的Java日誌指南中的Java Logging: Formatters章節

日誌級別

日誌級別提供了一種方式,我們可以用它來根據嚴重程度對日誌進行分類和識別。java.util.logging 按照嚴重程度從重到輕,提供了以下級別:

  • SEVERE(最高階別)
  • WARNING
  • INFO
  • CONFIG
  • FINE
  • FINER
  • FINEST(最低階別)

另外, 還有兩個日誌級別:ALL和OFF。ALL會讓Logger輸出所有訊息,而OFF則會關閉日誌功能。

設定日誌級別

在設定日誌級別後,Logger會自動忽略那些低於設定級別的日誌訊息。例如,下面的語句會讓Logger忽略那些低於WARNING級別的日誌訊息:

logger.setLevel(Level.WARNING);

然後,Logger會記錄任何WARNING或者更高階別的日誌訊息。我們也可以在配置檔案中設定Logger的日誌級別:

...
<Loggers>
  <Logger name="MyLogger" level="warning">
  ...

轉換模式

Log4j和Logback中的PatternLayout類都支援轉換模式,它決定了我們如何從每一條日誌事件中提取資訊以及如何對資訊進行格式化。下面顯示了這些模式的一個子集,對於Log4j和Logback來說,雖然這些特定的欄位都是一樣的,但是並不是所有的欄位都會使用相同的模式。想要了解更多資訊,可以檢視Log4jLogback的PatternLayout文件。

欄位名稱 Log4j/Logback 模式
訊息 %m
級別/嚴重程度 %p
異常 %ex
執行緒 %t
Logger %c
方法 %M

例如,下面的PatternLayout會在中括號內x顯示日誌級別,後面是執行緒名字和日誌事件的訊息:

[%p] %t: %m

下面是使用了上述轉換模式後的日誌輸出示例:

[INFO] main: initializing worker threads
[DEBUG] worker: listening on port 12222[INFO] worker: received request from 192.168.1.200[ERROR] worker: unknown request ID from 192.168.1.200

記錄棧跟蹤資訊

如果你在Java程式中使用過異常,那麼很有可能已經看到過棧跟蹤資訊它提供了一個程式中方法呼叫的快照,讓你準確定位程式執行的位置。例如,下面的棧跟蹤資訊是程式試圖開啟一個不存在的檔案後生成的:

[ERROR] main: Unable to open file! java.io.FileNotFoundException: foo.file (No such file or directory)
  at java.io.FileInputStream.open(Native Method) ~[?:1.7.0_79]
  at java.io.FileInputStream.<init>(FileInputStream.java:146) ~[?:1.7.0_79]
  at java.io.FileInputStream.<init>(FileInputStream.java:101) ~[?:1.7.0_79]
  at java.io.FileReader.<init>(FileReader.java:58) ~[?:1.7.0_79]
  at FooClass.main(FooClass.java:47)

這個示例使用了一個名為FooClass的類,它包含一個main方法在程式第47行,FileReader獨享試圖開啟一個名為foo.file的檔案,由於在程式目錄下沒有名字是foo.file的檔案,因此Java虛擬機器丟擲了一個FileNotFoundException。因為這個方法呼叫被放到了try-catch語塊中,所以我們能夠捕獲這個異常並記錄它,或者至少可以阻止程式崩潰。

使用PatternLayout記錄棧跟蹤資訊

在寫本篇文章時最新版本的Log4j和Logback中,如果在Layout中沒有和可拋異常相關的資訊,那麼都會自動將%xEx(這種棧跟蹤資訊包含了每次方法呼叫的包資訊)新增到PatternLayout中。如果對於普通的日誌資訊的模式如下:

[%p] %t: %m

它會變為:

[%p] %t: %m%xEx

這樣不僅僅錯誤資訊會被記錄下來,完整的棧跟蹤資訊也會被記錄:

[ERROR] main: Unable to open file! java.io.FileNotFoundException: foo.file (No such file or directory)
  at java.io.FileInputStream.open(Native Method) ~[?:1.7.0_79]
  at java.io.FileInputStream.<init>(FileInputStream.java:146) ~[?:1.7.0_79]
  at java.io.FileInputStream.<init>(FileInputStream.java:101) ~[?:1.7.0_79]
  at java.io.FileReader.<init>(FileReader.java:58) ~[?:1.7.0_79]
  at FooClass.main(FooClass.java:47)

%xEx中的包查詢是一個代價昂貴的操作,如果你頻繁的記錄異常資訊,那麼可能會碰到效能問題,例如:

  // ...
  } catch (FileNotFoundException ex) {
    logger.error(“Unable to open file!”, ex);
}

一種解決方法是在模式中顯式的包含%ex,這樣就只會請求異常的棧跟蹤資訊:

[%p] %t: %m%ex

另外一種方法是通過追加%xEx(none)的方法排除(在Log4j)中所有的異常資訊:

[%p] %t: %m%xEx{none}

或者在Logback中使用%nopex:

[%p] %t: %m%nopex

使用結構化佈局輸出棧跟蹤資訊

如你在“解析多行棧跟蹤資訊”一節中所見,對於站跟蹤資訊來說,使用結構化佈局來記錄是最合適的方式,例如JSON和XML。 這些佈局會自動將棧跟蹤資訊按照核心元件進行分解,這樣我們可以很容易將其匯出到其他程式或者日誌服務中。對於上述站跟蹤資訊,如果使用JSON格式,部分資訊顯示如下:

...
"loggerName" : "FooClass",
  "message" : "Foo, oh no! ",
  "thrown" : {
    "commonElementCount" : 0,
    "localizedMessage" : "foo.file (No such file or directory)",
    "message" : "foo.file (No such file or directory)",
    "name" : "java.io.FileNotFoundException",
    "extendedStackTrace" : [ {
    "class" : "java.io.FileInputStream",
    "method" : "open",
    "file" : "FileInputStream.java",
    ...

記錄未捕獲異常

通常情況下,我們通過捕獲的方式來處理異常如果一個異常沒有被捕獲,那麼它可能會導致程式終止。如果能夠留存任何日誌,那麼這是一個可以幫助我們除錯為什麼會發生異常的好辦法,這樣你就可以找到發生異常的根本原因並解決它。下面來說明我們如何建立一個預設的異常處理器來記錄這些錯誤。

Thread類中有兩個方法,我們可以用它來為未捕獲的異常指定一個ExceptionHandler:

setDefaultUncaughtExceptionHandler 可以讓你在任何執行緒上處理任何異常setUncaughtExceptionHandler可以讓你針對一個指定的執行緒設定一個不同的處理方法。而ThreadGroup則允許你設定一個處理方法。大部分人會使用預設的異常處理方法。

下面是一個示例,它設定了一個預設的異常處理方法,來建立一個日誌事件。它要求你傳入一個UncaughtExceptionHandler:

import java.util.logging.*;
public class ExceptionDemo {
  private static final Logger logger = Logger.getLogger(ExceptionDemo.class);
  public static void main(String[] args) {
    Thread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
      public void uncaughtException(Thread t, Throwable e) {
        logger.log(Level.SEVERE, t + " ExceptionDemo threw an exception: ", e);
      };
  });
  class adminThread implements Runnable {
    public void run() {
      throw new RuntimeException();
    }
  }
  Thread t = new Thread(new adminThread());
  t.start();
  }
}

下面是一個未處理異常的輸出示例:

May 29, 2015 2:21:15 PM ExceptionDemo$1 uncaughtException
SEVERE: Thread[Thread-1,5,main] ExceptionDemo threw an exception:
java.lang.RuntimeException
  at ExceptionDemo$1adminThread.run(ExceptionDemo.java:15)
  at java.lang.Thread.run(Thread.java:745)

JSON

JSON(JavaScript Object Notation)是一種用來儲存結構化資料的格式,它將資料儲存成鍵值對的集合,類似於HashMap或者Hashtable。JSON具有的可移植性和通用性,大部分現代語言都內建支援它或者通過已經準備好的第三方類庫來支援它。

JSON支援許多基本資料型別,包括字串、數字、布林、陣列和null。例如,你可以使用下面的JSON格式來表示一個電腦:

{
  "manufacturer": "Dell",
  "model": "Inspiron",
  "hardware": {
    "cpu": "Intel Core i7",
    "ram": 16384,
    “cdrom”: null
  },
  "peripherals": [
    {
      "type": "monitor",
      "manufacturer": "Acer",
      "model": "S231HL"
    }
  ]
}

JSON的可移植性使得它非常適合儲存日誌記錄,使用JSON後,Java日誌可以被任何數目的JSON直譯器所讀取。因為資料已經是結構化的,所以解析JSON日誌要遠比解析純文字日誌容易。

Java中的JSON

對於Java來說,有大量的JSON實現,其中一個是JSON.simpleJSON.simple是輕量級的、易於使用,並且全部符合JSON標準。

如果想將上面的computer物件轉換成可用的Java物件,我們可以從檔案中讀取JSON內容,將其傳遞給JSON.simple,然後返回一個Object,接著我們可以將Object轉換成JSONObject:

Object computer = JSONValue.parse(new FileReader("computer.json"));
JSONObject computerJSON = (JSONObject)computer;

另外,為了取得鍵值對的資訊,你可以使用任何日誌框架來記錄一個JSONObject,JSONObject物件包含一個toString()方法, 它可以將JSON轉換成文字:

2015-05-06 14:54:32,878 INFO  JSONTest main {"peripherals":[{"model":"S231HL","manufacturer":"Acer","type":"monitor"}],"model":"Inspiron","hardware":{"cdrom":null,"ram":16384,"cpu":"Intel Core i7"},"manufacturer":"Dell"}

雖然這樣做可以很容易的列印JSONObject,但如果你使用結構化的Layouts,例如JSONLayout或者XMLLayout,可能會導致意想不到的結果:

...
"message" : "{"peripherals":[{"model":"S231HL","manufacturer":"Acer","type":"monitor"}],"model":"Inspiron","hardware":{"cdrom":null,"ram":16384,"cpu":"Intel Core i7"},"manufacturer":"Dell"}",
...

Log4j中的JSONLayout並沒有內建支援內嵌JSON物件,但你可以通過建立自定義Layout的方式來新增一個JSONObject欄位,這個Layout會繼承或者替換JSONLayout然而,如果你使用一個日誌管理系統,需要記住許多日誌管理系統會針對某些欄位使用預定義的資料型別。如果你建立一個Layout並將JSONObject儲存到message欄位中,那麼它可能會和日誌系統中使用的String資料型別相沖突。一種解決辦法是將JSON資料儲存到一個欄位中,然後將字串型別的日誌訊息儲存到另外一個欄位中。

其它JSON庫

除了JSON.simple,Java中還有很多其它JSON庫。JSON-java是由JSON建立者開發的一個參考實現,它包含了額外的一些功能,可以轉換其它資料型別,包括web元素。但是目前JSON-java已經沒有人來維護和提供支援了。

如果想將JSON物件轉換成Java物件或者逆向轉換,Google提供了一個Gson庫使用Gson時,可以很簡單使用 toJson() 和 fromJson() 方法來解析JSON,這兩個方法分別用來將Java物件轉換成JSON字串以及將JSON字串轉換成Java物件。Gson甚至可以應用在記憶體物件中,允許你對映到那些沒有原始碼的物件上。

Jackson

Jackson是一個強大的、流行的、功能豐富的庫,它可以在Java中管理JSON物件。有一些框架甚至使用Jackson作為它們的JSONLayouts。儘管它很大並且複雜,但Jackson對於初學者和高階使用者來說,是很容易使用的。

Logback通過logback-jackson和logback-json-classic庫繼承了Jackson,這兩個庫也是logback-contrib專案的一部分。在整合了Jackson後,你可以將日誌以JSON的格式匯出到任何Appender中。

Logback Wiki詳細解釋瞭如何將JSON新增到logback中,在Wiki頁面中的示例使用了LogglyAppender,這裡的配置也可以應用到其他Appender上。下面的示例說明了如何將JSON格式化的日誌記錄寫入到名為myLog.json的檔案中:

...
<appender name="file" class="ch.qos.Logback.core.FileAppender">
  <file>myLog.json</file>
  <encoder class="ch.qos.Logback.core.encoder.LayoutWrappingEncoder">
  <layout class="ch.qos.Logback.contrib.json.classic.JsonLayout">
    <jsonFormatter class="ch.qos.Logback.contrib.jackson.JacksonJsonFormatter"/>
    </layout>
  </encoder>
</appender>
 ...

你也可以通過FasterXML Wiki找到更多關於Jackson的深度介紹。

瞭解更多JSON相關資訊

你可以通過JSON主頁學習更多JSON相關資訊,或者通過CodeAcademy來通過學習一個互動式的快速上手教程(請注意這個課程是基於JavaScript的,而不是Java)有一些線上工具例如JSONLintJSON線上編輯器可以幫助你解析、驗證以及格式化JSON程式碼。

NDCMDC以及ThreadContext

當處理多執行緒應用程式,特別是web服務時,跟蹤事件可能會變得困難。當針對多個同時存在的多個使用者生成日誌記錄時,你如何區分哪個行為和哪個日誌事件有關呢?如何兩個使用者沒有成功開啟一個相同的檔案,或者在同一時間沒有成功登陸,那麼怎麼處理日誌記錄?你可能需要一種方式來將日誌記錄和程式中的唯一標示符關聯起來,這些識別符號可能是使用者ID,會話ID或者裝置ID。而這就是NDC、MDC以及ThreadContext的用武之地。

NDC、MDC和ThreadContext通過向單獨的日誌記錄中新增獨一無二的資料戳,來建立日誌足跡(log trails)。這些資料戳也被稱為魚標記(fish tagging),我們可以通過一個或者多個獨一無二的值來區分日誌。這些資料戳在每個執行緒級別上進行管理,並且一直持續到執行緒結束,或者直到資料戳被刪掉。例如,如果你的Web應用程式為每個使用者生成一個新的執行緒,那麼你可以使用這個使用者的ID來標記日誌記錄。當你想在一個複雜的系統中跟蹤特定的請求、事務或者使用者,這是一種非常有用的方法。

巢狀診斷上下文(NDC)

NDC或者巢狀診斷上下文(Nested Diagnostic Context)是基於棧的思想,資訊可以被放到棧上或者從棧中移除。而棧中的值可以被Logger訪問,並且Logger無需顯示想日誌方法中傳入任何值。

下面的程式碼示例使用NDC和Log4j來將使用者姓名和一條日誌記錄關聯起來。NDC是一個靜態類,因此我們可以直接訪問它的方法,而無需例項化一個NDC物件。在這個示例中, NDC.oush(username) 和 NDC.push(sessionID) 方法在棧中儲存了當前的使用者名稱(admin)和會話ID(1234),而NDC.pop()方法將一些項從棧中移除,NDC.remove()方法讓Java回收記憶體,以免造成記憶體溢位。

Java日誌終極指南

import java.io.FileReader;
import org.apache.Log4j.Logger;
import org.apache.Log4j.NDC;
...
String username = "admin";
String sessionID = "1234";
NDC.push(username);
NDC.push(sessionID);
try {
  // tmpFile doesn't exist, causing an exception.
  FileReader fr = new FileReader("tmpFile");
}
catch (Exception ex) {
  logger.error("Unable to open file.");
}
finally {
  NDC.pop();
  NDC.pop();
  NDC.remove();
}

Log4j的PatternLayout類通過%x轉換字元從NDC中提取如果一個日誌事件被觸發,那麼完整的NDC棧就被傳到Log4j:

<PatternLayout pattern="%x %-5p - %m%n" />

執行程式後,我們可以得出下面的輸出:

"admin 1234 ERROR – Unable to open file."

對映診斷上下文(MDC)

MDC或者對映診斷上下文和NDC很相似,不同之處在於MDC將值儲存在鍵值對中,而不是棧中。這樣你可以很容易的在Layout中引用一個單獨的鍵。MDC.put(key,value) 方法將一個新的鍵值對新增到上下文中,而 MDC.remove(key) 方法會移除指定的鍵值對。

如果想在日誌中同樣顯示使用者名稱和會話ID,我們需要使用 MDC.put() 方法將這兩個變數儲存成鍵值對:

import java.io.FileReader;
import org.apache.Log4j.Logger;
import org.apache.Log4j.MDC;
...
MDC.put("username", "admin");
MDC.put("sessionID", "1234");
try {
  // tmpFile doesn't exist, causing an exception.
  FileReader fr = new FileReader("tmpFile");
}
catch (Exception ex) {
  logger.error("Unable to open file!");
}
finally {
  MDC.clear();
}

這裡再一次強調,在不需要使用Context後,我們需要使用 MDC.clear() 方法將所有的鍵值對從MDC中移除,這樣會降低記憶體的使用量,並阻止MDC在後面試圖呼叫那些已經過期的資料。

在日誌框架中訪問MDC的值時,也稍微有些區別。對於儲存在上下文中的任何鍵,我們可以使用%X(鍵)的方式來訪問對應的值。這樣,我們可以使用 %X(username) 和 %X(sessionID) 來獲取對應的使用者名稱和會話ID:

<PatternLayout pattern="%X{username} %X{sessionID} %-5p - %m%n" />
"admin 1234 ERROR – Unable to open file!"

如果我們沒有指定任何鍵,那麼MDC上下文就會被以 {(key, value),(key, value)} 的方式傳遞給Appender。

Logback中的NDC和MDC

和Log4j不同,Logback內建沒有實現NDC。但是slf4j-ext包提供了一個NDC實現,它使用MDC作為基礎。在Logback內部,你可以使用 MDC.put()MDC.remove() 和 MDC.clear() 方法來訪問和管理MDC:

import org.slf4j.MDC;
...
Logger logger = LoggerFactory.getLogger(MDCLogback.class);
...
MDC.put("username", "admin");
MDC.put("sessionID", "1234");
try {
  FileReader fr = new FileReader("tmpFile");
}
catch (Exception ex) {
  logger.error("Unable to open file.");
}
finally {
  MDC.clear();
}

在Logback中,你可以在Logback.xml中將如下模式應用到Appender上,它可以輸出和上面Log4j相同的結果:

<Pattern>[%X{username}] %X{sessionID} %-5p - %m%n</Pattern>
"[admin] 1234 ERROR - Unable to open file."

針對MDC的訪問並不僅僅限制在PatternLayout上,例如,當使用JSONFormatter時,MDC中的所有值都會被匯出:

{
"timestamp":"1431970324945",
"level":"ERROR",
"thread":"main",
"mdc":{
"username":"admin",
"sessionID":"1234"
},
"logger":"MyClass",
"message":"Unable to open file.",
"context":"default"
}

ThreadContext

Version 2 of Log4j merged MDC and NDC into a single concept known as the Thread Context. The Thread Context is an evolution of MDC and NDC, presenting them respectively as the Thread Context Map and Thread Context Stack. The Thread Context is managed through the static ThreadContext class, which is implemented similar to Log4j 1’s MDC and NDC classes.

Log4j版本2中將MDC和NDC合併到一個單獨的元件中,這個元件被稱為執行緒上下文。執行緒上下文是針對MDC和NDC的進化,它分別用執行緒上下文Map對映執行緒上下文棧來表示MDC和NDC。我們可以通過ThreadContext靜態類來管理執行緒上下文,這個類在實現上類似於Log4j版本1中的MDC和NDC。

When using the Thread Context Stack, data is pushed to and popped from a stack just like with NDC:

當使用執行緒上下文棧時,我們可以向NDC那樣向棧中新增或者刪除資料:

import org.apache.logging.Log4j.ThreadContext;
...
ThreadContext.push(username);
ThreadContext.push(sessionID);
// Logging methods go here
ThreadContext.pop();
...

當使用執行緒上下文對映時,我們可以像MDC那樣將值和鍵結合在一起:

import org.apache.logging.Log4j.ThreadContext;
...
ThreadContext.put(“username”,"admin");
ThreadContext.put("sessionID", "1234");
// Logging methods go here
ThreadContext.clearMap();
...

ThreadContext類提供了一些方法,用於清除棧、清除MDC、清除儲存在上下文中的所有值,對應的方法是ThreadContext.clearAll()ThreadContext.clearMap()和ThreadContext.clearStack()。

和在MDC以及NDC中一樣,我們可以使用Layouts線上程上下文中訪問這些值。使用PatternLayout時,%x轉換模式會從棧中獲取值,%X和%X(鍵)會從圖中獲取值。

ThreadContext過濾

一些框架允許你基於某些屬性對日誌進行過濾例如,Log4j的DynamicThresholdFilter 會在鍵滿足特定條件的情況下,自動調整日誌級別。再比如,如果我們想要觸發TRACE級別的日誌訊息,我們可以建立一個名為trace-logging-enabled的鍵,並向log4j配置檔案中新增一個過濾器:

<Configuration name="MyApp">
<DynamicThresholdFilter key="trace-logging-enabled" onMatch="ACCEPT" onMismatch="NEUTRAL">
<KeyValuePair key="true" value="TRACE" />
</DynamicThresholdFilter>
...

如果ThreadContext包含一個名為trace-logging-enabled的鍵,onMatch 和 onMismatch 會決定如何處理它。關於 onMatch 和 onMismatch,我們有三個可選項:ACCEPT,它會處理過濾器的規則;DENY,它會忽略過濾器的規則;NEUTRAL,它會推遲到下一個過濾器。除了這些,我們還定義一個鍵值對,當值為true時,我們啟用TRACE級別的日誌。

現在,當trace-logging-enabled被設定成true時,即使根Logger設定的日誌級別高於TRACE,Appender也會記錄TRACE級別的訊息。

你可能還想過濾一些特定的日誌到特定的Appender中,Log4j中提供了ThreadContextMapFilter來實現這一點。如果我們想要限制某個特定的Appender只記錄針對某個使用者的TRACE級別的訊息,我們可以基於username鍵新增一個ThreadContextMapFilter:

<Console name="ConsoleAppender" target="SYSTEM_OUT">
<ThreadContextMapFilter onMatch="ACCEPT" onMismatch="DENY">
<KeyValuePair key="username" value="admin" />
</ThreadContextMapFilter>
...

如果想了解更多資訊,你可以檢視Log4jLogback文件中關於DynamicThresholdFilter部分。

Markers

Markers允許你對單獨的日誌記錄新增一些獨一無二的資料它可以用來對日誌記錄進行分組,觸發一些行為或者對日誌記錄進行過濾並將過濾結果輸出到指定的Appender中。你甚至可以將Markers和ThreadContext結合在一起使用,以提高搜尋和過濾日誌資料的能力。

例如,假設我們有一個可以連線到資料庫的類,如果在開啟資料庫的時候發生了異常,我們需要把異常記錄成fatal錯誤。我們可以建立一個名為DB_ERROR的Marker,然後將其應用到日誌事件中:

import org.apache.logging.Log4j.Marker;
import org.apache.logging.Log4j.MarkerManager;
...
final static Marker DB_ERROR = MarkerManager.getMarker("DATABASE_ERROR");
...
logger.fatal(DB_ERROR, "An exception occurred.");

為了在日誌輸出中顯示Marker資訊,我們需要在PatternLayout中新增%marker轉換模式:

<PatternLayout pattern="%p %marker: %m%n" />
[FATAL] DATABASE_ERROR: An exception occurred.

或者對於JSON和XML格式的Layouts,會自動在輸出中包含Marker資訊:

...
"thread" : "main",
"level" : "FATAL",
"loggerName" : "DBClass",
"marker" : {
  "name" : "DATABASE_ERROR"
},
"message" : "An exception occurred.",
...

通過對Marker資料進行自動解析和排序,集中式的日誌服務可以很容易對日誌進行搜尋處理。

Markers過濾

Marker過濾器可以讓你決定哪些Marker由哪些Logger來處理。marker欄位會比較在日誌事件裡面的Marker名字,如果名字匹配,那麼Logger會執行後續的行為。例如,在Log4j中,我們可以配置一個Appender來只顯示哪些使用了DB_ERROR Marker的訊息,這可以通過log4j2.xml中的Appender新增如下資訊來實現:

<MarkerFilter marker="DATABASE_ERROR" onMatch="ACCEPT" onMismatch="DENY" />

如果日誌記錄中某一條的Marker可以匹配這裡的marker欄位,那麼onMatch會決定如何處理這條記錄。如果不能夠匹配,或者日誌記錄中沒有Marker資訊,那麼onMismatch就會決定如何處理這條記錄。對於onMatch和onMismatch來說,有3個可選項:ACCEPT,它允許記錄事件;DENY,它會阻塞事件;NEUTRAL,它不會對事件進行任何處理。

在Logback中,我們需要更多一些設定。首先,想Appender中新增一個新的EvaluatorFilter,並如上所述指定onMatch和onMismatch行為然後,新增一個OnMarkerEvaluator並將Marker的名字傳遞給它:

<filter class="ch.qos.Logback.core.filter.EvaluatorFilter">
  <evaluator class="ch.qos.Logback.classic.boolex.OnMarkerEvaluator">
    <marker>DATABASE_ERROR</marker>
  </evaluator>
  <onMatch>ACCEPT</onMatch>
  <onMismatch>DENY</onMismatch>
</filter>

將Markers和NDCMDC以及ThreadContext結合使用

Marker的功能和ThreadContext類似,它們都是向日志記錄中新增獨一無二的資料,這些資料可以被Appender訪問。如果把這兩者結合使用,可以讓你更容易的對日誌資料進行索引和搜尋。如果能夠知道何時使用哪一種技術,會對我們有所幫助。

NDCMDC和ThreadContext被用於將相關日誌記錄結合在一起。如果你的應用程式會處理多個同時存在的使用者,ThreadContext可以讓你將針對某個特定使用者的一組日誌記錄組合在一起。因為ThreadContext針對每個執行緒都是不一樣的,所以你可以使用同樣的方法來對相關的日誌記錄進行自動分組。

另一方面,Marker通常用於標記或者高亮顯示某些特殊事件。在上述示例中,我們使用DB_ERROR Marker來標明在方法中發生的SQL相關異常。我們可以使用DB_ERROR Marker來將這些事件的處理過程和其他事件區分開來,例如我們可以使用SMTP Appender來將這些事件通過郵件傳送給資料庫管理員。

額外資源

指南和教程

  • Java Logging(Jakob Jenkov)——使用Java Logging API進行日誌開發教程
  • Java Logging Overview(Oracle)—— Oracle提供的在Java中進行日誌開發的指南
  • Log4J Tutorial(Tutorials Point)——使用log4j 版本1進行日誌開發的指南

日誌抽象層

  • Apache Commons Logging(Apache)——針對Log4jAvalon LogKit和java.util.logging的抽象層
  • SLF4J(QOS.ch)——一個流程的抽象層,應用在多個日誌框架上,包括Log4jLogback以及java.util.logging

日誌框架

  • Java Logging API(Oracle)—— Java預設的日誌框架
  • Log4j(Apache)——開源日誌框架
  • Logback(Logback Project)——開源專案,被設計成Log4j版本1的後續版本
  • tinylog(tinylog)——輕量級開源logger

相關文章