【萬字長文】帶你了解日志的前世今生

日志就像車輛保險(xiǎn),沒人愿意為保險(xiǎn)付錢,但是一旦出了問題誰都又想有保險(xiǎn)可用

日志的作用和目的

日志文件

日志文件是用于記錄系統(tǒng)操作事件的文件集合,可以分為事件日志和消息日志。具有處理歷史數(shù)據(jù)、診斷問題的追蹤以及理解系統(tǒng)的活動(dòng)等重要作用。

在計(jì)算機(jī)中,日志文件是一個(gè)記錄了發(fā)生在運(yùn)行中的操作系統(tǒng)或者其他軟件中的事件的文件,或者記錄了在網(wǎng)絡(luò)聊天軟件的用戶之間發(fā)送的消息。日志記錄是指保存日志的行為。最簡單的做法的將日志寫入單個(gè)存放日志的文件。

為什么要打印日志

為什么要打印日志,或者什么時(shí)候打印日志這取決于打印的目的。不同的打印目的決定了日志輸出的格式,輸出的位置以及輸出的頻率

  1. 調(diào)試開發(fā):目的是開發(fā)調(diào)試程序時(shí)使用,只應(yīng)該出現(xiàn)在開發(fā)周期內(nèi),而不應(yīng)該在線上系統(tǒng)輸出
  2. 用戶行為日志:記錄用戶操作行為,多用于大數(shù)據(jù)分析,如監(jiān)控、風(fēng)控、推薦等等
  3. 程序運(yùn)行日志:記錄程序運(yùn)行時(shí)情況,特別是非預(yù)期的行為,異常情況,主要是開發(fā)維護(hù)使用
  4. 機(jī)器日志:主要是記錄網(wǎng)絡(luò)請(qǐng)求、系統(tǒng)CPU、內(nèi)存、IO使用等情況,供運(yùn)維或者監(jiān)控使用

日志中應(yīng)該包含什么

利用4W1H進(jìn)行分析

  • When:打印日志的時(shí)間戳,此時(shí)的時(shí)間應(yīng)該是日志記錄的事情發(fā)生的時(shí)間,具體的時(shí)間可以幫助我們分析時(shí)間發(fā)生的時(shí)間點(diǎn)
  • Where:日志在哪里被記錄,具體哪個(gè)模塊,記錄到哪個(gè)文件,哪個(gè)函數(shù),哪一行代碼
  • What:日志的主體是什么,簡明扼要描述日志記錄的事情
  • Who:事件生產(chǎn)者的唯一標(biāo)識(shí),以訂單為例就是訂單id,當(dāng)然也可以是某個(gè)動(dòng)作的聲明
  • How:日志的重要程度分級(jí),一般以ERROR > WARNNING > INFO > DEBUG > TRACE來劃分重要程度

Java日志的前世今生

為什么要用日志框架

軟件系統(tǒng)發(fā)展到現(xiàn)在已經(jīng)非常復(fù)雜了,特別是在服務(wù)器端軟件,涉及到的知識(shí)以及內(nèi)容問題太多。在某些方面使用別人成熟的框架,就相當(dāng)于讓別人幫你完成一些基礎(chǔ)工作,你只需要集中精力完成系統(tǒng)的業(yè)務(wù)邏輯設(shè)計(jì)。而且框架一般是成熟、穩(wěn)健的,他可以幫助你處理很多細(xì)節(jié)的問題,比如日志的異步處理、動(dòng)態(tài)控制等等問題。還有框架一般都是經(jīng)過很多人使用,所以結(jié)構(gòu)性、擴(kuò)展性都非常好。

現(xiàn)有的日志框架

按照日志門面和日志實(shí)現(xiàn)劃分的話現(xiàn)有的Java日志框架有以下幾種

  • 日志門面:JCL、Slf4j
  • 日志實(shí)現(xiàn):JUL、Logback、Log4j、Log4j2

[圖片上傳失敗...(image-a58ca1-1617936906298)]

為什么要有日志門面

當(dāng)我們的系統(tǒng)變得更加復(fù)雜的時(shí)候,我們的日志就容易發(fā)生混亂。隨著系統(tǒng)開發(fā)的進(jìn)行,可能會(huì)更新不同的日志框架,造成當(dāng)前系統(tǒng)中存在不同的日志依賴,讓我們難以統(tǒng)一的管理和控制。就算我們強(qiáng)制要求了我們公司內(nèi)開發(fā)的項(xiàng)目使用了相同的日志框架,但是系統(tǒng)中會(huì)引用其他類似Spring或者M(jìn)ybatis等等的第三方框架,它們依賴于我們規(guī)定不同的日志框架,而且他們自身的日志系統(tǒng)就有著不一致性,依然會(huì)出現(xiàn)日志體系的混亂。

所以借鑒JDBC的思想,為日志系統(tǒng)也提供一套門面,那么我們就可以面向這些接口規(guī)范來開發(fā),避免直接依賴具體的日志框架。這樣我們的系統(tǒng)在日志中就存在了日志的門面和日志的實(shí)現(xiàn)。

日志門面的日志實(shí)現(xiàn)的關(guān)系

[圖片上傳失敗...(image-874b22-1617936906299)]

Log4j

Apache Log4j 是一種基于 Java 的日志記錄工具,它是 Apache 軟件基金會(huì)的一個(gè)項(xiàng)目。在 jdk1.3 之前,還沒有現(xiàn)成的日志框架,Java 工程師只能使用原始的 System.out.println(), System.err.println() 或者 e.printStackTrace()。通過把 debug 日志寫到 StdOut 流,錯(cuò)誤日志寫到 ErrOut 流,以此記錄應(yīng)用程序的運(yùn)行狀態(tài)。這種原始的日志記錄方式缺陷明顯,不僅無法實(shí)現(xiàn)定制化,而且日志的輸出粒度不夠細(xì)。鑒于此,1999 年,大牛 Ceki Gülcü 創(chuàng)建了 Log4j 項(xiàng)目,并幾乎成為了 Java 日志框架的實(shí)際標(biāo)準(zhǔn)。

