1. 程式人生 > >logback 入門教程系列-02-logback 專案模組 Logger,Appender 和 Layout

logback 入門教程系列-02-logback 專案模組 Logger,Appender 和 Layout

Logback的架構

Logback的基本架構足夠通用,以便在不同情況下應用。

目前,logback分為三個模組:logback-core,logback-classic和logback-access。

核心模組為其他兩個模組奠定了基礎。經典模組擴充套件了核心。經典模組對應於log4j的顯著改進版本。

Logback-classic本身實現了SLF4J API,因此您可以在logback和其他日誌記錄系統(如JDK 1.4中引入的log4j或java.util.logging(JUL))之間來回切換。

第三個名為access的模組與Servlet容器整合,以提供HTTP訪問日誌功能。單獨的文件包含訪問模組文件。

在本文件的其餘部分中,我們將編寫“logback”來引用logback-classic模組。

Logger,Appender 和 Layout

Logback基於三個主要類:Logger,Appender和Layout。

這三種類型的元件協同工作,使開發人員能夠根據訊息型別和級別記錄訊息,並在執行時控制這些訊息的格式以及報告的位置。

Logger類是logback-classic模組的一部分。

另一方面,Appender和Layout介面是logback-core的一部分。作為通用模組,logback-core沒有 Logger 的概念。

記錄器上下文

任何日誌API優於普通 System.out.println

的第一個也是最重要的優勢在於它能夠禁用某些日誌語句,同時允許其他人不受阻礙地列印。

此功能假定日誌記錄空間(即所有可能的日誌記錄語句的空間)根據開發人員選擇的某些條件進行分類。

在logback-classic中,這種分類是記錄器的固有部分。

每個記錄器都附加到LoggerContext,後者負責製造記錄器以及將它們排列在樹狀層次結構中。

記錄器是命名實體。它們的名稱區分大小寫,它們遵循分層命名規則:

命名層次結構

如果記錄器的名稱後跟一個點是後代記錄器名稱的字首,則稱該記錄器是另一個記錄器的祖先。如果記錄器本身與後代記錄器之間沒有祖先,則稱記錄器是子記錄器的父節點。

例如,名為“com.foo”的記錄器是名為“com.foo.Bar”的記錄器的父級。

類似地,“java”是“java.util”的父級和“java.util.Vector”的祖先。大多數開發人員都應該熟悉這種命名方案。

根記錄器位於記錄器層次結構的頂部。它的特殊之處在於它是每個層次結構的一部分。

像每個記錄器一樣,它可以通過其名稱檢索,如下所示:

Logger rootLogger = LoggerFactory.getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME);

還可以使用org.slf4j.LoggerFactory類中的類靜態getLogger方法檢索所有其他記錄器。

此方法將所需記錄器的名稱作為引數。

下面列出了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); 
}

有效級別又是級別繼承

可以為記錄器分配級別。

可能的級別集(TRACE,DEBUG,INFO,WARN和ERROR)在ch.qos.logback.classic.Level類中定義。

請注意,在logback中,Level類是final類,不能是子類,因為以Marker物件的形式存在更靈活的方法。

如果給定的記錄器沒有分配級別,那麼它將從具有指定級別的最近祖先繼承一個級別。

更正式地說:

給定記錄器L的有效水平等於其層次結構中的第一個非空水平,從L本身開始並在層次結構中朝向根記錄器繼續向上。

為確保所有記錄器最終都能繼承一個級別,根記錄器始終具有指定的級別。預設情況下,此級別為DEBUG。

下面是四個示例,其中包含各種已分配的級別值以及根據級別繼承規則生成的有效(繼承)級別。

  • Example 1
Logger name Assigned level Effective level
root DEBUG DEBUG
X none DEBUG
X.Y none DEBUG
X.Y.Z none DEBUG

在上面的示例1中,僅為根記錄器分配了級別。

此級別值DEBUG由其他記錄器X,X.Y和X.Y.Z繼承

  • Example 2
Logger name Assigned level Effective level
root ERROR ERROR
X INFO INFO
X.Y DEBUG DEBUG
X.Y.Z WARN WARN

在上面的示例2中,所有記錄器都具有指定的級別值。

級別繼承不起作用。

  • Example 3
Logger name Assigned level Effective level
root DEBUG DEBUG
X INFO INFO
X.Y none INFO
X.Y.Z ERROR ERROR

在上面的示例3中,記錄器root,X和X.Y.Z分別被分配了DEBUG,INFO和ERROR級別。

Logger X.Y從其父X繼承其級別值。

  • Example 4
Logger name Assigned level Effective level
X INFO INFO
X.Y none INFO
X.Y.Z none INFO

在上面的示例4中,記錄器root和X分別被分配了DEBUG和INFO級別。記錄器X.Y和X.Y.Z從其最近的父X繼承其級別值,該父級具有指定的級別。

  • 個人總結

