logback架構

logback 的架構

在這裏插入圖片描述

一個著名的日誌系統是怎麼設計出來的強烈推薦一下這篇博文,它可以讓你詳細的瞭解java日誌發展史

Logger, Appender 和 Layouts

Logback 構建在三個主要的類上:Logger,Appender 和 Layouts。這三個不同類型的組件一起作用能夠讓開發者根據消息的類型以及日誌的級別來打印日誌。

Logger 類作爲 logback-classic 模塊的一部分。Appender 與 Layouts 接口作爲 logback-core 的一部分。作爲一個通用的模塊,logback-core 沒有 logger 的概念。

Logger 上下文(LoggerContext)

任何日誌 API 的優勢在於它能夠禁止某些日誌的輸出,但是又不會妨礙另一些日誌的輸出。通過假定一個日誌空間,這個空間包含所有可能的日誌語句,這些日誌語句根據開發人員設定的標準來進行分類。在 logback-classic 中,分類是 logger 的一部分,每一個 logger 都依附在 LoggerContext 上,它負責產生 logger,並且通過一個樹狀的層級結構來進行管理。
一個 Logger 被當作爲一個實體,它們的命名是大小寫敏感的,並且遵循一下規則:

命名層次結構

如果一個 logger 的名字加上一個 . 作爲另一個 logger 名字的前綴,那麼該 logger 就是另一個 logger 的祖先。如果一個 logger 與另一個 logger 之間沒有其它的 logger ,則該 logger 就是另一個 logger 的父級。

例如:名爲 com.foo 的 logger 是名爲 com.foo.Bar 的 logger 的父級。名爲 java 的 logger 是名爲 java.util 的父級,是名爲 java.util.Vector 的祖先。

root logger 作爲 logger 層次結構的最高層。它是一個特殊的 logger,因爲它是每一個層次結構的一部分。每一個 logger 都可以通過它的名字去獲取。例:

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

所有其它的 logger 通過 org.slf4j.LoggerFactory 類的靜態方法 getLogger 去獲取,這個方法需要傳入一個 logger 的名字。下面是 Logger 接口一些基本的方法:

package org.slf4j;
public interface Logger {
  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);
}

logback的等級繼承特性

Logger 能夠被分成不同的等級。不同的等級(TRACE, DEBUG, INFO, WARN, ERROR)定義在 ch.qos.logback.classic.Level 類中。在 logback 中,類 Level 使用 final 修飾的,所以它不能用來被繼承。一種更靈活的方式是使用 Marker 對象。

如果一個給定的 logger 沒有指定一個層級,那麼它就會繼承離它最近的一個祖先的層級。更正式的說法是:

對於一個給定的名爲 L 的 logger,它的有效層級爲從自身一直回溯到 root logger,直到找到第一個不爲空的層級作爲自己的層級。

爲了確保所有的 logger 都有一個層級,root logger 會有一個默認層級 DEBUG

以下四個例子指定不同的層級,以及根據繼承規則得到的最終有效層級

實例1

logger的名字 日誌級別 有效級別
root DEBUG DEBUG
X none DEBUG
X.Y none DEBUG
X.Y.Z none DEBUG

在這個例子中,只有 root logger 被指定了級別,所以 logger X,X.Y,X.Y.Z 的有效級別都是 DEBUG。

實例2

logger的名字 日誌級別 有效級別
root DEBUG DEBUG
X INFO INFO
X.Y DEBUG DEBUG
X.Y.Z WARN WARN

在這個例子中,每個 logger 都分配了級別,所以有效級別就是指定的級別。

實例3

logger的名字 日誌級別 有效級別
root DEBUG DEBUG
X INFO INFO
X.Y none INFO
X.Y.Z WARN WARN

在這個例子中,logger root,X,X.Y.Z 都分別分配了級別。logger X.Y 繼承它的父級 logger X。

方法打印以及基本選擇規則

根據定義,打印的方法決定的日誌的級別。例如:L 是一個 logger 實例,L.info("…") 的日誌級別就是 INFO。

如果一條的日誌的打印級別大於 logger 的有效級別,該條日誌纔可以被打印出來。這條規則總結如下:

基本選擇規則

日誌的打印級別爲 p,Logger 實例的級別爲 q,如果 p >= q,則該條日誌可以打印出來。

這條規則是 logbakc 的核心。各級別的排序爲:TRACE < DEBUG < INFO < WARN < ERROR

在下面的表格中,第一列表示的是日誌的打印級別,用 p 表示。第一行表示的是 logger 的有效級別,用 q 表示。行列交叉處的結果表示由—基本選擇規則 得出的結果。

在這裏插入圖片描述

實例:

package chapters.architecture;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import ch.qos.logback.classic.Level;

public class SelectionRule {