JUL

Log4j 作為 Apache 基金會(huì)的一員,Apache 希望將 Log4j 引入 jdk,不過被 sun 公司拒絕了。隨后,sun 模仿 Log4j,在 jdk1.4 中引入了 JUL(java.util.logging)。

JCL

為了解耦日志接口與實(shí)現(xiàn),2002 年 Apache 推出了 JCL(Jakarta Commons Logging),也就是 Commons Logging。Commons Logging 定義了一套日志接口,具體實(shí)現(xiàn)則由 Log4j 或 JUL 來完成。Commons Logging 基于動(dòng)態(tài)綁定來實(shí)現(xiàn)日志的記錄,在使用時(shí)只需要用它定義的接口編碼即可,程序運(yùn)行時(shí)會(huì)使用 ClassLoader 尋找和載入底層的日志庫,因此可以自由選擇由 log4j 或 JUL 來實(shí)現(xiàn)日志功能。

SlF4j和Logback

大牛 Ceki Gülcü 與 Apache 基金會(huì)關(guān)于 Commons-Logging 制定的標(biāo)準(zhǔn)存在分歧,后來,Ceki Gülcü 離開 Apache 并先后創(chuàng)建了 Slf4j 和 Logback 兩個(gè)項(xiàng)目。Slf4j 是一個(gè)日志門面,只提供接口,可以支持 Logback、JUL、Log4j 等日志實(shí)現(xiàn),Logback 提供具體的實(shí)現(xiàn),它相較于 log4j 有更快的執(zhí)行速度和更完善的功能。

Log4j2

為了維護(hù)在 Java 日志江湖的地位,防止 JCL、Log4j 被 Slf4j、Logback 組合取代 ,2014 年 Apache 推出了 Log4j 2。Log4j 2 與 Log4j 不兼容,經(jīng)過大量深度優(yōu)化,其性能顯著提升。

各個(gè)日志框架原理簡介及介紹

Log4j

Log4j是Apache下的一款開源的日志框架,通過在項(xiàng)目中使用Log4j,我們可以控制日志信息輸出到控制臺(tái)、文件、甚至是數(shù)據(jù)庫中。我們可以控制每一條日志的輸出格式,通過定義日志的輸出級(jí)別,可以更加靈活方便的控制日志的輸出過程。

Log4j的官方網(wǎng)站:http://logging.apache.org/log4j/1.2/

如果要在項(xiàng)目中使用Log4j的話需要引入相應(yīng)的Jar包

<dependency>
    <groupId>log4j</groupId>
    <artifactId>log4j</artifactId>
    <version>1.2.17</version>
</dependency>

Log4j主要是由Loggers、Appenders、和Layout組成

Loggers

Loggers主要負(fù)責(zé)處理日志記錄,Loggers的命名有繼承的機(jī)制,例如名稱為com.test.log的logger會(huì)繼承名稱為com.test的logger。

Log4j中有一個(gè)特殊的logger叫作“root”,他是所有l(wèi)ogger的根,也就是意味著其他所有的logger都會(huì)直接或者間接的繼承自root。

Appenders

Appender用來指定日志輸出到哪個(gè)地方,可以同時(shí)指定多個(gè)日志的輸出目的地。Log4j的輸出目的地有以下集中。

輸出端類型 作用
ConsoleAppender 將日志輸出到控制臺(tái)
FileAppender 將日志輸出到文件
DailyRollingFileAppender 將日志輸出到一個(gè)日志文件,并且每天輸出到一個(gè)新的文件
RollingFileAppender 將日志信息輸出到日志文件,并且按照指定文件的尺寸,當(dāng)文件大小達(dá)到指定尺寸時(shí),會(huì)自動(dòng)將文件改名,同時(shí)生成一個(gè)新的文件
JDBCAppender 把日志信息保存到數(shù)據(jù)庫中

Layouts

Layouts用于控制日志輸出內(nèi)容的格式,讓我們可以使用各種需要的格式輸出日志。Log4j常用的Layouts:

格式化器類型 作用
HTMLLayout 格式化日志輸出為HTML表格形式
SimpleLayout 簡單的日志輸出格式化
PatternLayout 可以根據(jù)自定義格式輸出日志
* log4j 采用類似 C 語言的 printf 函數(shù)的打印格式格式化日志信息,具體的占位符及其含義如下:
        %m 輸出代碼中指定的日志信息
        %p 輸出優(yōu)先級(jí),及 DEBUG、INFO 等
        %n 換行符(Windows平臺(tái)的換行符為 "\n",Unix 平臺(tái)為 "\n")
        %r 輸出自應(yīng)用啟動(dòng)到輸出該 log 信息耗費(fèi)的毫秒數(shù)
        %c 輸出打印語句所屬的類的全名
        %t 輸出產(chǎn)生該日志的線程全名
        %d 輸出服務(wù)器當(dāng)前時(shí)間,默認(rèn)為 ISO8601,也可以指定格式,如:%d{yyyy年MM月dd日
        HH:mm:ss}
        %l 輸出日志時(shí)間發(fā)生的位置,包括類名、線程、及在代碼中的行數(shù)。如:
        Test.main(Test.java:10)
        %F 輸出日志消息產(chǎn)生時(shí)所在的文件名稱
        %L 輸出代碼中的行號(hào)
        %% 輸出一個(gè) "%" 字符
