1. 程式人生 > >The logback manual #02# Architecture

The logback manual #02# Architecture

索引

  • Logback's architecture

  • Logger, Appenders and Layouts

    1. Logger context
    2. Effective Level(有效等級)
    3. basic selection rule
    4. Retrieving Loggers
    5. Parameterized logging
    6. 在引擎蓋下面偷看一下。。
  • Performance

參雜著原文翻譯和自己的話。。感謝有道翻譯。

全文註釋:

  • log請求=logging request=日誌請求,指代logger.info、logger.debug之類的呼叫。
  • 祖先的意思根據語境變化,有時候也包含父母,大多數時候只指爺爺輩以上。

Logback's architecture

logback被分成三個模組:logback-core, logback-classic和logback-access,其中core模組是其它兩個模組的基礎。classic模組是core模組的擴充套件,它對應著log4j的一個顯著改進版本。logback-classic實現了SLF4J API,這讓你可以在各種日誌系統(JUL、log4j等)和logback間無壓力地切換。第三個名為access的模組與Servlet容器整合,以提供HTTP訪問日誌功能。文件的剩餘部分,主要講述logback-classic相關的內容。

Logger, Appenders and Layouts

Logback建立在三種主要類的基礎上:LoggerAppender以及Layout。

Logger類屬於logback-classic模組,另外兩個介面屬於logback-core模組,作為一個通用模組,在logback-core模組中並沒有logger的概念。

Logger context

任何日誌API優於普通System.out.println的第一個也是最重要的優勢在於:日誌API能夠禁用特定的log語句,同時允許其他log語句不受阻礙地列印。這暗示著log語句按照開發人員制定的標準分成若干類。在logback-classic中,這種分類是logger的固有部分。所有logger都共同隸屬於一個LoggerContext

,後者負責製造logger以及將它們排列在樹狀層次結構中。

logger都是有名字的實體。它們的名字是大小寫敏感,並且遵循分層命名規則。例如說,名為“com.mycompany”的logger被稱作名為“com.mycompany.xxx”的logger的父母(直接關係!)。類似地,“com”被稱為“com.mycompany.xxx”的祖先(隔代關係)。

root logger位於logger的樹狀層次結構的頂端(根部)。它的特殊之處在於它是任何logger樹狀層次結構的一部分。像其它Logger一樣,可以通過名字拿到它:

        Logger rootLogger = LoggerFactory.getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME);
        rootLogger.debug("大家好!我是root logger");

Effective Level(有效等級)又名Level Inheritance

logger可以被賦予等級,可以分配的等級有:TRACE, DEBUG, INFO, WARN和ERROR。Level類是final的,不能有子類!文件上的言下之意貌似是有更靈活的方式來改變Level類。如果,一個logger沒有被賦予等級,則它的有效等級與和它最近的被賦予等級的祖先logger一樣。言下之意就是,所有logger雖然不一定都有賦予等級,但是都是具有“有效等級”的(繼承自祖先)。參考圖:

強調:子logger繼承的是最近祖先的assigned level,而不是effective level!

Printing methods and the basic selection rule

形如logger.info("xxxxxxxx...")是一個等級為info的log請求。只有在log請求的等級高於或者等於它的logger的有效等級時才是“啟用(enabled )”的,否則稱該請求被禁用(disabled)。

這個規則是logback的核心。它設定的級別(logger有效等級,log請求等級)順序如下:TRACE < DEBUG < INFO < WARN < ERROR。

basic selection rule例子程式:

package org.sample.logback;

import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class LogbackTest {

    @Test
    public void testLogback() {
        // root logger的預設賦予等級,也就是有效等級為DEBUG
        Logger rootLogger = LoggerFactory.getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME);

        // This request is disabled, because TRACE < DEBUG.
        rootLogger.trace("trace:大家好!我是root logger");
        // This request is enabled, because DEBUG >= DEBUG
        rootLogger.debug("debug:大家好!我是root logger");
        // This request is enabled, because INFO >= DEBUG
        rootLogger.info("info:大家好!我是root logger");
        // This request is enabled, because WARN >= DEBUG
        rootLogger.warn("warn:大家好!我是root logger");

        /*
        output=
        14:28:37.443 [main] DEBUG ROOT - debug:大家好!我是root logger
        14:28:37.448 [main] INFO ROOT - info:大家好!我是root logger
        14:28:37.448 [main] WARN ROOT - warn:大家好!我是root logger
        PS. 唯獨不見trace
         */
    }
}

Retrieving Loggers - - 檢索logger

名字相同的logger就是同一個logger,給定logger的名字可以在應用程式中的任何地方拿到這個logger。

按照生物學的觀點,老子總是得先於孩子的。但是logback有那麼點不同,它允許兒子先初始化、老子之後初始化,不過兩個logger的父子關係在老子初始化後也會自動關聯上去,讓我們來看一個例子:

package org.sample.logback;

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

public class LogbackTest {

    @Test
    public void testLogback() {
        Logger son = LoggerFactory.getLogger("father.son"); // 兒子先進行初始化
        // son預設繼承root logger的賦予等級,也就是son的有效等級是DEBUG
        son.trace("help!"); // 這個log請求未被啟用,因為 TRACE < DEBUG

        // Level類被定義在ch.qos.logback.classic裡。所以必須用ch.qos.logback.classic.Logger才能設定它的level
        ch.qos.logback.classic.Logger father =
                (ch.qos.logback.classic.Logger) LoggerFactory.getLogger("father"); 
        // 這個時候,初始化兒子他爹,將自動進行關聯
        father.setLevel(Level.TRACE); // 這樣兒子的有效等級也變成TRACE了

        son.trace("help!"); // 結果就是上一句不列印,這一句列印
    }
}

當然,在通常情況下,logger的配置在程式初始化的時候就完成了(通過讀取配置檔案)。用類全名來命名logger仍然是目前已知的最好的策略。

Appenders and Layouts

允許選擇性地啟用和禁用log請求只是logback功能的一部分。Logback還允許將log請求列印到多個目的地。用logback裡的術語來講,日誌的“輸出目的地”被稱為appender。一個logger可以有多個appender。

可以通過addAppender方法把一個appender新增到logger上。對於給定logger的每個enable的log請求都將被轉發給該logger中的所有appender以及層次結構中更高所有appender(祖先、父母輩的appender)。換句話說,appender具有可加性。兒子能夠共享老子(以及祖先,前提是老子沒有setAdditive(false);)的所有appender。舉個例子,在這裡忽略root logger的存在,假設老子logger有且僅有一個輸出到檔案的appender,而兒子logger有且僅有一個輸出到控制檯的appender,那麼老子就只能列印到檔案,而兒子列印到控制檯列印到檔案,如果有一天兒子不想再列印到檔案了,只要setAdditive(false);就可以了,並且孫子(兒子的兒子)也不會列印到檔案(等於兒子全家都和老子斷絕關係了。)

使用者通常都是慾求不滿的,他們並不滿足於決定“輸出目的地”,還希望能夠隨意操控列印格式。好在這個功能很簡單,只需要把layout和相應的appender關聯起來就ok了。layout負責按照使用者的期望格式化log請求。PatternLayout是標準logback發行版的一部分,它允許使用者根據類似於C語言printf函式的轉換模式指定輸出格式。

Parameterized logging引數化log請求

不用這樣寫:

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

這樣寫就可以了↓

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

以下兩行將產生完全相同的輸出。但是,如果log請求被禁用,那麼第二個變體的效能要比第一個變體好至少30倍

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

多個引數也是支援的。如下:

package org.sample.logback;

import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class LogbackTest {

    @Test
    public void testLogback() {
        Logger logger = LoggerFactory.getLogger(LogbackTest.class);
        logger.debug("Hello {} , how are {}", "xkfx", "you");
        // => 15:57:16.196 [main] DEBUG org.sample.logback.LogbackTest - Hello xkfx , how are you
        logger.debug("Hello {} , how are {}, what is your {}", "xkfx", "you", "name");
        // =>15:58:03.783 [main] DEBUG org.sample.logback.LogbackTest - Hello xkfx , how are you, what is your name

        // 還可以這樣↓
        Object[] paramArray = {"xkfx", "you", "name"};
        logger.debug("Hello {} , how are {}, what is your {}", paramArray); // 輸出同上
    }
}

A peek under the hood在引擎蓋下面偷看一下。。

介紹完了logback的幾個基本元件,我們已經可以描述logback框架在使用者發起log請求時所採取的步驟。現在讓我們分析一下當用戶呼叫名為com.wombat的logger的info()方法時,logback所採取的步驟。接下來用單步除錯跟蹤一下logger.info("Hello World");

1. Get the filter chain decision

首先step into進入logger.info方法:

出現一個看到名字想不出來具體含義的方法,繼續step into進入filterAndLog_0_Or3Plus方法:

    private void filterAndLog_0_Or3Plus(String localFQCN, Marker marker, Level level, String msg, Object[] params, Throwable t) {
        /*
        如果filter chain存在,則TurboFilter鏈將被呼叫
        1、Turbo過濾器可以設定上下文範圍的閾值,
        2、或者根據特定的資訊(Marker、Level、Logger、message以及與每個log請求關聯的Throwable等)
        過濾掉某些事件。
        (PS. 硬翻,可能有錯)
         */
        FilterReply decision = this.loggerContext.getTurboFilterChainDecision_0_3OrMore(marker, this, level, msg, params, t);
        if(decision == FilterReply.NEUTRAL) {
            if(this.effectiveLevelInt > level.levelInt) {
                // 這裡就比較顧名思義了。。
                // 因為level.levelInt=20000(INFO)> 有效等級DEBUG的10000
                // 所以logging request不會在這裡被拋棄
                return;
            }
        } else if(decision == FilterReply.DENY) {
            return;
        }
        this.buildLoggingEventAndAppend(localFQCN, marker, level, msg, params, t);
    }