    public static void main(String[] args) {

        // ch.qos.logback.classic.Logger 可以設置日誌的級別
        // 獲取一個名爲 "com.foo" 的 logger 實例
        ch.qos.logback.classic.Logger logger =
                (ch.qos.logback.classic.Logger)LoggerFactory.getLogger("com.foo");
        // 設置 logger 的級別爲 INFO
        logger.setLevel(Level.INFO);

        // 這條日誌可以打印,因爲 WARN >= INFO
        logger.warn("警告信息");
        // 這條日誌不會打印,因爲 DEBUG < INFO
        logger.debug("調試信息");

        // "com.foo.bar" 會繼承 "com.foo" 的有效級別
        Logger barLogger = LoggerFactory.getLogger("com.foo.bar");
        // 這條日誌會打印,因爲 INFO >= INFO
        barLogger.info("子級信息");
        // 這條日誌不會打印,因爲 DEBUG < INFO
        barLogger.debug("子級調試信息");
    }
}

獲取 Logger

通過 LoggerFactory.getLogger() 可以獲取到具體的 logger 實例,名字相同則返回的 logger 實例也相同。由於logback實現了slf4j,因此使用slf4j的Logger抽象類,和工廠類即可

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

x,y 是同一個 logger 對象。

可以通過配置一個 logger,然後在其它地方獲取,而不需要傳遞引用。父級 logger 總是優於子級 logger,並且父級 logger 會自動尋找並關聯子級 logger,即使父級 logger 在子級 logger 之後實例化。

logback 環境的配置會在應用初始化的時候完成。最優的方式是通過讀取配置文件。

在每個類裏面通過指定全限定類名爲 logger 的名字來實例化一個 logger 是最好也是最簡單的方式。因爲日誌能夠輸出這個 logger 的名字,所以這個命名策略能夠看出日誌的來源是哪裏。雖然這是命名 logger 常見的策略,但是 logback 不會嚴格限制 logger 的命名,你完全可以根據自己的喜好來,你開心就好。

但是,根據類的全限定名來對 logger 進行命名,是目前最好的方式,沒有之一。

Appender 與 Layout

有選擇的啓用或者禁用日誌的輸出只是 logger 的一部分功能。logback 允許日誌在多個地方進行輸出。站在 logback 的角度來說,輸出目的地叫做 appender。appender 包括console、file、remote socket server、MySQL、PostgreSQL、Oracle 或者其它的數據庫、JMS、remote UNIX Syslog daemons 中。

一個 logger 可以有多個 appender。

logger 通過 addAppender 方法來新增一個 appender。對於給定的 logger,每一個允許輸出的日誌都會被轉發到該 logger 的所有 appender 中去。換句話說,appender 從 logger 的層級結構中去繼承疊加性。例如:如果 root logger 添加了一個 console appender,所有允許輸出的日誌至少會在控制檯打印出來。如果再給一個叫做 L 的 logger 添加了一個 file appender,那麼 L 以及 L 的子級 logger 都可以在文件和控制檯打印日誌。可以通過設置 additivity = false 來改寫默認的設置,這樣 appender 將不再具有疊加性。

appender 的疊加性

logger L 的日誌輸出語句會遍歷 L 和它的子級中所有的 appender。這就是所謂的 appender 疊加性(appender additivity)

如果 L 的子級 logger 爲 P,且 P 設置了 additivity = false,那麼 L 的日誌會在 L 所有 的 appender 包括 P 本身的 appender 中輸出,但是不會在 P 的子級 appender 中輸出。

logger 默認設置 additivity = true。

Logger名 被附加的 Appenders Additivity Flag 輸出目標 描述
root A1 不適用 A1 root logger 爲 logger 層級中的最高層,additivity 對它不適用
x A-x1, A-x2 true A1, A-x1, A-x2 x 與 root 的 appender
x.y none true A1, A-x1, A-x2 x 與 root 的 appender
x.y.z A-xyz1 true A1, A-x1, A-x2, A-xyz1 x 與 x.y 與 root 的 appender
security A-sec false A-sec 因爲 additivity = false,所以只有 A-sec 這個 appender
security.access node true A-sec 因爲它的父級 logger security 設置了 additivity = false,所以只有 A-sec 這一個 appender

通常,用戶既想自定義日誌的輸出地,也想自定義日誌的輸出格式。通過給 appender 添加一個 layout 可以做到。layout 的作用是將日誌格式化,而 appender 的作用是將格式化後的日誌輸出到指定的目的地。PatternLayout 能夠根據用戶指定的格式來格式化日誌,類似於 C 語言的 printf 函數。

例:PatternLayout 通過格式化串 “%-4relative [%thread] %-5level %logger{32} - %msg%n” 會將日誌格式化成如下結果:

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

第一個參數表示程序啓動以來的耗時,單位爲毫秒。第二個參數表示當前的線程號。第三個參數表示當前日誌的級別。第四個參數是 logger 的名字。“-” 之後是具體的日誌信息。