* 可以在 % 與字符之間加上修飾符來控制最小寬度、最大寬度和文本的對(duì)其方式。如:
        %5c 輸出category名稱,最小寬度是5,category<5,默認(rèn)的情況下右對(duì)齊
        %-5c 輸出category名稱,最小寬度是5,category<5,"-"號(hào)指定左對(duì)齊,會(huì)有空格
        %.5c 輸出category名稱,最大寬度是5,category>5,就會(huì)將左邊多出的字符截掉,<5不會(huì)有空格
        %20.30c category名稱<20補(bǔ)空格,并且右對(duì)齊,>30字符,就從左邊交遠(yuǎn)銷出的字符截掉

JUL

JUL全稱是Java util Logging,是Java原生的日志框架,使用時(shí)不需要另外引用第三方類庫,相對(duì)其他日志框架使用方便,學(xué)習(xí)簡單,能夠在小型應(yīng)用中靈活使用。

JUL的架構(gòu)

[圖片上傳失敗...(image-ff6a35-1617936906299)]

  • Logger:被稱為記錄器,應(yīng)用程序通過獲取Logger對(duì)象,調(diào)用其API來發(fā)布日志信息。Logger通常是應(yīng)用程序訪問日志系統(tǒng)的入口程序
  • Handler(和Log4j的Appenders類似):每個(gè)Logger都會(huì)被關(guān)聯(lián)一組Handlers,Logger會(huì)將日志交給關(guān)聯(lián)的Handlers處理。此Handler是一個(gè)抽象,其具體的實(shí)現(xiàn)決定了日志記錄的位置可以是控制臺(tái)、文件、數(shù)據(jù)庫等等
  • Layouts:也被稱為Formatters,它負(fù)責(zé)對(duì)日志進(jìn)行格式化的處理,Layouts決定了數(shù)據(jù)在一條日志記錄中的最終形式
  • Filters:過濾器,根據(jù)需要定制哪些信息會(huì)被記錄

總結(jié)一下就是用戶使用Logger來進(jìn)行日志的記錄,Logger持有若干個(gè)Handler,日志的輸出操作是由Handler來完成的,在Handler輸出之前會(huì)通過自定義的Filter過濾規(guī)則過濾掉不需要輸出的信息,最終由Handler決定使用什么樣的Layout將日志格式化處理并決定輸出到什么地方去。

接下來就寫一個(gè)簡單的入門案例看一下JUL是如何進(jìn)行日志處理的

JUL日志處理無需引用任何日志框架,是Java自帶的功能

// 1.獲取日志記錄器對(duì)象
Logger logger = Logger.getLogger("com.macaque.JulLogTest");
// 關(guān)閉系統(tǒng)默認(rèn)配置
logger.setUseParentHandlers(false);
// 自定義配置日志級(jí)別
// 創(chuàng)建ConsolHhandler 控制臺(tái)輸出
ConsoleHandler consoleHandler = new ConsoleHandler();
// 創(chuàng)建簡單格式轉(zhuǎn)換對(duì)象
SimpleFormatter simpleFormatter = new SimpleFormatter();
// 進(jìn)行關(guān)聯(lián)
consoleHandler.setFormatter(simpleFormatter);
logger.addHandler(consoleHandler);
// 配置日志具體級(jí)別
logger.setLevel(Level.ALL);
consoleHandler.setLevel(Level.ALL);
logger.severe("severe");
logger.warning("waring");
logger.info("info");
logger.config("config");
logger.fine("fine");
logger.finer("finer");
logger.finest("finest");

JCL

全稱為Jakarta Commons Logging,是由Apache提供的一個(gè)通用的日志API。

它的目標(biāo)是“為所有的Java日志實(shí)現(xiàn)”提供一個(gè)統(tǒng)一的接口,它自身也提供一個(gè)日志實(shí)現(xiàn),但是功能非常弱(SimpleLog)。所以一般不單獨(dú)使用JCL。他允許開發(fā)人員使用不同的日志實(shí)現(xiàn)工具:Log4j、JDK自帶的日志(JUL)

JCL有兩個(gè)基本的抽象類:Log和LogFactory

[圖片上傳失敗...(image-68dac2-1617936906299)]

如何使用

如果要在項(xiàng)目中使用JCL則要引入相應(yīng)的jar包

<dependency>
    <groupId>commons-logging</groupId>
    <artifactId>commons-logging</artifactId>
    <version>1.2</version>
</dependency>

這只是引入了相應(yīng)的日志門面,具體的日志實(shí)現(xiàn)還需要自己引入。

原理介紹

在使用JCL打印日志的時(shí)候是通過調(diào)用其LogFactory動(dòng)態(tài)加載Log的實(shí)現(xiàn)類

Log log = LogFactory.getLog(xxxx.class);

[圖片上傳失敗...(image-c243fc-1617936906299)]

然后在初始化的時(shí)候通過遍歷數(shù)組進(jìn)行查找有沒有符合的實(shí)現(xiàn)類,遍歷的數(shù)組初始化是

/**
 * The names of classes that will be tried (in order) as logging
 * adapters. Each class is expected to implement the Log interface,
 * and to throw NoClassDefFound or ExceptionInInitializerError when
 * loaded if the underlying logging library is not available. Any
 * other error indicates that the underlying logging library is available
 * but broken/unusable for some reason.
 */
private static final String[] classesToDiscover = {
        "org.apache.commons.logging.impl.Log4JLogger",
        "org.apache.commons.logging.impl.Jdk14Logger",
        "org.apache.commons.logging.impl.Jdk13LumberjackLogger",
        "org.apache.commons.logging.impl.SimpleLog"
};

遍歷這個(gè)數(shù)組的邏輯

for(int i=0; i<classesToDiscover.length && result == null; ++i) {
    result = createLogFromClass(classesToDiscover[i], logCategory, true);
}

SlF4j