=> 補充turbo filter的相關資料:在Logback-classic中提供兩個型別的filters , 一種是regular filters,另一種是turbo filter。regular filters是與appender 繫結的,而turbo filter是與與logger context(logger上下文繫結的,區別就是,turbo filter過濾所有logging request(log請求),而regular filter只過濾某個appender的logging request。(作者:巫巫巫政霖,原文:https://blog.csdn.net/Doraemon_wu/article/details/52072360

2. Apply the basic selection rule

就是指上面程式碼中的這一段:

如果decision == FilterReply.ACCEPT的話,就沒有這個步驟了。

3. Create a LoggingEvent object

step into進入buildLoggingEventAndAppend(localFQCN, marker, level, msg, params, t);

引數情況:

我們將用這些引數,以及當前時間、當前執行緒、各種類的相關資料、MDC等構造一個LoggingEvent物件。

    private void buildLoggingEventAndAppend(String localFQCN, Marker marker, Level level, String msg, Object[] params, Throwable t) {
        LoggingEvent le = new LoggingEvent(localFQCN, this, level, msg, t, params);
        le.setMarker(marker);
        this.callAppenders(le);
    }

請注意,其中一些欄位是惰性初始化的,只有在實際需要時才初始化。MDC用於附加上下文資訊來修飾logging request。MDC在後面的一章中討論。

4. Invoking appenders

在建立LoggingEvent物件之後,logback將呼叫所有當前logger可用的appender(就是自己的、老子的、祖先的)的doAppend()方法。step into進入callAppenders(ILogginEvent event);↓

    public void callAppenders(ILoggingEvent event) {
        int writes = 0;

        for(Logger l = this; l != null; l = l.parent) {
            writes += l.appendLoopOnAppenders(event);
            if(!l.additive) {
                break;
            }
        }

        if(writes == 0) {
            this.loggerContext.noAppenderDefinedWarning(this);
        }

    }

這個for迴圈寫得也是非常酷炫。。首先檢查com.wombat的AppenderAttachableImpl,發現沒有,再找com的aai,發現也沒有,最後找root logger的aai,終於找到了,於是對這個aai(應該是和logger的特定appender共同存亡的一個物件)呼叫AppenderAttachableImpl類的appendLoopOnAppenders(event)方法,如下所示,外圍的appendLoopOnAppenders方法是屬於ch.qos.logback.classic.Logger類的:

    private int appendLoopOnAppenders(ILoggingEvent event) {
        return this.aai != null?this.aai.appendLoopOnAppenders(event):0;
    }

在appendLoopOnAppenders的方法體裡拿到對應appender的引用,呼叫doAppend方法(傳入logginevent物件作為引數)。所有logback發行版裡的appender都繼承自抽象類AppenderBase,這個抽象類在synchronized塊裡實現了doAppend方法,從而確保了執行緒安全。doAppend方法體中同樣會呼叫appender所關聯的過濾器,同樣是會拿到一個FilterReply來決定要不要放棄掉這個logging request。

但是我卻進入了一個叫做UnsynchronizedAppenderBase的doAppend方法裡,並沒有看到synchronized塊:

    public void doAppend(E eventObject) {
        if(!Boolean.TRUE.equals(this.guard.get())) {
            try {
                this.guard.set(Boolean.TRUE);
                if(this.started) {
                    if(this.getFilterChainDecision(eventObject) == FilterReply.DENY) {
                        return;
                    }

                    this.append(eventObject);
                    return;
                }

                if(this.statusRepeatCount++ < 3) {
                    this.addStatus(new WarnStatus("Attempted to append to non started appender [" + this.name + "].", this));
                }
            } catch (Exception var6) {
                if(this.exceptionCount++ < 3) {
                    this.addError("Appender [" + this.name + "] failed to append.", var6);
                }

                return;
            } finally {
                this.guard.set(Boolean.FALSE);
            }

        }
    }
doAppend

5. Formatting the output

被呼叫的appender負責格式化logging event。然而,有些(但不是全部)appender將格式化logging event的任務委託給一個layout。layout格式化LoggingEvent例項並以字串的形式返回結果。注意,有些appender(比如SocketAppender)不會將logging event轉換為字串,而是序列化它。因此,它們既沒有也不需要layout。

6. Sending out the LoggingEvent

logging event完全格式化後,由每個appender傳送到其目的地。

貼張自己做的圖:

Performance

詳細參考原文末尾