參數化日誌

考慮到 logback-classic 實現了 SLF4J 的 Logger 接口,一些打印方法可以接收多個傳參。這些打印方法的變體主要是爲了提高性能以及減少對代碼可讀性的影響。

對於一些 Logger 如下輸出日誌:

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

會產生構建消息參數的成本,是因爲需要將整數轉爲字符串,然後再將字符串拼接起來。但是我們是不需要關心 debug 信息是否被記錄(強行曲解作者的意思)。

爲了避免構建參數帶來的損耗,可以在日誌記錄之前做一個判斷,如下:

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

在這種情況下,如果 logger沒有開啓 debug 模式,不會有構建參數帶來的性能損耗。換句話說,如果 logger 在 debug 級別,將會有兩次性能的損耗,一次是判斷是否啓用了 debug 模式,一次是打印 debug 日誌。在實際應用當中,這種性能上的損耗是可以忽略不計的,因爲它所花費的時間小於打印一條日誌的時間的 1%。

更好的選擇

有一種更好的方式去格式化日誌信息。假設 entry 是一個 Object 對象:

Object entry = new SomeObject();
logger.debug("The entry is {}", entry);

只有在需要打印 debug 信息的時候,纔會去格式化日誌信息,將 ‘{}’ 替換成 entry 的字符串形式。也就是說在這種情況下,如果禁止了日誌的打印,也不會有構建參數上的性能消耗。

下面兩行輸出的結果是一樣的,但是一旦禁止日誌打印,第二個變量的性能至少比第一個變量好上 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 組件之後,我們準備介紹一下,當用戶調用日誌的打印方法時,logback 所執行的步驟。現在我們來分析一下當用戶通過一個名爲 com.wombat 的 logger 調用了 info() 方法時,logback 執行了哪些步驟。

第一步:獲取過濾器鏈

如果存在,則 TurboFilter 過濾器會被調用,Turbo 過濾器會設置一個上下文的閥值,或者根據每一條相關的日誌請求信息,例如:Marker, Level, Logger, 消息,Throwable 來過濾某些事件。如果過濾器鏈的響應是 FilterReply.DENY,那麼這條日誌請求將會被丟棄。如果是 FilterReply.NEUTRAL,則會繼續執行下一步,例如:第二步。如果響應是 FilterRerply.ACCEPT,則會直接跳到第三步。

第二步:應用基本選擇規則

在這步,logback 會比較有效級別與日誌請求的級別,如果日誌請求被禁止,那麼 logback 將會丟棄調這條日誌請求,並不會再做進一步的處理,否則的話,則進行下一步的處理。

第三步:創建一個 LoggingEvent 對象

如果日誌請求通過了之前的過濾器,logback 將會創建一個 ch.qos.logback.classic.LoggingEvent 對象,這個對象包含了日誌請求所有相關的參數,請求的 logger,日誌請求的級別,日誌信息,與日誌一同傳遞的異常信息,當前時間,當前線程,以及當前類的各種信息和 MDC。MDC 將會在後續章節進行討論。

第四步:調用 appender

在創建了 LoggingEvent 對象之後,logback 會調用所有可用 appender 的 doAppend() 方法。這些 appender 繼承自 logger 上下文。

所有的 appender 都繼承了 AppenderBase 這個抽象類,並實現了 doAppend() 這個方法,該方法是線程安全的。AppenderBase 的 doAppend() 也會調用附加到 appender 上的自定義過濾器。自定義過濾器能動態的動態的添加到 appender 上,在過濾器章節會詳細討論。

第五步:格式化輸出

被調用的 appender 負責格式化日誌時間。但是,有些 appender 將格式化日誌事件的任務委託給 layout。layout 格式化 LoggingEvent 實例並返回一個字符串。對於其它的一些 appender,例如 SocketAppender,並不會把日誌事件轉變爲一個字符串,而是進行序列化,因爲,它們就不需要一個 layout。

第六步:發送 LoggingEvent

當日志事件被完全格式化之後將會通過每個 appender 發送到具體的目的地。

下圖是 logback 執行步驟的 UML 圖:

在這裏插入圖片描述

性能

記錄日誌經常被提到的一個點是它的計算代價。這是一個合理的考慮,因爲一箇中等大小的應用都可以產生成千上萬的日誌。我們的大部分努力都花在了測量以及調整 logback 的性能。但是用戶還是應該知道以下有關性能的問題。

  • 當日志記錄被關閉時記錄日誌的性能

通過設置 root logger 的日誌級別爲 Level.OFF 來完全關閉日誌的打印。當日志完全關閉的時候,日誌請求的成本爲方法的調用以及整數的比較。在 3.2Ghz 奔騰D 的電腦上的耗時大約爲 20 納秒。