自己有就用自己的,自己沒有就用最近的父類的。

print 方法和基本選擇規則

根據定義,列印方法確定日誌記錄請求的級別。

例如,如果L是記錄器例項,則語句 L.info(“..”) 是級別INFO的記錄語句。

如果日誌記錄請求的級別高於或等於其記錄器的有效級別,則稱其已啟用。否則,該請求被稱為禁用。

如前所述,沒有指定級別的記錄器將從其最近的祖先繼承一個。該規則總結如下。

  • 基本選擇規則

如果 p >= q,則啟用向具有有效級別q的記錄器發出的級別p的日誌請求。

此規則是logback的核心。它假定級別按如下順序排序:TRACE <DEBUG <INFO <WARN <ERROR。

實際測試程式碼

  • LevelTest.java
package com.github.houbb.logback.learn.architecture;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ch.qos.logback.classic.Level;

/**
 * @author binbin.hou
 * @date 2018/11/19 14:32
 */
public class LevelTest {

    public static void main(String[] args) {
// get a logger instance named "com.github.houbb". Let us further assume that the
// logger is of type  ch.qos.logback.classic.Logger so that we can
// set its level
        ch.qos.logback.classic.Logger logger =
                (ch.qos.logback.classic.Logger) LoggerFactory.getLogger("com.github.houbb");


        Logger barlogger = LoggerFactory.getLogger("com.github.houbb.logback");

//set its Level to INFO. The setLevel() method requires a logback logger
        logger.setLevel(Level.INFO);

// This request is enabled, because WARN >= INFO
        logger.warn("Low fuel level.");

// This request is disabled, because DEBUG < INFO.
        logger.debug("Starting search for nearest gas station.");

// The logger instance barlogger, named "com.github.houbb.logback",
// will inherit its level from the logger named
// "com.github.houbb" Thus, the following request is enabled
// because INFO >= INFO.
        barlogger.info("Located nearest gas station.");

// This request is disabled, because DEBUG < INFO.
        barlogger.debug("Exiting gas station search");
    }

}

日誌資訊

14:40:40.071 [main] WARN  com.github.houbb - Low fuel level.
14:40:40.075 [main] INFO  com.github.houbb.logback - Located nearest gas station.

檢索記錄器

呼叫具有相同名稱的LoggerFactory.getLogger方法將始終返回對完全相同的logger物件的引用。

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

x、y 二者返回的是同一個引用。

因此,可以配置記錄器,然後在程式碼中的其他位置檢索相同的例項,而不傳遞引用。

與生父母一樣,父母總是先於孩子的基本矛盾,可以按任何順序建立和配置logback記錄器。特別是,“父”記錄器將查詢並連結到其後代,即使它們在它們之後被例項化。

通常在應用程式初始化時完成對logback環境的配置。首選方法是讀取配置檔案。這種方法將很快討論。

Logback可以輕鬆地按軟體元件命名記錄器。這可以通過在每個類中例項化記錄器來完成,記錄器名稱等於類的完全限定名稱。這是定義記錄器的有用且直接的方法。由於日誌輸出帶有生成記錄器的名稱,因此該命名策略可以輕鬆識別日誌訊息的來源。但是,這只是命名記錄器的一種可能的策略,儘管很常見。

Logback不會限制可能的記錄器集。作為開發人員,您可以根據需要自由命名記錄器。

儘管如此,在它們所在的類之後命名記錄器似乎是目前已知的最佳通用策略。

Appenders 和佈局

基於其記錄器選擇性地啟用或禁用記錄請求的能力僅是圖片的一部分。

Logback允許將記錄請求列印到多個目標。在logback中,輸出目標稱為appender。

目前,控制檯,檔案,遠端套接字伺服器,MySQL,PostgreSQL,Oracle和其他資料庫,JMS和遠端UNIX Syslog守護程式都存在appender。

可以將多個appender連線到記錄器。

addAppender方法將appender新增到給定的記錄器。給定記錄器的每個啟用的日誌記錄請求都將轉發到該記錄器中的所有appender以及層次結構中較高的appender。換句話說,appender是從記錄器層次結構中附加地繼承的。

例如,如果將控制檯appender新增到根記錄器,則所有啟用的日誌記錄請求將至少在控制檯上列印。如果另外將檔案追加器新增到記錄器(例如L),則對L和L’子項啟用的記錄請求將列印在檔案和控制檯上。通過將記錄器的additivity標誌設定為false,可以覆蓋此預設行為,以便不再新增appender累積。

有關appender可加性的規則總結如下。

記錄器L的日誌語句的輸出將轉到L及其祖先中的所有appender。這就是術語“appender additivity”的含義。

