【萬字長文】帶你瞭解日誌的前世此生

日誌就像車輛保險,沒人願意爲保險付錢,可是一旦出了問題誰都又想有保險可用html

日誌的做用和目的

日誌文件

日誌文件是用於記錄系統操做事件的文件集合,能夠分爲事件日誌和消息日誌。具備處理歷史數據、診斷問題的追蹤以及理解系統的活動等重要做用。java

在計算機中,日誌文件是一個記錄了發生在運行中的操做系統或者其餘軟件中的事件的文件,或者記錄了在網絡聊天軟件的用戶之間發送的消息。日誌記錄是指保存日誌的行爲。最簡單的作法的將日誌寫入單個存放日誌的文件。git

爲何要打印日誌

爲何要打印日誌,或者何時打印日誌這取決於打印的目的。不一樣的打印目的決定了日誌輸出的格式,輸出的位置以及輸出的頻率github

  1. 調試開發:目的是開發調試程序時使用,只應該出如今開發週期內,而不該該在線上系統輸出
  2. 用戶行爲日誌:記錄用戶操做行爲,多用於大數據分析,如監控、風控、推薦等等
  3. 程序運行日誌:記錄程序運行時狀況,特別是非預期的行爲,異常狀況,主要是開發維護使用
  4. 機器日誌:主要是記錄網絡請求、系統CPU、內存、IO使用等狀況,供運維或者監控使用

日誌中應該包含什麼

利用4W1H進行分析數據庫

  • When:打印日誌的時間戳,此時的時間應該是日誌記錄的事情發生的時間,具體的時間能夠幫助咱們分析時間發生的時間點
  • Where:日誌在哪裏被記錄,具體哪一個模塊,記錄到哪一個文件,哪一個函數,哪一行代碼
  • What:日誌的主體是什麼,簡明扼要描述日誌記錄的事情
  • Who:事件生產者的惟一標識,以訂單爲例就是訂單id,固然也能夠是某個動做的聲明
  • How:日誌的重要程度分級,通常以ERROR > WARNNING > INFO > DEBUG > TRACE來劃分重要程度

Java日誌的前世此生

爲何要用日誌框架

軟件系統發展到如今已經很是複雜了,特別是在服務器端軟件,涉及到的知識以及內容問題太多。在某些方面使用別人成熟的框架,就至關於讓別人幫你完成一些基礎工做,你只須要集中精力完成系統的業務邏輯設計。並且框架通常是成熟、穩健的,他能夠幫助你處理不少細節的問題,好比日誌的異步處理、動態控制等等問題。還有框架通常都是通過不少人使用,因此結構性、擴展性都很是好。apache

現有的日誌框架

按照日誌門面和日誌實現劃分的話現有的Java日誌框架有如下幾種api

  • 日誌門面:JCL、Slf4j
  • 日誌實現:JUL、Logback、Log4j、Log4j2

圖片

爲何要有日誌門面

當咱們的系統變得更加複雜的時候,咱們的日誌就容易發生混亂。隨着系統開發的進行,可能會更新不一樣的日誌框架,形成當前系統中存在不一樣的日誌依賴,讓咱們難以統一的管理和控制。就算咱們強制要求了咱們公司內開發的項目使用了相同的日誌框架,可是系統中會引用其餘相似Spring或者Mybatis等等的第三方框架,它們依賴於咱們規定不一樣的日誌框架,並且他們自身的日誌系統就有着不一致性,依然會出現日誌體系的混亂。數組

因此借鑑JDBC的思想,爲日誌系統也提供一套門面,那麼咱們就能夠面向這些接口規範來開發,避免直接依賴具體的日誌框架。這樣咱們的系統在日誌中就存在了日誌的門面和日誌的實現。服務器

日誌門面的日誌實現的關係

圖片

Log4j

Apache Log4j 是一種基於 Java 的日誌記錄工具,它是 Apache 軟件基金會的一個項目。在 jdk1.3 以前,尚未現成的日誌框架,Java 工程師只能使用原始的 System.out.println(), System.err.println() 或者 e.printStackTrace()。經過把 debug 日誌寫到 StdOut 流,錯誤日誌寫到 ErrOut 流,以此記錄應用程序的運行狀態。這種原始的日誌記錄方式缺陷明顯,不只沒法實現定製化,並且日誌的輸出粒度不夠細。鑑於此,1999 年,大牛 Ceki Gülcü 建立了 Log4j 項目,並幾乎成爲了 Java 日誌框架的實際標準。markdown