簡單日志門面(Simple Logging Facade For Java)SlF4j主要是為了給Java日志訪問提供一套標(biāo)準(zhǔn)、規(guī)范的API框架,其主要意義在于提供接口,具體的實(shí)現(xiàn)可以交由其他日志框架。對(duì)于一般的Java項(xiàng)目而言,日志框架會(huì)選擇Slf4j-api作為門面,配上具體的實(shí)現(xiàn)框架,中間使用橋接器進(jìn)行橋接。

官方網(wǎng)站:http://www.slf4j.org/

Slf4j是目前市面上最流行的日志門面,其主要提供兩大功能:

  • 日志框架的綁定
  • 日志框架的橋接

日志的綁定

Slf4j支持各種日志框架,而Slf4j只是作為一個(gè)日志門面的存在,定義一個(gè)日志的打印規(guī)范,那么就會(huì)有兩種情況,針對(duì)這兩種情況引入包的類別略有不同。

  1. 遵守Slf4j定義的規(guī)范:如果是遵守了Slf4j定義的日志規(guī)范的話,那么只需要引入兩個(gè)包,一個(gè)是Slf4j的依賴,以及遵守了其規(guī)范的日志jar包實(shí)現(xiàn)即可
  2. 沒遵守Slf4j定義的規(guī)范:如果未遵守Slf4j定義的日志規(guī)范,那么需要引入三個(gè)包,一個(gè)是Slf4j的依賴,一個(gè)是適配器的包,一個(gè)是未遵守Slf4j定義的日志規(guī)范的包.

這是官網(wǎng)上給出的一張圖,描述的就是其綁定的過程。

[圖片上傳失敗...(image-ce8400-1617936906299)]

日志綁定底層原理簡介

在上面介紹的JCL的底層綁定原理我們了解到JCL是通過輪詢的機(jī)制進(jìn)行啟動(dòng)時(shí)檢測(cè)綁定的日志實(shí)現(xiàn),但是在Slf4j中不一樣,我們可以從LoggerFactory.getLogger方法中進(jìn)行入手查看,最終定位到LoggerFactory的findPossibleStaticLoggerBinderPathSet方法,具體如下。

private static String STATIC_LOGGER_BINDER_PATH = "org/slf4j/impl/StaticLoggerBinder.class";
static Set<URL> findPossibleStaticLoggerBinderPathSet() {
    Set<URL> staticLoggerBinderPathSet = new LinkedHashSet<URL>();
    try {
        ClassLoader loggerFactoryClassLoader = LoggerFactory.class.getClassLoader();
        Enumeration<URL> paths;
        if (loggerFactoryClassLoader == null) {
            paths = ClassLoader.getSystemResources(STATIC_LOGGER_BINDER_PATH);
        } else {
        // 這一處是重點(diǎn),通過類加載器找到所有org/slf4j/impl/StaticLoggerBinder.class的類
            paths = loggerFactoryClassLoader.getResources(STATIC_LOGGER_BINDER_PATH);
        }
        while (paths.hasMoreElements()) {
            URL path = paths.nextElement();
            staticLoggerBinderPathSet.add(path);
        }
    } catch (IOException ioe) {
        Util.report("Error getting resources from path", ioe);
    }
    return staticLoggerBinderPathSet;
}

所以其加載過程簡單如下

  1. Slf4j通過LoggerFactory加載日志的具體實(shí)現(xiàn)
  2. LoggerFactory在初始化的過程中,會(huì)通過performInitialization()方法綁定具體的日志實(shí)現(xiàn)
  3. 在綁定具體實(shí)現(xiàn)的時(shí)候,通過類加載器,加載org/slf4j/impl/StaticLoggerBinder.class類
  4. 所以,只要是一個(gè)日志實(shí)現(xiàn)框架,在org.slf4j.impl包中提供一個(gè)自己的StaticLoggerBinder類,在其中提供具體日志實(shí)現(xiàn)的LoggerFactory就可以被Slf4j進(jìn)行加載管理了

日志框架的橋接

在一些老項(xiàng)目中有可能一開始使用的不是Slf4j框架,如果在這時(shí)想要進(jìn)行日志的升級(jí),那么Slf4j也提供了這樣的功能,提供了相應(yīng)的橋接器進(jìn)行對(duì)原有日志框架的替換,下圖是官網(wǎng)所表示的如何進(jìn)行的日志橋接。其實(shí)簡單來說就是將原有的日志重定向到Slf4j然后交由Slf4j進(jìn)行管理。

[圖片上傳失敗...(image-8f1b9d-1617936906299)]

有可能看圖不好理解橋接的意思,我們直接使用例子來演示一下Slf4j是如何替換原有的日志框架的。

首先我們建立一個(gè)項(xiàng)目首先使用Log4j進(jìn)行打印日志,引入Log4j的jar包

<dependency>
    <groupId>log4j</groupId>
    <artifactId>log4j</artifactId>
    <version>1.2.17</version>
</dependency>

然后簡單加入Log4j的配置進(jìn)行打印日志

@Test
public void testLog4jToSlf4j(){
    Logger logger = Logger.getLogger(TestSlf4jBridge.class);
    logger.info("testLog4jToSlf4j");
}

控制臺(tái)的輸出如下,因?yàn)闆]有做日志格式的處理,所以只是簡單輸出了字符串。

[圖片上傳失敗...(image-767fca-1617936906299)]

接下來我們要在不改動(dòng)一點(diǎn)代碼的情況,只是加入和移除一些依賴包就可以完成日志框架的升級(jí),我們這里假設(shè)要升級(jí)為Logback,按照以下步驟進(jìn)行即可。

  1. 移除原有的日志框架(這里就是Log4j的日志框架)
  2. 移除了原有日志框架,代碼肯定報(bào)錯(cuò)了,所以再添加Log4j的日志橋接器
  3. 加入Slf4j-api的依賴
  4. 再加入Logback的日志實(shí)現(xiàn)依賴

