
以下內容翻譯整理自logback官方手冊,地址:logback官方手冊
logback 的架構
logback的基本架構足夠通用,可以應用于不同的環(huán)境。目前,logback分為三個模塊:logback-core,logback-classic和logback-access。
core模塊是其它兩個模塊的基礎,classic模塊繼承core模塊,classic模塊相對log4j版本有顯著的改進,logback-classic天生實現(xiàn)了SLF4J API,所以你可以在logback和其他日志框架之間自由切換,比如log4j和JDK1.4引入的JUL(java.util.logging)。access模塊集成了Servlet容器,用來提供HTTP-access日志功能,一個單獨的文檔包含訪問模塊文檔。
在本文檔的其余部分中,我們將引用logback-classic模塊來編寫logback。
Logger,Appenders 和 Layouts
Logback基于三個主要類:Logger,Appender和Layout,這三種類型的組件協(xié)同工作,使開發(fā)人員能夠根據(jù)消息類型和級別記錄消息,并在運行時控制這些消息的格式和報告位置。
Logger類是logback-classic模塊的一部分,Appender和Layout接口是logback-core模塊的一部分。作為通用模塊,logback-core沒有日志記錄器的概念。
logger是日志記錄器,appender是追加器,layout是布局。
Logger 上下文
任何日志API相對于普通的System.out.println的最重要的優(yōu)勢是能夠禁用某些日志語句,同時允許其他語句不受阻礙地打印。該功能假定的日志空間是根據(jù)一些開發(fā)人員選擇的標準進行分類的。在logback-classic中,這種分類是logger的固有組成部分。每個logger都附加到一個LoggerContext,該上下文負責生成logger,并將它們安排在類似層次結構的樹中。
logger是命名實體。它們的名字區(qū)分大小寫,并遵循分層命名規(guī)則:
如果一個
logger的名稱后面跟著一個點,那么這個logger就是另一個logger的祖先。如果在其自身和后代logger之間沒有祖先,則該logger被稱為子logger的父。
例如,名稱為com.foo的logger是名稱為com.foo.Bar的logger的父,類似地,java是java.util的父,是java.util.Vector的祖先。開發(fā)人員都應該熟悉這種命名方案。
根logger位于logger層次結構的頂部。它的特殊之處在于,它一開始就是每個層次結構的一部分。與每個logger一樣,可以通過它的名稱獲取它,如下所示:
Logger rootLogger = LoggerFactory.getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME);
所有其他logger也可以通過org.slf4j.LoggerFactory類中的靜態(tài)方法getLogger()來獲取。該方法需要傳遞日志記錄器的名稱作為參數(shù)。下面列出了Logger接口中的一些基本方法。
package org.slf4j;
public interface Logger {
String ROOT_LOGGER_NAME = "ROOT";
// 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);
}
有效級別(級別繼承)
logger可以被分配級別,可以設置的級別有TRACE, DEBUG, INFO, WARN, ERROR,這些級別別定義在ch.qos.logback.classic.Level類中,該類是final修飾的,不能被子類化。
如果一個給定的logger沒有被分配一個級別,那么它將從其最近的祖先那里繼承一個級別。為了確保所有的logger最終都能繼承到一個級別,根logger有一個默認級別DEBUG。
下面是四個例子,根據(jù)級別繼承規(guī)則,使用各種指定的級別值和產(chǎn)生的有效(繼承)級別。
示例1
| Logger name | 指定級別 | 有效級別 |
|---|---|---|
| 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 name | 指定級別 | 有效級別 |
|---|---|---|
| root | ERROR | ERROR |
| X | INFO | INFO |
| X.Y | DEBUG | DEBUG |
| X.Y.Z | WARN | WARN |
示例2中,所有logger都有一個指定的級別值,級別繼承不起作用。
示例3
| Logger name | 指定級別 | 有效級別 |
|---|---|---|
| root | DEBUG | DEBUG |
| X | INFO | INFO |
| X.Y | none | INFO |
| X.Y.Z | ERROR | ERROR |
示例3中,日志記錄器root, X和X.Y.Z都有指定的級別,X.Y沒有指定級別,是從父日志記錄器X繼承的級別。
示例4
| Logger name | 指定級別 | 有效級別 |
|---|---|---|
| root | DEBUG | DEBUG |
| X | INFO | INFO |
| X.Y | none | INFO |
| X.Y.Z | none | INFO |
示例4中,日志記錄器root和X有指定的級別,X.Y和X.Y.Z沒有指定級別,從最近的有指定級別的父級X繼承級別值。
打印方法和基本選擇規(guī)則
根據(jù)定義,打印方法確定日志請求的級別。例如,如果L是一個logger實例,那么語句L. INFO(“..”)就是一個級別INFO的日志語句。
如果日志記錄請求的級別高于或等于其日志記錄程序的有效級別,則啟用日志記錄請求。否則,該請求將被禁用。如前所述,沒有指定級別的日志記錄器將從其最近的祖先那里繼承一個級別。這條規(guī)則是logback的核心。它規(guī)定各級的次序如下:
TRACE < DEBUG < INFO < WARN < ERROR
下面是一個基本選擇規(guī)則的例子。
package com.wangbo.cto.logback;
import ch.qos.logback.classic.Level;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* @date 2019/9/13 22:48
* @auther wangbo
*/
public class LogLevelTest {
public static void main(String[] args) {
//獲取一個名為“com.foo”的logger,為了能設置級別,轉換為ch.qos.logback.classic.Logger類型
ch.qos.logback.classic.Logger logger = (ch.qos.logback.classic.Logger) LoggerFactory.getLogger("com.foo");
//設置級別
logger.setLevel(Level.INFO);
//繼承最近的父com.foo的級別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.");
//info >= info,啟用此請求
barlogger.info("Located nearest gas station.");
//debug <= info,此請求已禁用
barlogger.debug("Exiting gas station search");
}
}
運行結果
22:59:44.139 [main] WARN com.foo - Low fuel level.
22:59:44.141 [main] INFO com.foo.Bar - Located nearest gas station.
獲取 Logger
調用LoggerFactory.getLogger,相同名稱的方法將始終返回相同Logger對象的引用。例如:
Logger x = LoggerFactory.getLogger("wombat");
Logger y = LoggerFactory.getLogger("wombat");
X和Y是相同的Logger對象。
因此,可以配置一個日志程序,然后在代碼的其他地方通過相同的名字獲取到相同的實例,而不需要傳遞引用。與生物學意義上的父母(父母總是先于子女)相反,logback日志記錄器可以按任何順序創(chuàng)建和配置。特別是,父logger將發(fā)現(xiàn)并鏈接到它的后代,即使它是在它們之后實例化的。
通常在應用程序初始化時配置logback環(huán)境。首選的方法是讀取配置文件。不久將討論這種方法。
以日志記錄器所在的類命名日志記錄器似乎是迄今為止所知的最佳通用策略。
Appenders 和 Layouts
根據(jù)日志程序選擇性地啟用或禁用日志記錄請求的功能只是一部分。Logback允許將日志請求打印到多個目的地。在logback中,輸出目的地稱為appender。目前,針對控制臺、文件、遠程套接字服務器、MySQL、PostgreSQL、Oracle和其他數(shù)據(jù)庫、JMS和遠程UNIX Syslog守護進程存在附加程序。
一個logger可以附加多個appender。
addAppender方法向給定的logger添加一個appender。對于給定的logger,每個啟用的日志請求都將被轉發(fā)到該logger中的所有appender以及層次結構中更高的appender。換句話說,appender是附加地從日志程序層次結構繼承的。例如,如果將控制臺appender添加到根logger,那么所有啟用的日志請求至少都將打印在控制臺上。此外,如果向logger(L)添加了一個文件appender,然后,為 L 和 L 的子節(jié)點啟用的日志記錄請求將打印在文件里和控制臺上。通過將logger的additivity flag設置為false,可以覆蓋此默認行為,使追加器積累不再是附加的。
下表是一個例子:
| Logger Name | 附加的 Appenders | Additivity Flag | 輸出目標 | 注釋 |
|---|---|---|---|---|
| root | A1 | 不適用 | A1 | 由于根日志程序位于日志程序層次結構的頂部,所以不應用加法標志。 |
| x | A-x1, A-x2 | true | A1, A-x1, A-x2 | 使用了 x 和 root 的追加器 |
| x.y | none | true | A1, A-x1, A-x2 | 使用了 x 和 root 的追加器 |
| x.y.z | A-xyz1 | true | A1, A-x1, A-x2, A-xyz1 | 使用了 x.y.z,x 和 root的追加器 |
| security | A-sec | false | A-sec | 由于可加性標志設置為 false,所以沒有追加器累加,只會使用一個追加器 A-sec |
| security.access | none | true | A-sec | 因為 security 中的可加性標志設置為 false,所以只使用 security 的追加器 A-sec |
通常,用戶不僅希望自定義輸出目的地,還希望自定義輸出格式??梢酝ㄟ^將layout與appender關聯(lián)來實現(xiàn)。layout負責根據(jù)用戶的意愿格式化日志請求,appender負責將格式化的輸出發(fā)送到它的目的地。PatternLayout是標準logback分發(fā)版的一部分,允許用戶根據(jù)類似于C語言printf函數(shù)的轉換模式指定輸出格式。
例如,PatternLayout設置為%-4relative [%thread] %-5level %logger{32} - %msg%n,將輸出類似于下面格式的內容:
176 [main] DEBUG manual.architecture.HelloWorld2 - Hello world.
第一個字段是自程序啟動以來經(jīng)過的毫秒數(shù)。第二個字段是發(fā)出日志請求的線程。第三個字段是日志請求的級別。第四個字段是與日志請求關聯(lián)的日志記錄器的名稱。'-'后面的文本是請求的消息。
參數(shù)化日志
考慮到logback-classic中的logger實現(xiàn)了SLF4J的Logger接口,某些打印方法允許多個參數(shù)。這些打印方法變體主要是為了提高性能,同時降低對代碼可讀性的影響。
普通寫法
對于一些logger,可以這樣寫:
logger.debug("Entry number: " + i + " is " + String.valueOf(entry[i]));
該參數(shù)將整數(shù) i 和 entry[i] 轉換為字符串,并連接中間的字符串。會導致構造消息參數(shù)的額外開銷,但是這與是否記錄消息沒有關系。
避免參數(shù)構造額外開銷的一種方法是用一個測試包圍 log 語句。比如這樣:
if(logger.isDebugEnabled()) {
logger.debug("Entry number: " + i + " is " + String.valueOf(entry[i]));
}
這樣,如果logger禁用了DEBUG級別,就不會產(chǎn)生參數(shù)構造的開銷。另一方面,如果logger啟用了DEBUG級別,系統(tǒng)將承擔兩次評估日志記錄器是否啟用的成本,一次是在debugEnabled,第二次是在debug,在實踐中,這種開銷是微不足道的,因為評估一個日志記錄器所需時間相對于實際記錄一個請求所需的時間不到1%。
推薦寫法
存在一種基于消息格式的替代方法。假設entry是一個對象,可以這樣寫:
Object entry = new SomeObject();
logger.debug("The entry is {}.", entry);
只有在評估是否進行日志記錄之后,并且只有在決定記錄日志的情況下,日志程序才會實現(xiàn)將消息格式化,并用條目的字符串值替換“{}”。換句話說,當禁用 log 語句時,這種寫法不會產(chǎn)生參數(shù)構造的成本。
下面兩行代碼將產(chǎn)生完全相同的輸出。然而,在禁用日志語句的情況下,第二種變體的性能至少比第一種變體好30倍。
logger.debug("The new entry is "+entry+".");
logger.debug("The new entry is {}.", entry);
還有一種雙參數(shù)變體。例如,你可以這樣寫:
logger.debug("The new entry is {}. It replaces {}.", entry, oldEntry);
如果需要傳遞三個或多個參數(shù),還可以使用Object[]變體。例如,你可以這樣寫:
Object[] paramArray = {newVal, below, above};
logger.debug("Value {} was inserted between {} and {}.", paramArray);
底層原理
在介紹了基本的logback組件之后,現(xiàn)在可以描述當用戶調用日志程序的打印方法時,logback框架所采取的步驟?,F(xiàn)在讓我們分析用戶調用名為com.wombat的日志記錄器的info()方法時,logback所采取的步驟。
1. 獲得過濾器鏈決策
如果存在,則調用TurboFilter鏈。Turbo 過濾器可以設置上下文范圍的閾值,或者根據(jù)與每個日志請求關聯(lián)的標記、級別、日志記錄器、消息或可拋出性等信息過濾掉某些事件。如果過濾器鏈的響應是拒絕FilterReply.DENY,則日志請求將被刪除。如果是中性FilterReply.NEUTRAL,然后我們繼續(xù)下一步,即第2步。如果是接受FilterReply.ACCEPT,我們跳過下一步,直接跳到步驟3。
2. 應用基本的選擇規(guī)則
在此步驟中,logback將日志記錄器的有效級別與請求的級別進行比較。如果根據(jù)此測試禁用日志記錄請求,那么logback將刪除該請求,而不進行進一步處理。否則,將繼續(xù)下一步。
3. 創(chuàng)建一個 LoggingEvent 對象
如果請求通過了前面的過濾器,logback將創(chuàng)建一個ch. qs .logback.classic.LoggingEvent對象,該對象包含請求的所有相關參數(shù),例如請求的日志記錄器,請求級別,消息本身,可能隨請求一起傳遞的異常、當前時間、當前線程、發(fā)出日志記錄請求的類的各種數(shù)據(jù)以及 MDC。注意,其中一些字段是延遲初始化的,只有在實際需要時才會這樣做。MDC 用于用附加的上下文信息裝飾日志記錄請求。MDC將在下一章中討論。
4. 調用 appenders
創(chuàng)建 LoggingEvent對象之后,logback將調用所有適用的appender的doAppend()方法,即從日志程序上下文中繼承的appender。
logback發(fā)行版附帶的所有附加程序都擴展了AppenderBase抽象類,該類在確保線程安全的同步塊中實現(xiàn)doAppend方法。如果存在附加的自定義過濾器,AppenderBase的doAppend()方法也能調用??梢詣討B(tài)附加到任何附加器的自定義過濾器將在單獨的一章中介紹。
5. 格式化輸出
被調用的附加程序負責格式化日志事件。然而,一些(但不是所有)附加程序將格式化日志事件的任務委托給了layout,布局可以格式化LoggingEvent實例并以字符串的形式返回結果。注意,有些附加程序,如SocketAppender,不將日志事件轉換為字符串,而是序列化它。因此,它們沒有也不需要布局。
6. 發(fā)送 LoggingEvent
日志事件完全格式化后,由每個附加程序將其發(fā)送到目的地。下面是一個序列 UML 圖,展示了所有事情是如何工作的。