JUL

Log4j 做爲 Apache 基金會的一員,Apache 但願將 Log4j 引入 jdk,不過被 sun 公司拒絕了。隨後,sun 模仿 Log4j,在 jdk1.4 中引入了 JUL(java.util.logging)。

JCL

爲了解耦日誌接口與實現,2002 年 Apache 推出了 JCL(Jakarta Commons Logging),也就是 Commons Logging。Commons Logging 定義了一套日誌接口,具體實現則由 Log4j 或 JUL 來完成。Commons Logging 基於動態綁定來實現日誌的記錄,在使用時只須要用它定義的接口編碼便可,程序運行時會使用 ClassLoader 尋找和載入底層的日誌庫,所以能夠自由選擇由 log4j 或 JUL 來實現日誌功能。

SlF4j和Logback

大牛 Ceki Gülcü 與 Apache 基金會關於 Commons-Logging 制定的標準存在分歧,後來,Ceki Gülcü 離開 Apache 並前後建立了 Slf4j 和 Logback 兩個項目。Slf4j 是一個日誌門面,只提供接口,能夠支持 Logback、JUL、Log4j 等日誌實現,Logback 提供具體的實現,它相較於 log4j 有更快的執行速度和更完善的功能。

Log4j2

爲了維護在 Java 日誌江湖的地位,防止 JCL、Log4j 被 Slf4j、Logback 組合取代 ,2014 年 Apache 推出了 Log4j 2。Log4j 2 與 Log4j 不兼容,通過大量深度優化,其性能顯著提高。

各個日誌框架原理簡介及介紹

Log4j

Log4j是Apache下的一款開源的日誌框架,經過在項目中使用Log4j,咱們能夠控制日誌信息輸出到控制檯、文件、甚至是數據庫中。咱們能夠控制每一條日誌的輸出格式,經過定義日誌的輸出級別,能夠更加靈活方便的控制日誌的輸出過程。

Log4j的官方網站:logging.apache.org/log4j/1.2/

若是要在項目中使用Log4j的話須要引入相應的Jar包

<dependency>
    <groupId>log4j</groupId>
    <artifactId>log4j</artifactId>
    <version>1.2.17</version>
</dependency>
複製代碼

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

Loggers

Loggers主要負責處理日誌記錄,Loggers的命名有繼承的機制,例如名稱爲com.test.log的logger會繼承名稱爲com.test的logger。

Log4j中有一個特殊的logger叫做「root」,他是全部logger的根,也就是意味着其餘全部的logger都會直接或者間接的繼承自root。

Appenders

Appender用來指定日誌輸出到哪一個地方,能夠同時指定多個日誌的輸出目的地。Log4j的輸出目的地有如下集中。

輸出端類型 做用
ConsoleAppender 將日誌輸出到控制檯
FileAppender 將日誌輸出到文件
DailyRollingFileAppender 將日誌輸出到一個日誌文件,而且天天輸出到一個新的文件
RollingFileAppender 將日誌信息輸出到日誌文件,而且按照指定文件的尺寸,當文件大小達到指定尺寸時,會自動將文件更名,同時生成一個新的文件
JDBCAppender 把日誌信息保存到數據庫中

Layouts

Layouts用於控制日誌輸出內容的格式,讓咱們可使用各類須要的格式輸出日誌。Log4j經常使用的Layouts:

格式化器類型 做用
HTMLLayout 格式化日誌輸出爲HTML表格形式
SimpleLayout 簡單的日誌輸出格式化
PatternLayout 能夠根據自定義格式輸出日誌
* log4j 採用相似 C 語言的 printf 函數的打印格式格式化日誌信息,具體的佔位符及其含義以下:
        %m 輸出代碼中指定的日誌信息
        %p 輸出優先級,及 DEBUG、INFO 等
        %n 換行符(Windows平臺的換行符爲 "\n",Unix 平臺爲 "\n")
        %r 輸出自應用啓動到輸出該 log 信息耗費的毫秒數
        %c 輸出打印語句所屬的類的全名
        %t 輸出產生該日誌的線程全名
        %d 輸出服務器當前時間,默認爲 ISO8601,也能夠指定格式,如:%d{yyyy年MM月dd日
        HH:mm:ss}
        %l 輸出日誌時間發生的位置,包括類名、線程、及在代碼中的行數。如:
        Test.main(Test.java:10)
        %F 輸出日誌消息產生時所在的文件名稱
        %L 輸出代碼中的行號
        %% 輸出一個 "%" 字符