完成這四步以后,日志框架就完成了升級(jí),接下來我們看一下效果,這里在Logback的日志輸出中加入了格式的處理。能看到日志已經(jīng)是由Logback打印出來了。

[圖片上傳失敗...(image-7e1633-1617936906299)]

Logback

Logback是由Log4j的創(chuàng)始人設(shè)計(jì)的另一款開源日志組件,性能比Log4j性能要好,官方網(wǎng)站:https://logback.qos.ch/index.html

Logback主要分為三個(gè)模塊

  • logback-core:其他兩個(gè)模塊的基礎(chǔ)模塊
  • logback-classic:它是Log4j的一個(gè)改良版本,同時(shí)它完整實(shí)現(xiàn)了Slf4j的API
  • logback-access:訪問模塊與Servlet容器集成,通過Http來訪問日志的功能

后續(xù)的日志都是通過Slf4j日志門面搭建日志系統(tǒng),所以在代碼是沒有什么區(qū)別的,主要是通過改變配置文件和pom依賴。

pom依賴

<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-api</artifactId>
    <version>1.7.26</version>
</dependency>
<dependency>
    <groupId>ch.qos.logback</groupId>
    <artifactId>logback-classic</artifactId>
    <version>1.2.3</version>
</dependency>

基本配置

logback會(huì)依次讀取以下類型配置文件:

  • logback.groovy
  • logabck-test.xml
  • logback.xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!--
日志輸出格式:
%-5level
%d{yyyy-MM-dd HH:mm:ss.SSS}日期
%c類的完整名稱
%M為method
%L為行號(hào)
%thread線程名稱
%m或者%msg為信息
%n換行
-->
<!--格式化輸出:%d表示日期,%thread表示線程名,%-5level:級(jí)別從左顯示5個(gè)字符寬度
%msg:日志消息,%n是換行符-->
<property name="pattern" value="%d{yyyy-MM-dd HH:mm:ss.SSS} %c [%thread]
%-5level %msg%n"/>
<!--
Appender: 設(shè)置日志信息的去向,常用的有以下幾個(gè)
ch.qos.logback.core.ConsoleAppender (控制臺(tái))
3. FileAppender配置
ch.qos.logback.core.rolling.RollingFileAppender (文件大小到達(dá)指定尺
寸的時(shí)候產(chǎn)生一個(gè)新文件)
ch.qos.logback.core.FileAppender (文件)
-->
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
<!--輸出流對(duì)象 默認(rèn) System.out 改為 System.err-->
<target>System.err</target>
<!--日志格式配置-->
<encoder
class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>${pattern}</pattern>
</encoder>
</appender>
<!--
用來設(shè)置某一個(gè)包或者具體的某一個(gè)類的日志打印級(jí)別、以及指定<appender>。
<loger>僅有一個(gè)name屬性,一個(gè)可選的level和一個(gè)可選的addtivity屬性
name:
用來指定受此logger約束的某一個(gè)包或者具體的某一個(gè)類。
level:
用來設(shè)置打印級(jí)別,大小寫無關(guān):TRACE, DEBUG, INFO, WARN, ERROR, ALL 和
OFF,
如果未設(shè)置此屬性,那么當(dāng)前l(fā)ogger將會(huì)繼承上級(jí)的級(jí)別。
additivity:
是否向上級(jí)loger傳遞打印信息。默認(rèn)是true。
<logger>可以包含零個(gè)或多個(gè)<appender-ref>元素,標(biāo)識(shí)這個(gè)appender將會(huì)添加到這個(gè)
logger
-->
<!--
也是<logger>元素,但是它是根logger。默認(rèn)debug
level:用來設(shè)置打印級(jí)別,大小寫無關(guān):TRACE, DEBUG, INFO, WARN, ERROR, ALL
和 OFF,
<root>可以包含零個(gè)或多個(gè)<appender-ref>元素,標(biāo)識(shí)這個(gè)appender將會(huì)添加到這個(gè)
logger。
-->
<root level="ALL">
<appender-ref ref="console"/>
</root>
</configuration>

FileAppender配置

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!-- 自定義屬性 可以通過${name}進(jìn)行引用-->
<property name="pattern" value="[%-5level] %d{yyyy-MM-dd HH:mm:ss} %c %M
%L [%thread] %m %n"/>
      <!--
        日志輸出格式:
        %d{pattern}日期
        %m或者%msg為信息
        %M為method
        %L為行號(hào)
        %c類的完整名稱
        %thread線程名稱
        %n換行
        %-5level
      -->
<!-- 日志文件存放目錄 -->
<property name="log_dir" value="d:/logs"></property>
<!--控制臺(tái)輸出appender對(duì)象-->
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
<!--輸出流對(duì)象 默認(rèn) System.out 改為 System.err-->
<target>System.err</target>
<!--日志格式配置-->
<encoder
class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>${pattern}</pattern>
</encoder>
</appender>
<!--日志文件輸出appender對(duì)象-->
<appender name="file" class="ch.qos.logback.core.FileAppender">
<!--日志格式配置-->
<encoder
class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>${pattern}</pattern>
</encoder>
<!--日志輸出路徑-->
<file>${log_dir}/logback.log</file>
</appender>
<!-- 生成html格式appender對(duì)象 -->
<appender name="htmlFile" class="ch.qos.logback.core.FileAppender">
<!--日志格式配置-->
<encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder">
<layout class="ch.qos.logback.classic.html.HTMLLayout">
<pattern>%level%d{yyyy-MM-dd
HH:mm:ss}%c%M%L%thread%m</pattern>
</layout>
</encoder>
<!--日志輸出路徑-->
<file>${log_dir}/logback.html</file>
</appender>
<!--RootLogger對(duì)象-->
<root level="all">
<appender-ref ref="console"/>
<appender-ref ref="file"/>
<appender-ref ref="htmlFile"/>
</root>
</configuration>

