1. 程式人生 > >Logback中文文檔(二):體系結構

Logback中文文檔(二):體系結構

努力 獲取 功能 back soc 忽略 開始 技術分享 factor

logback

Logback 的基本結構充分通用,可應用於各種不同環境。目前,logback 分為三個模塊:Core、Classic 和 Access。
Core模塊是其他兩個模塊的基礎。Classic 模塊擴展了core模塊。Classic 模塊相當於 log4j的顯著改進版。Logback-classic 直接實現了 SLF4J API,因此你可以在 logback 與其他記錄系統如 log4j 和 java.util.logging (JUL)之間輕松互相切換。Access 模塊與 Servlet 容器集成,提供 HTTP 訪問記錄功能。本文不講述 access 模塊。
本文中,“logback”代表 logback-classic 模塊。

Logger 、Appender 和 和 Layout

Logback 建立於三個主要類之上:Logger、Appender 和 Layout。這三種組件協同工作,使開發者可以按照消息類型和級別來記錄消息,還可以在程序運行期內控制消息的輸出格式和輸出目的地。
Logger類是logback-classic 模塊的一部分,而Appender和Layout接口來自logback-core。
作為一個多用途模塊,logback-core 不包含任何 logger。

Logger 上下文

任何比System.out.println高級的記錄API的第一個也是最重要的優點便是能夠在禁用特定記錄語句的同時卻不妨礙輸出其他語句。這種能力源自記錄隔離(space)——即所有各種記錄語句的隔離——是根據開發者選擇的條件而進行分類的。在 logback-classic 裏,這種分類是 logger 固有的。各個 logger 都被關聯到一個 LoggerContext,LoggerContext 負責制造logger,也負責以樹結構排列各 logger。
Logger 是命名了的實體。它們的名字大小寫敏感且遵從下面的層次化的命名規則:
命名層次:
如果 logger 的名稱帶上一個點號後是另外一個 logger 的名稱的前綴,那麽, 前者 就被稱為 後者的祖先。 如果 logger 與代 其後代 logger 之間沒有其他祖先, 那麽,前者就被稱為子logger 之 父。
比如,名為“com.foo"”的 logger 是名為“com.foo.Bar”之父。同理, “java”是“java.util"”之父,也是“java.util.Vector”的祖先。
根 logger 位於 logger 等級的最頂端,它的特別之處是它是每個層次等級的共同始祖。
如同其他各 logger,根 logger 可以通過其名稱取得,如下所示:
Logger rootLogger = LoggerFactory.getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME);

其他所有 logger 也通過 org.slf4j.LoggerFactory 類的靜態方法 getLogger 取得。getLogger方法以 logger 名稱為參數。Logger 接口的部分基本方法列舉如下:

package org.slf4j;
public interface Logger {
// Printing methods:
public void trace(String message);
public void debug(String message);
public void info(String message);
public void warn(String message);
public void error(String message);

}

有效 級別(Level ) 即 級別 繼承

Logger 可以被分配級別。級別包括:TRACE、DEBUG、INFO、WARN 和 ERROR,定義於 ch.qos.logback.classic.Level 類。註意在 logback 裏,Level 類是 final 的,不能被繼承,Marker 對象提供了更靈活的方法。
如果 logger 沒有被分配級別,那麽它將從有被分配級別的最近的祖先那裏繼承級別。
更正式地說:logger L 的有效級別等於其層次等級裏的第一個非 null級別,順序是從 從 L 開始, 向上直至根 logger 。
為確保所有 logger 都能夠最終繼承一個級別,根 logger 總是有級別,默認情況下,這個級別是 DEBUG。
下面的四個例子包含各種分配級別值和根據級別繼承規則得出的最終有效(繼承)級別。

  • 例 1
Logger名 分配級別 有效級別
root DEBUG DEBUG
X none DEBUG
X.Y none DEBUG
X.Y.Z none DEBUG

例 1 裏,僅根 logger 被分配了級別。級別值 DEBUG 被其他 logger X、X.Y 和 X.Y.Z 繼
承。

  • 例 2
Logger名 分配級別 有效級別
root ERROR ERROR
X INFO INFO
X.Y DEBUG DEBUG
X.Y.Z WARN WARN

例 2 裏,所有 logger 都被分配了級別。級別繼承不發揮作用。

  • 例 3
Logger名 分配級別 有效級別
root DEBUG DEBUG
X INFO INFO
X.Y none INFO
X.Y.Z ERROR ERROR

例 3 裏,根 logger、X 和 X.Y.Z 分別被分配了 DEBUG、INFO 和 ERROR 級別。X.Y 從
其父 X 繼承級別。

  • 例 4