任何方法的調用都有參數構建這個隱含的成本在裏面。例如下面這個例子:

x.debug("Entry number: " + i + "is " + entry[i]);
把整數 i、entry[i] 轉變爲字符串,並且連接在一起,而不管這條日誌是否會被打印。

構建參數的成本取決於參數的大小,爲了避免不必要的性能損耗,可以使用 SLF4J’s 的參數化構建:

x.debug(“Entry number: {} is {}”, i, entry[i]);
這種形式不會有構建參數的成本在裏面。與上一個例子做比較,這個的速度會更快。只有當日志信息傳遞給了附加的 appender 時纔會被格式化,而且格式化日誌信息的組件也是被優化過的。

  • 當日記記錄被打開時是否記錄日誌的性能
    在 logback 中,不需要遍歷 logger 的層次結構。logger 在創建的時候就知道自己的有效級別。如果父級 logger 的級別被更改,則會通知所有子級 logger 注意這個更改。因此,在基於有效級別的基礎上,logger 能夠準實時的做出決定是否接受或者拒絕日誌請求,而不需要考慮它的祖先的級別。

  • 日記記錄的實際情況(格式化輸出到指定設備)
    這是指格式化日誌輸出以及發送指定的目的地所需要的成本。我們儘可能快的讓 layout(格式化)以及 appender 執行。在本地機器上,將日誌輸出到文件大概耗費 9-12 微秒的時間。當把日誌輸出到數據庫或者遠程服務器上時會上升到幾毫秒。

儘管 logback 功能豐富,但是它最重要的目標之一是處理速度,這是僅次於可靠性的要求。爲了提高性能,一些 logback 的組件被重寫了幾次。

官方實例代碼

Bar.java

/**
 * Logback: the reliable, generic, fast and flexible logging framework.
 * Copyright (C) 1999-2015, QOS.ch. All rights reserved.
 *
 * This program and the accompanying materials are dual-licensed under
 * either the terms of the Eclipse Public License v1.0 as published by
 * the Eclipse Foundation
 *
 *   or (per the licensee's choosing)
 *
 * under the terms of the GNU Lesser General Public License version 2.1
 * as published by the Free Software Foundation.
 */
package chapters.architecture;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

class Bar {
    Logger logger = LoggerFactory.getLogger(Bar.class);

    public void doIt() {
        logger.debug("doing my job");
    }
}

MyAppWithConfigFile.java

/**
 * Logback: the reliable, generic, fast and flexible logging framework.
 * Copyright (C) 1999-2015, QOS.ch. All rights reserved.
 *
 * This program and the accompanying materials are dual-licensed under
 * either the terms of the Eclipse Public License v1.0 as published by
 * the Eclipse Foundation
 *
 *   or (per the licensee's choosing)
 *
 * under the terms of the GNU Lesser General Public License version 2.1
 * as published by the Free Software Foundation.
 */
package chapters.architecture;

//Import SLF4J classes.
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import ch.qos.logback.classic.LoggerContext;
import ch.qos.logback.classic.joran.JoranConfigurator;
import ch.qos.logback.core.joran.spi.JoranException;
import ch.qos.logback.core.util.StatusPrinter;

public class MyAppWithConfigFile {

    public static void main(String[] args) {
        Logger logger = LoggerFactory.getLogger(MyAppWithConfigFile.class);
        LoggerContext lc = (LoggerContext) LoggerFactory.getILoggerFactory();

        try {
            JoranConfigurator configurator = new JoranConfigurator();
            lc.reset();
            configurator.setContext(lc);
            configurator.doConfigure(args[0]);
        } catch (JoranException je) {
            StatusPrinter.print(lc.getStatusManager());
        }
        logger.info("Entering application.");
        Bar bar = new Bar();
        bar.doIt();
        logger.info("Exiting application.");

    }
}

SelectionRule.java

/**
 * Logback: the reliable, generic, fast and flexible logging framework.
 * Copyright (C) 1999-2015, QOS.ch. All rights reserved.
 *
 * This program and the accompanying materials are dual-licensed under
 * either the terms of the Eclipse Public License v1.0 as published by
 * the Eclipse Foundation
 *
 *   or (per the licensee's choosing)
 *
 * under the terms of the GNU Lesser General Public License version 2.1
 * as published by the Free Software Foundation.
 */
package chapters.architecture;

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

/**
 * @author Ceki G&uuml;lc&uuml;
 */
public class SelectionRule {

    public static void main(String[] args) {
        // get a logger instance named "com.foo". 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.foo");
        // set its Level to INFO. The setLevel() method requires a logback logger
        logger.setLevel(Level.INFO);

        Logger barlogger = LoggerFactory.getLogger("com.foo.Bar");

        // 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.foo.Bar",
        // will inherit its level from the logger named
        // "com.foo" 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");

    }
}

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章