RollingFileAppender配置

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!-- 自定義屬性 可以通過${name}進(jìn)行引用-->
<property name="pattern" value="[%-5level] %d{yyyy-MM-dd HH:mm:ss} %c %M
%L [%thread] %m %n"/>
      <!--
        日志輸出格式:
        %d{pattern}日期
        %m或者%msg為信息
        %M為method
        %L為行號(hào)
        %c類的完整名稱
        %thread線程名稱
        %n換行
        %-5level
      -->
<!-- 日志文件存放目錄 -->
<property name="log_dir" value="d:/logs"></property>
<!--控制臺(tái)輸出appender對(duì)象-->
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
  <!--輸出流對(duì)象 默認(rèn) System.out 改為 System.err-->
  <target>System.err</target>
  <!--日志格式配置-->
  <encoder
  class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
  <pattern>${pattern}</pattern>
  </encoder>
</appender>
<!-- 日志文件拆分和歸檔的appender對(duì)象-->
<appender name="rollFile"
class="ch.qos.logback.core.rolling.RollingFileAppender">
  <!--日志格式配置-->
  <encoder
  class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
  <pattern>${pattern}</pattern>
  </encoder>
  <!--日志輸出路徑-->
  <file>${log_dir}/roll_logback.log</file>
  <!--指定日志文件拆分和壓縮規(guī)則-->
  <rollingPolicy
    class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
    <!--通過指定壓縮文件名稱,來確定分割文件方式-->
    <fileNamePattern>${log_dir}/rolling.%d{yyyy-MMdd}.
    log%i.gz</fileNamePattern>
    <!--文件拆分大小-->
    <maxFileSize>1MB</maxFileSize>
  </rollingPolicy>
</appender>
<!--RootLogger對(duì)象-->
<root level="all">
  <appender-ref ref="console"/>
  <appender-ref ref="rollFile"/>
</root>
</configuration>

Filter和異步日志配置

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!-- 自定義屬性 可以通過${name}進(jìn)行引用-->
<property name="pattern" value="[%-5level] %d{yyyy-MM-dd HH:mm:ss} %c %M
%L [%thread] %m %n"/>
<!--
日志輸出格式:
%d{pattern}日期
%m或者%msg為信息
%M為method
%L為行號(hào)
%c類的完整名稱
%thread線程名稱
%n換行
%-5level
-->
<!-- 日志文件存放目錄 -->
<property name="log_dir" value="d:/logs/"></property>
<!--控制臺(tái)輸出appender對(duì)象-->
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
<!--輸出流對(duì)象 默認(rèn) System.out 改為 System.err-->
<target>System.err</target>
<!--日志格式配置-->
<encoder
class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>${pattern}</pattern>
</encoder>
</appender>
<!-- 日志文件拆分和歸檔的appender對(duì)象-->
<appender name="rollFile"
class="ch.qos.logback.core.rolling.RollingFileAppender">
<!--日志格式配置-->
<encoder
class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>${pattern}</pattern>
</encoder>
<!--日志輸出路徑-->
<file>${log_dir}roll_logback.log</file>
<!--指定日志文件拆分和壓縮規(guī)則-->
<rollingPolicy
class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<!--通過指定壓縮文件名稱,來確定分割文件方式-->
<fileNamePattern>${log_dir}rolling.%d{yyyy-MMdd}.
log%i.gz</fileNamePattern>
<!--文件拆分大小-->
<maxFileSize>1MB</maxFileSize>
</rollingPolicy>
<!--filter配置-->
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<!--設(shè)置攔截日志級(jí)別-->
<level>error</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
</appender>
<!--異步日志-->
<appender name="async" class="ch.qos.logback.classic.AsyncAppender">
<appender-ref ref="rollFile"/>
</appender>
<!--RootLogger對(duì)象-->
<root level="all">
<appender-ref ref="console"/>
<appender-ref ref="async"/>
</root>
<!--自定義logger additivity表示是否從 rootLogger繼承配置-->
<logger name="com.macaque" level="debug" additivity="false">
<appender-ref ref="console"/>
</logger>
</configuration>

Log4j轉(zhuǎn)向Logback

官方提供了Log4j.properties轉(zhuǎn)換成logback.xml文件配置的工具:http://logback.qos.ch/translator/

Log4j2

Apache Log4j2是對(duì)Log4j的升級(jí)版,參考了logback的一些優(yōu)秀設(shè)計(jì),并且修復(fù)了一些問題帶來了一些重大的提升,主要有:

  • 異常處理:在logback中Appender中的異常不會(huì)被應(yīng)用感知到,但是在log4j2中提供了一些異常的處理機(jī)制
  • 性能提升,log4j2相較于log4j和logback都具有很明顯的性能提升,后面會(huì)有官方的測(cè)試數(shù)據(jù)
  • 自動(dòng)重載配置,參考了logback的配置,當(dāng)然會(huì)提供自動(dòng)刷新參數(shù)配置,最實(shí)用的就是在我們生產(chǎn)環(huán)境中動(dòng)態(tài)的修改日志的級(jí)別而不需要重啟應(yīng)用
  • 無垃圾機(jī)制,log4j在大部分情況下,都可以使用其設(shè)計(jì)的一套無垃圾機(jī)制,避免頻繁的日志收集導(dǎo)致的jvm gc

官方網(wǎng)站:https://logging.apache.org/log4j/2.x/

如何使用Log4j2

目前市面上最主流的日志門面是Slf4j,雖然本身Log4j2也是日志門面,因?yàn)樗娜罩緦?shí)現(xiàn)功能非常強(qiáng)大,性能優(yōu)越。所以大家一般還是將Log4j2看作是日志額實(shí)現(xiàn),Slf4j+Log4j2應(yīng)該是未來的大勢(shì)所趨。