Logger名 分配級別 有效級別
root DEBUG DEBUG
X INFO INFO
X.Y none INFO
X.Y.Z none none

例 4 裏,根 logger 和 X 分別被分配了 DEBUG 和 INFO 級別。X.Y 和 X.Y.Z 從其最近的
父 X 繼承級別,因為 X 被分配了級別

打印方法和基本選擇規則

根據定義,打印方法決定記錄請求的級別。例如,如果 L 是一個 logger 實例,那麽,語句 L.info("..")是一條級別為 INFO 的記錄語句。
記錄請求的級別在高於或等於其 logger 的有效級別時被稱為被啟用,否則,稱為被禁用。如前所述,沒有被分配級別的 logger 將從其最近的祖先繼承級別。該規則總結如下:

基本選擇規則
記錄為 請求級別為 p ,其 logger 的有效級別為 為 q, , 只有當 則當 p>=q, 時, 該請求才會被執行。

該規則是 logback 的核心。級別排序為:TRACE < DEBUG < INFO < WARN < ERROR。

下表顯示了選擇規則是如何工作的。行頭是記錄請求的級別 p。列頭是 logger 的有效級別 q。行(請求級別)與列(有效級別)的交叉部分是按照基本選擇規則得出的布爾值。

請求級別 p有效級別 q
TRACEDEBUGINFOWARNERROROFF
TRACEYESNONONONONO
DEBUGYESYESNONONONO
INFOYESYESYESNONONO
WARNYESYESYESYESNONO
ERRORYESYESYESYESYESNO
OFFYESNONONONONO

下面是基本選擇規則的例子。

// 取得名為"com.foo"的 logger 實例
Logger logger = LoggerFactory.getLogger("com.foo");
// 設其級別為 INFO
logger.setLevel(Level.INFO);
Logger barlogger = LoggerFactory.getLogger("com.foo.Bar");
// 該請求有效,因為 WARN >= INFO
logger.warn("Low fuel level.");
// 該請求無效,因為 DEBUG < INFO.
logger.debug("Starting search for nearest gas station.");
// 名為"com.foo.Bar"的 logger 實例 barlogger, 從"com.foo"繼承級別
// 因此下面的請求有效,因為 INFO >= INFO.
barlogger.info("Located nearest gas station.");
// 該請求無效,因為 DEBUG < INFO.
barlogger.debug("Exiting gas station search");

譯者註:上例的 logger.setLevel(Level.INFO)無效。org.slf4j.Logger 沒有 setLevel()方法,ch.qos.logback.classic.Logger 有此方法。

獲取 Logger

用同一名字調用 LoggerFactory.getLogger 方法所得到的永遠都是同一個 logger 對象的引用。
例如:

Logger x = LoggerFactory.getLogger("wombat");
Logger y = LoggerFactory.getLogger("wombat");

x 和 y 指向同一個 logger 對象。

因此,可以配置一個 logger,然後從其他地方取得同一個實例,不需要到處傳遞引用。

生物學裏的父母總是先於其孩子,而 logback 不同,它可以以任何順序創建和配置 logger。

特別的是,即使“父”logger 是在其後代初始化之後才初始化的,它仍將查找並鏈接到其後代們。

通常是在程序初始化時對 logback 環境進行配置。推薦用讀配置文件類進行配置。稍後會講這種方法。

Logback 簡化了 logger 命名,方法是在每個類裏初始化 logger,以類的全限定名作為
logger 名。這種定義 logger 的方法即有用又直觀。由於記錄輸出裏包含 logger 名,這種命名
方法很容易確定記錄消息來源。Logback 不限制 logger 名,你可以隨意命名 logger。
然而,目前已知最好的策略是以 logger 所在類的名字作為 logger 名稱。

Appender 和 和 Layout

有選擇性地啟用或禁用記錄請求僅僅是 logback 功能的冰山一角。Logback 允許打印記錄請求到多個目的地。在 logback 裏,一個輸出目的地稱為一個 appender。目前有控制臺、文件、遠程套接字服務器、MySQL、PostreSQL、Oracle和其他數據庫、JMS和遠程UNIX Syslog守護進程等多種 appender。
一個 logger 可以被關聯多個 appender。
方法 addAppender 為指定的 logger 添加一個 appender。對於 logger 的每個啟用了的記錄請求,都將被發送到 logger 裏的全部 ppender 及更高等級的 appender。換句話說,appender疊加性地繼承了 logger 的層次等級。例如,如果根 logger 有一個控制臺 appender,那麽所有
啟用了的請求都至少會被打印到控制臺。如果 logger L 有額外的文件 appender,那麽,L 和L後代的所有啟用了的請求都將同時打印到控制臺和文件。設置 logger 的 additivity 為 false,則可以取消這種默認的 appender 累積行為。
控制 appender 疊加性的規則總結如下。
Appender 疊加性
Logger L 的記錄 語句的輸出會發送給 L 及其祖先的全部appender。這就是“appender疊加性”的含義。
然而,果 如果 logger L 的某個祖先 P 設置疊加性標識為 false,那麽,L 的輸出會發送給L 與 與 P 之間(含 P )的所有 appender ,但不會發送給 P 的任何祖先的 appender。