但是,如果記錄器L的祖先,比如說P,將additivity標誌設定為false,那麼L’s輸出將被定向到L中的所有appender及其祖先,包括P,但不包括任何祖先中的appenders。

記錄器預設情況下將其可加性標誌設定為true。

例子

Logger Name Attached Appenders Additivity Flag Output Targets Comment

root A1 not applicable A1 Since the root logger stands at the top of the logger hierarchy, the additivity flag does not apply to it.
x A-x1, A-x2 true A1, A-x1, A-x2 Appenders of “x” and of root.
x.y none true A1, A-x1, A-x2 Appenders of “x” and of root.
x.y.z A-xyz1 true A1, A-x1, A-x2, A-xyz1 Appenders of “x.y.z”, “x” and of root.
security A-sec false A-sec No appender accumulation since the additivity flag is set to false. Only appender A-sec will be used.
security.access none true A-sec Only appenders of “security” because the additivity flag in “security” is set to false.

格式化輸出

使用者通常不僅要定製輸出目的地,還要定製輸出格式。

這是通過將佈局與appender相關聯來實現的。佈局負責根據使用者的意願格式化日誌記錄請求,而appender負責將格式化的輸出傳送到其目的地。

PatternLayout是標準logback分發的一部分,它允許使用者根據類似於C語言printf函式的轉換模式指定輸出格式。

比如 PatternLayout 為 %-4relative [%thread] %-5level %logger{32} - %msg%n,輸出如下:

176  [main] DEBUG manual.architecture.HelloWorld2 - Hello world.

第一個欄位是自程式啟動以來經過的毫秒數。第二個欄位是發出日誌請求的執行緒。第三個欄位是日誌請求的級別。第四個欄位是與日誌請求關聯的記錄器的名稱。

- 後面的文字是請求的訊息。

引數化日誌記錄

鑑於logback-classic中的記錄器實現了SLF4J的Logger介面,某些列印方法允許多個引數。

這些列印方法變體主要用於提高效能,同時最小化對程式碼可讀性的影響。

對於一些Logger記錄器,寫作,

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

導致構造訊息引數的成本,即將整數 i 和 entry[i] 轉換為String,並連線中間字串。

無論是否記錄訊息,都是如此。

避免參數構造成本的一種可能方法是使用測試包圍日誌語句。這是一個例子。

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

這樣,如果對記錄器禁用除錯,則不會產生引數構造的成本。

另一方面,如果為DEBUG級別啟用了記錄器,則會產生評估記錄器是否啟用的成本,兩次:一次在debugEnabled中,一次在debug中。

實際上,這種開銷是微不足道的,因為評估記錄器所需的時間不到實際記錄請求所需時間的1%。

更好的選擇

存在基於訊息格式的便利替代方案。假設entry是一個物件,你可以寫:

Object entry = new SomeObject(); 
logger.debug("The entry is {}.", entry);
  • 個人感覺

這種寫法和傳統的 log4j 對比,使用起來是真的很方便。

  1. 可以使你編寫的時候程式碼更加優雅,閱讀的時候也不至於被字串拼接打斷。

  2. 效能也更加優異

只有在評估是否記錄之後,並且只有在決策是肯定的情況下,記錄器實現才會格式化訊息並將 {} 對替換為條目的字串值。

換句話說,當禁用日誌語句時,此表單不會產生引數構造的成本。

以下兩行將產生完全相同的輸出。但是,在禁用日誌記錄語句的情況下,第二個變體將比第一個變體優於至少30倍。

logger.debug("The new entry is "+entry+".");
logger.debug("The new entry is {}.", entry);

兩個引數:

logger.debug("The new entry is {}. It replaces {}.", entry, oldEntry);
  • 更多引數

你可以使用陣列,來實現格式化輸出。

Object[] paramArray = {newVal, below, above};
logger.debug("Value {} was inserted between {} and {}.", paramArray);

在引擎蓋下偷看

在我們介紹了基本的logback元件之後,我們現在準備描述當用戶呼叫記錄器的列印方法時logback框架所採取的步驟。

現在讓我們分析當用戶呼叫名為 com.wombat 的記錄器的 info() 方法時的logback步驟。

1.獲取過濾器鏈決定

如果存在,則呼叫 TurboFilter 鏈。

Turbo過濾器可以設定上下文範圍的閾值,或者根據與每個日誌記錄請求關聯的標記,級別,記錄器,訊息或Throwable等資訊過濾掉某些事件。

如果過濾器鏈的回覆是FilterReply.DENY,則刪除日誌記錄請求。

如果是FilterReply.NEUTRAL,那麼我們繼續下一步,即步驟2.如果回覆是FilterReply.ACCEPT,我們跳過下一步並直接跳到第3步。

2.應用基本選擇規則