添加依賴(配合Slf4j進(jìn)行使用)

<!--使用slf4j作為日志的門面,使用log4j2來記錄日志 -->
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-api</artifactId>
    <version>1.7.25</version>
</dependency>
<!--為slf4j綁定日志實(shí)現(xiàn) log4j2的適配器 -->
<dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-slf4j-impl</artifactId>
    <version>2.10.0</version>
</dependency>
<!-- Log4j2 門面API-->
<dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-api</artifactId>
    <version>2.11.1</version>
</dependency>
<!-- Log4j2 日志實(shí)現(xiàn) -->
<dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-core</artifactId>
    <version>2.11.1</version>
</dependency>

Log4j2的配置

Log4j2的配合Logback的配置特別一樣

<?xml version="1.0" encoding="UTF-8"?>
<!--
    status="warn" 日志框架本身的輸出日志級(jí)別
    monitorInterval="5" 自動(dòng)加載配置文件的間隔時(shí)間,不低于 5 秒
-->
<Configuration status="debug" monitorInterval="5">
    <!--
        集中配置屬性進(jìn)行管理
        使用時(shí)通過:${name}
    -->
    <properties>
        <property name="LOG_HOME">/logs</property>
    </properties>
    <!--日志處理-->
    <Appenders>
        <!--控制臺(tái)輸出 appender-->
        <Console name="Console" target="SYSTEM_ERR">
            <PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] [%-5level] %c{36}:%L --- %m%n" />
        </Console>
        <!--日志文件輸出 appender-->
        <File name="file" fileName="${LOG_HOME}/myfile.log">
            <PatternLayout pattern="[%d{yyyy-MM-dd HH:mm:ss.SSS}] [%-5level] %l %c{36} - %m%n" />
        </File>
        <!--<Async name="Async">-->
            <!--<AppenderRef ref="file"/>-->
        <!--</Async>-->
        <!--使用隨機(jī)讀寫劉的日志文件輸出 appender,性能提高-->
        <RandomAccessFile name="accessFile" fileName="${LOG_HOME}/myAcclog.log">
            <PatternLayout pattern="[%d{yyyy-MM-dd HH:mm:ss.SSS}] [%-5level] %l %c{36} - %m%n" />
        </RandomAccessFile>
        <!--按照一定規(guī)則拆分的日志文件的 appender-->
        <RollingFile name="rollingFile" fileName="${LOG_HOME}/myrollog.log"
                     filePattern="/logs/$${date:yyyy-MM-dd}/myrollog-%d{yyyy-MM-dd-HH-mm}-%i.log">
            <!--日志級(jí)別過濾器-->
            <ThresholdFilter level="debug" onMatch="ACCEPT" onMismatch="DENY" />
            <!--日志消息格式-->
            <PatternLayout pattern="[%d{yyyy-MM-dd HH:mm:ss.SSS}] [%-5level] %l %c{36} - %msg%n" />
            <Policies>
                <!--在系統(tǒng)啟動(dòng)時(shí),出發(fā)拆分規(guī)則,生產(chǎn)一個(gè)新的日志文件-->
                <OnStartupTriggeringPolicy />
                <!--按照文件大小拆分,10MB -->
                <SizeBasedTriggeringPolicy size="10 MB" />
                <!--按照時(shí)間節(jié)點(diǎn)拆分,規(guī)則根據(jù)filePattern定義的-->
                <TimeBasedTriggeringPolicy />
            </Policies>
            <!--在同一個(gè)目錄下,文件的個(gè)數(shù)限定為 30 個(gè),超過進(jìn)行覆蓋-->
            <DefaultRolloverStrategy max="30" />
        </RollingFile>
    </Appenders>
    <!--logger 定義-->
    <Loggers>
        <!--自定義異步 logger 對(duì)象
            includeLocation="false" 關(guān)閉日志記錄的行號(hào)信息
            additivity="false" 不在繼承 rootlogger 對(duì)象
        -->
        <AsyncLogger name="com.macaque" level="trace" includeLocation="false" additivity="false">
            <AppenderRef ref="Console"/>
        </AsyncLogger>
        <!--使用 rootLogger 配置 日志級(jí)別 level="trace"-->
        <Root level="trace">
            <!--指定日志使用的處理器-->
            <AppenderRef ref="Console" />
            <!--使用異步 appender-->
            <AppenderRef ref="Async" />
        </Root>
    </Loggers>
</Configuration>

異步日志

Log4j2最大的特點(diǎn)就是異步日志,其性能的提升也是從異步日志中受益的。Log4j2提供了兩種異步日志的實(shí)現(xiàn),一種是AsyncAppender,一個(gè)是通過AsyncLogger,分別對(duì)應(yīng)前面我們說的Apperder組件和Logger組件。

如果要使用異步日志還需要額外引入一個(gè)Jar包

<dependency>
    <groupId>com.lmax</groupId>
    <artifactId>disruptor</artifactId>
    <version>3.4.2</version>
</dependency>

官網(wǎng)目前不建議使用AsyncAppender的模式,所以這里就不介紹了,著重介紹一下關(guān)于AsyncLogger的日志。其中AsyncLogger有兩種選擇:全局異步和混合異步。

  • 全局異步就是所有的日志都是異步的記錄,在配置文件上不需要任何改動(dòng),只需要加一個(gè)全局的system配置即可:-Dlog4j2.contextSelector=org.apache.logging.log4j.core.async.AsyncLoggerContextSelector
  • 混合異步就是,你可以在應(yīng)用中同時(shí)使用同步日志和異步日志,這使得日志的配置方式更加靈活
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN">
    <properties>
        <property name="LOG_HOME">D:/logs</property>
    </properties>
    <Appenders>
        <File name="file" fileName="${LOG_HOME}/myfile.log">
            <PatternLayout>
                <Pattern>%d %p %c{1.} [%t] %m%n</Pattern>
            </PatternLayout>
        </File>
        <Async name="Async">
            <AppenderRef ref="file"/>
        </Async>
    </Appenders>
    <Loggers>
        <AsyncLogger name="com.macaque" level="trace"
                     includeLocation="false" additivity="false">
            <AppenderRef ref="file"/>
        </AsyncLogger>
        <Root level="info" includeLocation="true">
            <AppenderRef ref="file"/>
        </Root>
    </Loggers>