Logger 的疊加性默認為 true。

示例:

Logger 名 關聯的Appender 疊加性標識 輸出目標 說明
root A1 不可用 A1 疊加性標識不適用於根 logger
x A-x1,A-x2 true A1,A-x1,A-x2 根和 x
x.y none true A1, A-x1, A-x2 根和 x
x.y.z A-xyz1 true A1, A-x1, A-x2,A-xyz1 根、x.y.z 和 x
security A-sec false A-sec 因為疊加性標識為 false,所以appender 不累積。只有 A-sec
security.access none true A-sec 只有 security,因為 security 的疊 加 性 標 識 為false。

有些用戶希望不僅可以定制輸出目的地,還可以定制輸出格式。這時為 appender 關聯一個 layout 即可。Layout 負責根據用戶意願對記錄請求進行格式化,appender 負責將格式化化後的輸出發送到目的地。PatternLayout 是標準 logback 發行包的一部分,允許用戶按照類似於 C 語言的 printf 函數的轉換模式設置輸出格式。
例如,轉換模式"%-4relative [%thread] %-5level %logger{32} - %msg%n"在 PatternLayout裏會輸出形如:
176 [main] DEBUG manual.architecture.HelloWorld2 - Hello world.

第一個字段是自程序啟動以來的逝去時間,單位是毫秒。
第二個地段發出記錄請求的線程。
第三個字段是記錄請求的級別。
第四個字段是與記錄請求關聯的 logger 的名稱。
“-”之後是請求的消息文字。

參數化記錄

因為 logback-classic 裏的 logger 實現了 SLF4J 的 Logger 接口,某些打印方法可接受多個參數。這些不同的打印方法主要是為了在提高性能的同時盡量不影響代碼可讀性。
對於某個 Logger,下面的代碼
logger.debug("Entry number: " + i + " is " + String.valueOf(entry[i]));
在構造消息參數時有性能消耗,即把整數 i 和 entry[i]都轉換為字符串時,還有連接多個字符串時。不管消息會不會被記錄,都會造成上述消耗。
一個可行的辦法是用測試語句包圍記錄語句以避免上述消耗,比如,

if(logger.isDebugEnabled()) {
logger.debug("Entry number: " + i + " is " + String.valueOf(entry[i]));
}

當 logger 的 debug 級別被禁用時,這個方法可以避免參數構造帶來的性能消耗。另一方面,如果 logger 的 DEBUG 級別被啟用,那麽會導致兩次評估 logger 是否被啟用:一次是isDebugEnabled 方法,一次是 debug 方法。在實踐中,這種額外開銷無關緊要,因為評估 logger所消耗的時間不足實際記錄請求所用時間的 1%。

更好的替代方法

還有一種基於消息格式的方便的替代方法。假設 entry 是一個 object,你可以編寫:
Object entry = new SomeObject();
logger.debug("The entry is {}.", entry);
在評估是否作記錄後,僅當需要作記錄時,logger 才會格式化消息,用 entry 的字符串值替換"{}"。換句話說,當記錄語句被禁用時,這種方法不會產生參數構造所帶來的性能消耗。

工作原理