* 能夠在 % 與字符之間加上修飾符來控制最小寬度、最大寬度和文本的對其方式。如:
        %5c 輸出category名稱,最小寬度是5,category<5,默認的狀況下右對齊
        %-5c 輸出category名稱,最小寬度是5,category<5,"-"號指定左對齊,會有空格
        %.5c 輸出category名稱,最大寬度是5,category>5,就會將左邊多出的字符截掉,<5不會有空格
        %20.30c category名稱<20補空格,而且右對齊,>30字符,就從左邊交遠銷出的字符截掉
複製代碼

JUL

JUL全稱是Java util Logging,是Java原生的日誌框架,使用時不須要另外引用第三方類庫,相對其餘日誌框架使用方便,學習簡單,可以在小型應用中靈活使用。

JUL的架構

圖片

  • Logger:被稱爲記錄器,應用程序經過獲取Logger對象,調用其API來發布日誌信息。Logger一般是應用程序訪問日誌系統的入口程序
  • Handler(和Log4j的Appenders相似):每一個Logger都會被關聯一組Handlers,Logger會將日誌交給關聯的Handlers處理。此Handler是一個抽象,其具體的實現決定了日誌記錄的位置能夠是控制檯、文件、數據庫等等
  • Layouts:也被稱爲Formatters,它負責對日誌進行格式化的處理,Layouts決定了數據在一條日誌記錄中的最終形式
  • Filters:過濾器,根據須要定製哪些信息會被記錄

總結一下就是用戶使用Logger來進行日誌的記錄,Logger持有若干個Handler,日誌的輸出操做是由Handler來完成的,在Handler輸出以前會經過自定義的Filter過濾規則過濾掉不須要輸出的信息,最終由Handler決定使用什麼樣的Layout將日誌格式化處理並決定輸出到什麼地方去。

接下來就寫一個簡單的入門案例看一下JUL是如何進行日誌處理的

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

// 1.獲取日誌記錄器對象
Logger logger = Logger.getLogger("com.macaque.JulLogTest");
// 關閉系統默認配置
logger.setUseParentHandlers(false);
// 自定義配置日誌級別
// 建立ConsolHhandler 控制檯輸出
ConsoleHandler consoleHandler = new ConsoleHandler();
// 建立簡單格式轉換對象
SimpleFormatter simpleFormatter = new SimpleFormatter();
// 進行關聯
consoleHandler.setFormatter(simpleFormatter);
logger.addHandler(consoleHandler);
// 配置日誌具體級別
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提供的一個通用的日誌API。

它的目標是「爲全部的Java日誌實現」提供一個統一的接口,它自身也提供一個日誌實現,可是功能很是弱(SimpleLog)。因此通常不單獨使用JCL。他容許開發人員使用不一樣的日誌實現工具:Log4j、JDK自帶的日誌(JUL)

JCL有兩個基本的抽象類:Log和LogFactory

圖片

如何使用

若是要在項目中使用JCL則要引入相應的jar包

<dependency>
    <groupId>commons-logging</groupId>
    <artifactId>commons-logging</artifactId>
    <version>1.2</version>
</dependency>
複製代碼

這只是引入了相應的日誌門面,具體的日誌實現還須要本身引入。

原理介紹

在使用JCL打印日誌的時候是經過調用其LogFactory動態加載Log的實現類

Log log = LogFactory.getLog(xxxx.class);
複製代碼

圖片

而後在初始化的時候經過遍歷數組進行查找有沒有符合的實現類,遍歷的數組初始化是