在此步驟中,logback會將記錄器的有效級別與請求級別進行比較。如果根據此測試禁用了日誌記錄請求,則logback將丟棄請求而不進行進一步處理。否則,它將繼續進行下一步。

3.建立LoggingEvent物件

如果請求在前面的過濾器中存活,則logback將建立一個ch.qos.logback.classic.LoggingEvent物件,其中包含請求的所有相關引數,例如請求的記錄器,請求級別,訊息本身,異常可能已與請求,當前時間,當前執行緒,發出日誌記錄請求的類和MDC的各種資料一起傳遞。

請注意,其中一些欄位是懶惰地初始化的,只有在實際需要時才會這樣。

MDC用於使用其他上下文資訊來裝飾日誌記錄請求。

MDC將在後續章節中討論。

  • 個人筆記

MDC 非常適合不同系統呼叫的鏈路追蹤,可以不同的執行緒指定不同的 traceId。非常強大好用。

4.呼叫appender

在建立LoggingEvent物件之後,logback將呼叫所有適用的appender的doAppend()方法,即從記錄器上下文繼承的appender。

帶有logback發行版的所有appender都擴充套件了AppenderBase抽象類,該類在同步塊中實現了doAppend方法,從而確保了執行緒安全性。如果存在任何此類過濾器,AppenderBase的doAppend()方法還會呼叫附加到appender的自定義過濾器。可以動態附加到任何appender的自定義過濾器將在單獨的章節中介紹。

5.格式化輸出

呼叫的appender負責格式化日誌記錄事件。但是,某些(但不是全部)appender將將日誌記錄事件格式化的任務委派給佈局。佈局格式化LoggingEvent例項並將結果作為String返回。請注意,某些appender(如SocketAppender)不會將日誌記錄事件轉換為字串,而是將其序列化。因此,他們沒有也不需要佈局。

6.傳送LoggingEvent

記錄事件完全格式化後,每個appender將其傳送到目標。

這是一個序列UML圖,顯示一切是如何工作的。您可能需要單擊影象以顯示其更大的版本。

underTheHoodSequence2_small

效能

經常引用的反對日誌記錄的一個論點就是它的計算成本。這是一個合理的問題,因為即使是中等規模的應用程式也可以生成數千個日誌請求。我們的大部分開發工作都花在測量和調整logback的效能上。與這些努力無關,使用者仍應瞭解以下效能問題。

1.完全關閉日誌記錄時的記錄效能

您可以通過將根記錄器的級別設定為Level.OFF(可能的最高級別)來完全關閉日誌記錄。完全關閉日誌記錄時,日誌請求的成本包括方法呼叫和整數比較。在3.2Ghz Pentium D機器上,此成本通常約為20納秒。

但是,任何方法呼叫都涉及引數構造的“隱藏”成本。

例如,對於某些記錄器x寫入,

x.debug("Entry number: " + i + "is " + entry[i]);

導致構造訊息引數的成本,即將整數i和entry [i]都轉換為字串,並連線中間字串,而不管是否記錄訊息。

引數構造的成本可能非常高,並且取決於所涉及的引數的大小。

為了避免參數構造的成本,您可以利用SLF4J的引數化日誌記錄:

x.debug("Entry number: {} is {}", i, entry[i]);

該變體不會產生引數構造的成本。與之前呼叫debug()方法相比,它的速度更快。

僅當要將記錄請求傳送到附加的appender時,才會格式化該訊息。此外,格式化訊息的元件也得到了高度優化。

儘管上述將緊密迴圈中的日誌語句(即非常頻繁呼叫的程式碼)放置在一起,但卻是一種雙輸的提議,可能會導致效能下降。

即使關閉日誌記錄,登入緊密迴圈也會降低應用程式的速度,如果開啟日誌記錄,則會產生大量(因而無用的)輸出。

2.開啟日誌記錄時決定是否記錄日誌的效能。

在logback中,不需要遍歷記錄器層次結構。記錄器在建立時知道其有效級別(即,其級別,一旦考慮了級別繼承)。

如果更改了父記錄器的級別,則會聯絡所有子記錄器以注意更改。

因此,在基於有效水平接受或拒絕請求之前,記錄器可以做出準瞬時決定,而無需諮詢其祖先。

3.實際記錄(格式化和寫入輸出裝置)

這是格式化日誌輸出並將其傳送到目標目標的成本。

在這裡,我們再次努力使佈局(格式化程式)儘可能快地執行。

appender也是如此。記錄到本地計算機上的檔案時,實際記錄的典型成本約為9到12微秒。登入到遠端伺服器上的資料庫時,它會持續幾毫秒。

雖然功能豐富,但是logback最重要的設計目標之一是執行速度,這是僅次於可靠性的要求。

一些logback元件已被重寫幾次以提高效能。

參考資料

https://logback.qos.ch/manual/architecture.html