介紹過 logback 的核心組件後,下面描述 logback 框架在用戶調用 logger 的打印方法時
所做的事情。在本例中,用戶調用名為 com.wombat 的 logger 的 info()方法。

  1. 取得過濾鏈(filter chain)的判定結果
    如果 TurboFilter 鏈存在,它將被調用。Turbo filters 能夠設置一個上下文範圍內的臨界值,這個臨界值或者表示過濾某些與信息有關(比如 Marker、級別、Logger、消息)的特定事件,或者表示與每個記錄請求相關聯的 Throwable。如果過濾鏈的結果是 FilterReply.DENY,則記錄請求被拋棄。如果結果是 FilterReply.NEUTRAL,則繼續下一步,也就是第二步。如果結果是 FilterReply.ACCEPT,則忽略過第二步,進入第三步。
  2. 應用基本選擇規則
    Logback 對 logger 的有效級別與請求的級別進行比較。如果比較的結果是記錄請求被禁用,logback 會直接拋棄請求,不做任何進一步處理。否則,繼續下一步。
  3. 創建 LoggingEvent 對象
    記錄請求到了這一步後,logback 會創建一個 ch.qos.logback.classic.LoggingEvent 對象,該對象包含所有與請求相關的參數,比如請求用的 logger、請求級別、消息、請求攜帶的異常、當前時間、當前線程、執行記錄請求的類的各種數據,還有 MDC。
    註意有些成員是延遲初始化的,只有當它們真正被使用時才會被初始化。MDC 用來為記錄請求添加額外的上下文信息。之後的章節會討論 MDC。
  4. 調用 appender
    創建了 LoggingEvent 對象後,logback 將調用所有可用 appender 的 doAppend()方法,這就是說,appender 繼承 logger 的上下文。
    所有 appender 都繼承 AppenderBase 抽象類,AppenderBase 在一個同步塊裏實現了doAppend 方以確保線程安全。AppenderBase 的 doAppender()方法也調用 appender關聯的自定義過濾器,如果它們存在的話。自定義過濾器能被動態地關聯到任何appender,另有章節專門講述它。
  5. 格式化輸出
    那些被調用了的 appender 負責對記錄事件(LoggingEvent)進行格式化。然而,有些但不是全部 appender 把格式化記錄事件的工作委托給 layout。Layout 對LoggingEvent 實例進行格式化,然後把結果以字符串的形式返回。註意有些appender,比如 SocketAppender,把記錄事件進行序列化而不是轉換成字符串,所以它們不需要也沒有 layout。
  6. 發送記錄事件(LoggingEvent)
    記錄事件被格式化後,被各個 appender 發送到各自的目的地。
    下圖是整個流程的 UML 圖。技術分享圖片

性能

一個關於記錄的常見爭論是它的計算代價。這種關心很合理,因為即使是中等大小的應
用程序也會生成數以千計的記錄請求。人們花了很多精力來測算和調整記錄性能。盡管如此,
用戶還是需要註意下面的性能問題。

  1. 記錄被徹底關閉時的記錄性能
    你可以將根 logger 的級別設為最高級的 Level.OFF,就可以徹底關閉記錄。當記錄被徹底關閉時,記錄請求的消耗包括一次方法調用和一次整數比較。在 CPU 為3.2Ghz 的 Pentium D 電腦上,一般需要 20 納秒。
    但是,任何方法調用都會涉及“隱藏的” 參數構造消耗,例如,對於 logger x,x.debug("Entry number: " + i + "is " + entry[i]);
    把整數 i 和 entry[i]都轉換為字符串和連接各字符串會造成消息參數構造消耗,不管消息是否被記錄。
    參數構造消耗可以變得非常高,同時也跟參數大小有關。利用 SLF4J 的參數化記錄可以避免這種消耗。
    x.debug("Entry number: {} is {}", i, entry[i]);
    這種方式不會造成參數構造消耗。與前面的 debug()方法相比,這種方法快得多。
    只有當請求在被發送給 appender 時,消息才會被格式化。在格式化的時候,負責格式化消息的組件性能很高,不會對整個過程造成負面影響。格式化 1 個和 3 個參數分別需要 2 和 4 微妙。
    請註意,無論如何,應當避免在緊密循環裏或者非常頻繁地調用記錄語句,因為很可能降低性能。即使記錄被禁用,在緊密循環裏作記錄仍然會拖慢應用程序,如果記錄被啟用,就會產生大量(也是無用的)輸出。
  2. 當記錄啟用時,判斷是否進行記錄的性能
    在 logback 中,logger 在被創建時就明確地知道其有效級別(已經考慮了級別繼承)。
    當父 logger 的級別改變時,所有子 logger 都會得知這個改變。因此,在根據有效級別去接受或拒絕請求之前,logger 能夠作出準即時判斷,不需要咨詢其祖先。
  3. 實際記錄(格式化和寫入輸出設備)
    性能消耗包括格式化肌瘤輸出和發送到目的地。我們努力使 layout(formatter)和appender都盡可能地快。記錄到本地機器的文件裏的耗時一般大約在 9至12微秒。
    如果目的地是遠程服務器上的數據庫時,會增加早幾個毫秒。
    盡管功能豐富,logback 最首要的一項設計目標就是執行速度,重要程度僅排在可靠性之後。為提高性能,logback 的一些組件已經被多次重寫。

Logback中文文檔(二):體系結構