</Configuration>

如上的配置:com.macaque日志是異步的,root日志是同步的

使用異步日志需要注意兩個(gè)問題

  • 如果使用異步日志,AsyncApperder、AsyncLogger和全局日志,不要同時(shí)出現(xiàn)。行呢個(gè)會(huì)和AsyncApperder一致,降至最低。
  • 設(shè)置includeLocation=false,打印位置信息會(huì)急劇降低異步日志的性能,比同步日志還要慢

Log4j2的性能

Log4j2最厲害的地方在于異步輸出日志時(shí)的性能表現(xiàn),Log4j2再多線程的環(huán)境下吞吐量與Log4j和Logback比較官網(wǎng)提供的圖。可以看到使用全局異步模式性能時(shí)最好的,其次是使用混合異步模式。

[圖片上傳失敗...(image-14f0c6-1617936906299)]

打印日志的最佳實(shí)踐

堅(jiān)持把簡單的事情做好就是不簡單,堅(jiān)持把平凡的事情做好就是不平凡。所謂成功,就是在平凡中做出不平凡的堅(jiān)持!

好的日志記錄方式可以提供我們足夠多定位問題的依據(jù)。日志記錄大家都會(huì)認(rèn)為很簡單,但是如何通過日志可以高效定位問題并不是簡單的事情。

怎么記日志更方便我們查問題

  1. 對(duì)外部的調(diào)用封裝

程序中對(duì)外部系統(tǒng)與模塊的依賴調(diào)用前后都記下日志,方便接口調(diào)試。出問題時(shí)也可以很快理清是哪塊的問題。

boolean debugEnabled = logger.isDebugEnabled();
if (debugEnabled){
    logger.debug("Calling external system : {}",requestParam);
}
try{
    result = callRemoteSystem(requestParam);
    if (debugEnabled){
        logger.debug("Called successfully result is :{}",result);
    }
}catch (BusinessException e){
    logger.warn("Failed at calling xxx system request:{}",requestParam,e);
}catch (Exception e){
    logger.error("Failed at calling xxx system Exception request:{}",requestParam,e);
}
  1. 狀態(tài)變化

程序中重要的狀態(tài)信息變化應(yīng)該記錄下來,方便查問題時(shí)還原現(xiàn)場,推斷程序運(yùn)行過程。

  1. 系統(tǒng)入口與出口

這個(gè)粒度可以是重要的方法或者模塊級(jí)別的,記錄它的輸入和輸出,方便定位。

  1. 業(yè)務(wù)異常

任何業(yè)務(wù)異常都應(yīng)該記下來并且將異常棧給輸出出來。

  1. 很少出現(xiàn)的else情況

很少出現(xiàn)的else情況可能吞掉你的請(qǐng)求,或是賦予難以理解的最終結(jié)果

應(yīng)該避免怎樣的日志方式

  1. 混淆信息的Log

日志應(yīng)該是清晰準(zhǔn)確的,比如當(dāng)看到下面日志的時(shí)候,你知道是因?yàn)檫B接池取不到連接導(dǎo)致的問題嗎?

  Connection connection = ConnectionFactory.getConnection();  
  if (connection == null) {  
      LOG.warn("System initialized unsuccessfully");  
  }  
  1. 不分級(jí)別的記錄日志

無論是異常情況還是入?yún)⒄?qǐng)求使用打印日志的級(jí)別都是info級(jí)別,沒有區(qū)分級(jí)別。這樣有兩個(gè)不好的地方。

  • 無法將打印日志在物理進(jìn)行區(qū)分至不同文件
  • 大量輸出無效日志,不利于系統(tǒng)性能提升,也不利于快速定位錯(cuò)誤點(diǎn)
  1. 遺漏關(guān)鍵信息

這里有可能包括兩種情況

  • 正常情況下未打印關(guān)鍵信息,比如下單流程的訂單ID
  • 異常情況下未打印異常棧
  1. 動(dòng)態(tài)拼接字符串

使用String字符串的拼接會(huì)使用StringBuilder的append()方式,有一定的性能損耗。使用占位符僅僅是替換動(dòng)作,可以有效提升性能。

  1. 重復(fù)打印日志

避免重復(fù)打印日志,浪費(fèi)磁盤空間,務(wù)必在日志配置文件中設(shè)置additivety=false

  1. 不加開關(guān)的日志輸出
logger.debug("Called successfully result is :{}", JSONObject.toJSONString(result));

打印的是debug日志,如果這時(shí)候?qū)⑷罩炯?jí)別改為info,雖然說不會(huì)輸出debug 的日志,但是參數(shù)會(huì)進(jìn)行字符串拼接運(yùn)算,也就是JSON序列化的方法會(huì)被調(diào)用。是會(huì)浪費(fèi)方法調(diào)用的性能。

  1. 所有日志輸入到一個(gè)文件中

不同級(jí)別的日志信息應(yīng)該輸出到不同的日志文件中。將信息進(jìn)行區(qū)分,不僅能夠有效的定位問題,也能夠?qū)F(xiàn)場保留的更久。

源代碼

關(guān)于日志中的所有涉及到的源代碼都在:https://github.com/modouxiansheng/Macaque/tree/master/macaque-log中,大家可以自己下載下來修改配置文件自己理解一下。

參考文章

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容