/**
 * 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"
};
複製代碼

遍歷這個數組的邏輯

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

SlF4j

簡單日誌門面(Simple Logging Facade For Java)SlF4j主要是爲了給Java日誌訪問提供一套標準、規範的API框架,其主要意義在於提供接口,具體的實現能夠交由其餘日誌框架。對於通常的Java項目而言,日誌框架會選擇Slf4j-api做爲門面,配上具體的實現框架,中間使用橋接器進行橋接。

官方網站:www.slf4j.org/

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

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

日誌的綁定

Slf4j支持各類日誌框架,而Slf4j只是做爲一個日誌門面的存在,定義一個日誌的打印規範,那麼就會有兩種狀況,針對這兩種狀況引入包的類別略有不一樣。

  1. 遵照Slf4j定義的規範:若是是遵照了Slf4j定義的日誌規範的話,那麼只須要引入兩個包,一個是Slf4j的依賴,以及遵照了其規範的日誌jar包實現便可
  2. 沒遵照Slf4j定義的規範:若是未遵照Slf4j定義的日誌規範,那麼須要引入三個包,一個是Slf4j的依賴,一個是適配器的包,一個是未遵照Slf4j定義的日誌規範的包.

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

圖片

日誌綁定底層原理簡介

在上面介紹的JCL的底層綁定原理咱們瞭解到JCL是經過輪詢的機制進行啓動時檢測綁定的日誌實現,可是在Slf4j中不同,咱們能夠從LoggerFactory.getLogger方法中進行入手查看,最終定位到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 {
        // 這一處是重點,經過類加載器找到全部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加載日誌的具體實現
  2. LoggerFactory在初始化的過程當中,會經過performInitialization()方法綁定具體的日誌實現
  3. 在綁定具體實現的時候,經過類加載器,加載org/slf4j/impl/StaticLoggerBinder.class類
  4. 因此,只要是一個日誌實現框架,在org.slf4j.impl包中提供一個本身的StaticLoggerBinder類,在其中提供具體日誌實現的LoggerFactory就能夠被Slf4j進行加載管理了

日誌框架的橋接

在一些老項目中有可能一開始使用的不是Slf4j框架,若是在這時想要進行日誌的升級,那麼Slf4j也提供了這樣的功能,提供了相應的橋接器進行對原有日誌框架的替換,下圖是官網所表示的如何進行的日誌橋接。其實簡單來講就是將原有的日誌重定向到Slf4j而後交由Slf4j進行管理。

圖片

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

首先咱們創建一個項目首先使用Log4j進行打印日誌,引入Log4j的jar包

<dependency>
    <groupId>log4j</groupId>
    <artifactId>log4j</artifactId>
    <version>1.2.17</version>
</dependency>
複製代碼

而後簡單加入Log4j的配置進行打印日誌

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

控制檯的輸出以下,由於沒有作日誌格式的處理,因此只是簡單輸出了字符串。

圖片

接下來咱們要在不改動一點代碼的狀況,只是加入和移除一些依賴包就能夠完成日誌框架的升級,咱們這裏假設要升級爲Logback,按照如下步驟進行便可。

  1. 移除原有的日誌框架(這裏就是Log4j的日誌框架)
  2. 移除了原有日誌框架,代碼確定報錯了,因此再添加Log4j的日誌橋接器
  3. 加入Slf4j-api的依賴
  4. 再加入Logback的日誌實現依賴

完成這四步之後,日誌框架就完成了升級,接下來咱們看一下效果,這裏在Logback的日誌輸出中加入了格式的處理。能看到日誌已是由Logback打印出來了。

圖片

Logback

Logback是由Log4j的創始人設計的另外一款開源日誌組件,性能比Log4j性能要好,官方網站:logback.qos.ch/index.html

Logback主要分爲三個模塊

  • logback-core:其餘兩個模塊的基礎模塊
  • logback-classic:它是Log4j的一個改良版本,同時它完整實現了Slf4j的API
  • logback-access:訪問模塊與Servlet容器集成,經過Http來訪問日誌的功能

後續的日誌都是經過Slf4j日誌門面搭建日誌系統,因此在代碼是沒有什麼區別的,主要是經過改變配置文件和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會依次讀取如下類型配置文件:

  • 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爲行號
%thread線程名稱
%m或者%msg爲信息
%n換行
-->
<!--格式化輸出:%d表示日期,%thread表示線程名,%-5level:級別從左顯示5個字符寬度
%msg:日誌消息,%n是換行符-->
<property name="pattern" value="%d{yyyy-MM-dd HH:mm:ss.SSS} %c [%thread]
%-5level %msg%n"/>
<!--
Appender: 設置日誌信息的去向,經常使用的有如下幾個
ch.qos.logback.core.ConsoleAppender (控制檯)
3. FileAppender配置
ch.qos.logback.core.rolling.RollingFileAppender (文件大小到達指定尺
寸的時候產生一個新文件)
ch.qos.logback.core.FileAppender (文件)
-->
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
<!--輸出流對象 默認 System.out 改成 System.err-->
<target>System.err</target>
<!--日誌格式配置-->
<encoder
class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>${pattern}</pattern>
</encoder>
</appender>
<!--
用來設置某一個包或者具體的某一個類的日誌打印級別、以及指定<appender>。
<loger>僅有一個name屬性,一個可選的level和一個可選的addtivity屬性
name:
用來指定受此logger約束的某一個包或者具體的某一個類。
level:
用來設置打印級別,大小寫無關:TRACE, DEBUG, INFO, WARN, ERROR, ALL 和
OFF,
若是未設置此屬性,那麼當前logger將會繼承上級的級別。
additivity:
是否向上級loger傳遞打印信息。默認是true。
<logger>能夠包含零個或多個<appender-ref>元素,標識這個appender將會添加到這個
logger
-->
<!--
也是<logger>元素,可是它是根logger。默認debug
level:用來設置打印級別,大小寫無關:TRACE, DEBUG, INFO, WARN, ERROR, ALL
和 OFF,
<root>能夠包含零個或多個<appender-ref>元素,標識這個appender將會添加到這個
logger。
-->
<root level="ALL">
<appender-ref ref="console"/>
</root>
</configuration>
複製代碼

FileAppender配置

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!-- 自定義屬性 能夠經過${name}進行引用-->
<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爲行號
        %c類的完整名稱
        %thread線程名稱
        %n換行
        %-5level
      -->
<!-- 日誌文件存放目錄 -->
<property name="log_dir" value="d:/logs"></property>
<!--控制檯輸出appender對象-->
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
<!--輸出流對象 默認 System.out 改成 System.err-->
<target>System.err</target>
<!--日誌格式配置-->
<encoder
class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>${pattern}</pattern>
</encoder>
</appender>
<!--日誌文件輸出appender對象-->
<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對象 -->
<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對象-->
<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}進行引用-->
<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爲行號
        %c類的完整名稱
        %thread線程名稱
        %n換行
        %-5level
      -->
<!-- 日誌文件存放目錄 -->
<property name="log_dir" value="d:/logs"></property>
<!--控制檯輸出appender對象-->
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
  <!--輸出流對象 默認 System.out 改成 System.err-->
  <target>System.err</target>
  <!--日誌格式配置-->
  <encoder
  class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
  <pattern>${pattern}</pattern>
  </encoder>
</appender>
<!-- 日誌文件拆分和歸檔的appender對象-->
<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>
  <!--指定日誌文件拆分和壓縮規則-->
  <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對象-->
<root level="all">
  <appender-ref ref="console"/>
  <appender-ref ref="rollFile"/>
</root>
</configuration>
複製代碼

Filter和異步日誌配置

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!-- 自定義屬性 能夠經過${name}進行引用-->
<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爲行號
%c類的完整名稱
%thread線程名稱
%n換行
%-5level
-->
<!-- 日誌文件存放目錄 -->
<property name="log_dir" value="d:/logs/"></property>
<!--控制檯輸出appender對象-->
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
<!--輸出流對象 默認 System.out 改成 System.err-->
<target>System.err</target>
<!--日誌格式配置-->
<encoder
class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>${pattern}</pattern>
</encoder>
</appender>
<!-- 日誌文件拆分和歸檔的appender對象-->
<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>
<!--指定日誌文件拆分和壓縮規則-->
<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">
<!--設置攔截日誌級別-->
<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對象-->
<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轉向Logback

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

Log4j2

Apache Log4j2是對Log4j的升級版,參考了logback的一些優秀設計,而且修復了一些問題帶來了一些重大的提高,主要有:

  • 異常處理:在logback中Appender中的異常不會被應用感知到,可是在log4j2中提供了一些異常的處理機制
  • 性能提高,log4j2相較於log4j和logback都具備很明顯的性能提高,後面會有官方的測試數據
  • 自動重載配置,參考了logback的配置,固然會提供自動刷新參數配置,最實用的就是在咱們生產環境中動態的修改日誌的級別而不須要重啓應用
  • 無垃圾機制,log4j在大部分狀況下,均可以使用其設計的一套無垃圾機制,避免頻繁的日誌收集致使的jvm gc

官方網站:logging.apache.org/log4j/2.x/

如何使用Log4j2

目前市面上最主流的日誌門面是Slf4j,雖然自己Log4j2也是日誌門面,由於它的日誌實現功能很是強大,性能優越。因此你們通常仍是將Log4j2看做是日誌額實現,Slf4j+Log4j2應該是將來的大勢所趨。

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

<!--使用slf4j做爲日誌的門面,使用log4j2來記錄日誌 -->
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-api</artifactId>
    <version>1.7.25</version>
</dependency>
<!--爲slf4j綁定日誌實現 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 日誌實現 -->
<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" 日誌框架自己的輸出日誌級別
    monitorInterval="5" 自動加載配置文件的間隔時間,不低於 5 秒
-->
<Configuration status="debug" monitorInterval="5">
    <!--
        集中配置屬性進行管理
        使用時經過:${name}
    -->
    <properties>
        <property name="LOG_HOME">/logs</property>
    </properties>
    <!--日誌處理-->
    <Appenders>
        <!--控制檯輸出 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>-->
        <!--使用隨機讀寫劉的日誌文件輸出 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>
        <!--按照必定規則拆分的日誌文件的 appender-->
        <RollingFile name="rollingFile" fileName="${LOG_HOME}/myrollog.log"
                     filePattern="/logs/$${date:yyyy-MM-dd}/myrollog-%d{yyyy-MM-dd-HH-mm}-%i.log">
            <!--日誌級別過濾器-->
            <ThresholdFilter level="debug" onMatch="ACCEPT" onMismatch="DENY" />
            <!--日誌消息格式-->
            <PatternLayout pattern="[%d{yyyy-MM-dd HH:mm:ss.SSS}] [%-5level] %l %c{36} - %msg%n" />
            <Policies>
                <!--在系統啓動時,出發拆分規則,生產一個新的日誌文件-->
                <OnStartupTriggeringPolicy />
                <!--按照文件大小拆分,10MB -->
                <SizeBasedTriggeringPolicy size="10 MB" />
                <!--按照時間節點拆分,規則根據filePattern定義的-->
                <TimeBasedTriggeringPolicy />
            </Policies>
            <!--在同一個目錄下,文件的個數限定爲 30 個,超過進行覆蓋-->
            <DefaultRolloverStrategy max="30" />
        </RollingFile>
    </Appenders>
    <!--logger 定義-->
    <Loggers>
        <!--自定義異步 logger 對象
            includeLocation="false" 關閉日誌記錄的行號信息
            additivity="false" 不在繼承 rootlogger 對象
        -->
        <AsyncLogger name="com.macaque" level="trace" includeLocation="false" additivity="false">
            <AppenderRef ref="Console"/>
        </AsyncLogger>
        <!--使用 rootLogger 配置 日誌級別 level="trace"-->
        <Root level="trace">
            <!--指定日誌使用的處理器-->
            <AppenderRef ref="Console" />
            <!--使用異步 appender-->
            <AppenderRef ref="Async" />
        </Root>
    </Loggers>
</Configuration>
複製代碼

異步日誌

Log4j2最大的特色就是異步日誌,其性能的提高也是從異步日誌中受益的。Log4j2提供了兩種異步日誌的實現,一種是AsyncAppender,一個是經過AsyncLogger,分別對應前面咱們說的Apperder組件和Logger組件。

若是要使用異步日誌還須要額外引入一個Jar包

<dependency>
    <groupId>com.lmax</groupId>
    <artifactId>disruptor</artifactId>
    <version>3.4.2</version>
</dependency>
複製代碼

官網目前不建議使用AsyncAppender的模式,因此這裏就不介紹了,着重介紹一下關於AsyncLogger的日誌。其中AsyncLogger有兩種選擇:全局異步和混合異步。

  • 全局異步就是全部的日誌都是異步的記錄,在配置文件上不須要任何改動,只須要加一個全局的system配置便可:-Dlog4j2.contextSelector=org.apache.logging.log4j.core.async.AsyncLoggerContextSelector
  • 混合異步就是,你能夠在應用中同時使用同步日誌和異步日誌,這使得日誌的配置方式更加靈活
<?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日誌是同步的

使用異步日誌須要注意兩個問題

  • 若是使用異步日誌,AsyncApperder、AsyncLogger和全局日誌,不要同時出現。行呢個會和AsyncApperder一致,降至最低。
  • 設置includeLocation=false,打印位置信息會急劇下降異步日誌的性能,比同步日誌還要慢

Log4j2的性能

Log4j2最厲害的地方在於異步輸出日誌時的性能表現,Log4j2再多線程的環境下吞吐量與Log4j和Logback比較官網提供的圖。能夠看到使用全局異步模式性能時最好的,其次是使用混合異步模式。

圖片

打印日誌的最佳實踐

堅持把簡單的事情作好就是不簡單,堅持把平凡的事情作好就是不平凡。所謂成功,就是在平凡中作出不平凡的堅持!

好的日誌記錄方式能夠提供咱們足夠多定位問題的依據。日誌記錄你們都會認爲很簡單,可是如何經過日誌能夠高效定位問題並非簡單的事情。

怎麼記日誌更方便咱們查問題

  1. 對外部的調用封裝

程序中對外部系統與模塊的依賴調用先後都記下日誌,方便接口調試。出問題時也能夠很快理清是哪塊的問題。

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. 狀態變化

程序中重要的狀態信息變化應該記錄下來,方便查問題時還原現場,推斷程序運行過程。

  1. 系統入口與出口

這個粒度能夠是重要的方法或者模塊級別的,記錄它的輸入和輸出,方便定位。

  1. 業務異常

任何業務異常都應該記下來而且將異常棧給輸出出來。

  1. 不多出現的else狀況

不多出現的else狀況可能吞掉你的請求,或是賦予難以理解的最終結果

應該避免怎樣的日誌方式

  1. 混淆信息的Log

日誌應該是清晰準確的,好比當看到下面日誌的時候,你知道是由於鏈接池取不到鏈接致使的問題嗎?

  Connection connection = ConnectionFactory.getConnection();  
  if (connection == null) {  
      LOG.warn("System initialized unsuccessfully");  
  }  
複製代碼
  1. 不分級別的記錄日誌

不管是異常狀況仍是入參請求使用打印日誌的級別都是info級別,沒有區分級別。這樣有兩個很差的地方。

  • 沒法將打印日誌在物理進行區分至不一樣文件
  • 大量輸出無效日誌,不利於系統性能提高,也不利於快速定位錯誤點
  1. 遺漏關鍵信息

這裏有可能包括兩種狀況

  • 正常狀況下未打印關鍵信息,好比下單流程的訂單ID
  • 異常狀況下未打印異常棧
  1. 動態拼接字符串

使用String字符串的拼接會使用StringBuilder的append()方式,有必定的性能損耗。使用佔位符僅僅是替換動做,能夠有效提高性能。

  1. 重複打印日誌

避免重複打印日誌,浪費磁盤空間,務必在日誌配置文件中設置additivety=false

  1. 不加開關的日誌輸出
logger.debug("Called successfully result is :{}", JSONObject.toJSONString(result));
複製代碼

打印的是debug日誌,若是這時候將日誌級別改成info,雖說不會輸出debug 的日誌,可是參數會進行字符串拼接運算,也就是JSON序列化的方法會被調用。是會浪費方法調用的性能。

  1. 全部日誌輸入到一個文件中

不一樣級別的日誌信息應該輸出到不一樣的日誌文件中。將信息進行區分,不只可以有效的定位問題,也可以將現場保留的更久。

源代碼

關於日誌中的全部涉及到的源代碼都在:github.com/modouxiansh…中,你們能夠本身下載下來修改配置文件本身理解一下。

參考文章

相關文章
相關標籤/搜索