1. 程式人生 > 實用技巧 >【日誌】經典日誌框架

【日誌】經典日誌框架

0. 學習目標

  1. 日誌的作用和目的
  2. 日誌的框架
  3. JUL的使用
  4. LOG4J的使用
  5. JCL的使用

1. 日誌的概念

1.1 日誌檔案

日誌檔案是用於記錄系統操作事件的檔案集合,可分為事件日誌和訊息日誌。具有處理歷史資料、診斷問題的追蹤以及理解系統的活動等重要作用。

在計算機中,日誌檔案是記錄在作業系統或其他軟體執行中發生的事件或在通訊軟體的不同使用者之間的訊息的檔案。記錄是保持日誌的行為。在最簡單的情況下,訊息被寫入單個日誌檔案。

許多作業系統,軟體框架和程式包括日誌系統。廣泛使用的日誌記錄標準是在因特網工程任務組(IETF)RFC5424中定義的syslog。 syslog標準使專用的標準化子系統能夠生成,過濾,記錄和分析日誌訊息。

1.1.1 除錯日誌

軟體開發中,我們經常需要去除錯程式,做一些資訊,狀態的輸出便於我們查詢程式的執行狀況。為了
讓我們能夠更加靈活和方便的控制這些除錯的資訊,所有我們需要專業的日誌技術。java中尋找bug會
需要重現。除錯也就是debug 可以在程式執行中暫停程式執行,可以檢視程式在執行中的情況。日誌主
要是為了更方便的去重現問題。

1.1.2 系統日誌

系統日誌是記錄系統中硬體、軟體和系統問題的資訊,同時還可以監視系統中發生的事件。使用者可以通過它來檢查錯誤發生的原因,或者尋找受到攻擊時攻擊者留下的痕跡。系統日誌包括系統日誌、應用程式日誌和安全日誌。

系統日誌的價值
系統日誌策略可以在故障剛剛發生時就向你傳送警告資訊,系統日誌幫助你在最短的時間內發現問題。

系統日誌是一種非常關鍵的元件,因為系統日誌可以讓你充分了解自己的環境。這種系統日誌資訊對於決定故障的根本原因或者縮小系統攻擊範圍來說是非常關鍵的,因為系統日誌可以讓你瞭解故障或者襲擊發生之前的所有事件。為虛擬化環境制定一套良好的系統日誌策略也是至關重要的,因為系統日誌需要和許多不同的外部元件進行關聯。良好的系統日誌可以防止你從錯誤的角度分析問題,避免浪費寶貴的排錯時間。另外一種原因是藉助於系統日誌,管理員很有可能會發現一些之前從未意識到的問題,在幾乎所有剛剛部署系統日誌的環境當中。

2. JAVA日誌框架

問題:

  1. 控制日誌輸出的內容和格式
  2. 控制日誌輸出的位置
  3. 日誌優化:非同步日誌,日誌檔案的歸檔和壓縮
  4. 日誌系統的維護
  5. 面向介面開發 -- 日誌的門面

2.1 為什麼要用日誌框架

因為軟體系統發展到今天已經很複雜了,特別是伺服器端軟體,涉及到的知識,內容,問題太多。在某些方面使用別人成熟的框架,就相當於讓別人幫你完成一些基礎工作,你只需要集中精力完成系統的業務邏輯設計。而且框架一般是成熟,穩健的,他可以處理系統很多細節問題,比如,事務處理,安全性,資料流控制等問題。還有框架一般都經過很多人使用,所以結構很好,所以擴充套件性也很好,而且它是不斷升級的,你可以直接享受別人升級程式碼帶來的好處。

2.2 現有的日誌框架

JUL(java util logging)、logback、log4j、log4j2
JCL(Jakarta Commons Logging)、slf4j( Simple Logging Facade for Java)

日誌門面
JCL、slf4j
日誌實現
JUL、logback、log4j、log4j2

3. JUL 學習

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

3.1 JUL入門

3.1.1 架構介紹

  • Loggers:被稱為記錄器,應用程式通過獲取Logger物件,呼叫其API來來發布日誌資訊。Logger通常時應用程式訪問日誌系統的入口程式。
  • Appenders:也被稱為Handlers,每個Logger都會關聯一組Handlers,Logger會將日誌交給關聯Handlers處理,由Handlers負責將日誌做記錄。Handlers在此是一個抽象,其具體的實現決定了日誌記錄的位置可以是控制檯、檔案、網路上的其他日誌服務或作業系統日誌等。
  • Layouts:也被稱為Formatters,它負責對日誌事件中的資料進行轉換和格式化。Layouts決定了資料在一條日誌記錄中的最終形式。
  • Level:每條日誌訊息都有一個關聯的日誌級別。該級別粗略指導了日誌訊息的重要性和緊迫,我可以將Level和Loggers,Appenders做關聯以便於我們過濾訊息。
  • Filters:過濾器,根據需要定製哪些資訊會被記錄,哪些資訊會被放過。

總結一下就是:
使用者使用Logger來進行日誌記錄,Logger持有若干個Handler,日誌的輸出操作是由Handler完成的。在Handler在輸出日誌前,會經過Filter的過濾,判斷哪些日誌級別過濾放行哪些攔截,Handler會將日誌內容輸出到指定位置(日誌檔案、控制檯等)。Handler在輸出日誌時會使用Layout,將輸出內容進行排版。

3.1.2 入門案例

public class JULTest {
    @Test
    public void testQuick() throws Exception {
        // 1.建立日誌記錄器物件
        Logger logger = Logger.getLogger("com.itheima.log.JULTest");
        // 2.日誌記錄輸出
        logger.info("hello jul");
        logger.log(Level.INFO, "info msg");
        String name = "jack";
        Integer age = 18;
        logger.log(Level.INFO, "使用者資訊:{0},{1}", new Object[]{name, age});
    }
}

3.2 日誌的級別

jul中定義的日誌級別

  • java.util.logging.Level中定義了日誌的級別:
    • SEVERE(最高值)
    • WARNING
    • INFO (預設級別)
    • CONFIG
    • FINE
    • FINER
    • FINEST(最低值)
  • 還有兩個特殊的級別:
    • OFF,可用來關閉日誌記錄。
    • ALL,啟用所有訊息的日誌記錄。

雖然我們測試了7個日誌級別但是預設只實現info以上的級別

@Test
public void testLogLevel() throws Exception {
    // 1.獲取日誌物件
    Logger logger = Logger.getLogger("com.itheima.log.QuickTest");
    // 2.日誌記錄輸出
    logger.severe("severe");
    logger.warning("warning");
    logger.info("info");
    logger.config("cofnig");
    logger.fine("fine");
    logger.finer("finer");
    logger.finest("finest");
}

自定義日誌級別配置

@Test
public void testLogConfig() throws Exception {
    // 1.建立日誌記錄器物件
    Logger logger = Logger.getLogger("com.itheima.log.JULTest");

    // 一、自定義日誌級別
    // a.關閉系統預設配置
    logger.setUseParentHandlers(false);
    // b.建立handler物件
    ConsoleHandler consoleHandler = new ConsoleHandler();
    // c.建立formatter物件
    SimpleFormatter simpleFormatter = new SimpleFormatter();
    // d.進行關聯
    consoleHandler.setFormatter(simpleFormatter);
    logger.addHandler(consoleHandler);
    // e.設定日誌級別
    logger.setLevel(Level.ALL);
    consoleHandler.setLevel(Level.ALL);

    // 二、輸出到日誌檔案
    FileHandler fileHandler = new FileHandler("d:/logs/jul.log");
    fileHandler.setFormatter(simpleFormatter);
    logger.addHandler(fileHandler);
    // 2.日誌記錄輸出
    logger.severe("severe");
    logger.warning("warning");
    logger.info("info");
    logger.config("config");
    logger.fine("fine");
    logger.finer("finer");
    logger.finest("finest");
}

3.3 Logger之間的父子關係

JUL中Logger之間存在父子關係,這種父子關係通過樹狀結構儲存,JUL在初始化時會建立一個頂層RootLogger作為所有Logger父Logger,儲存上作為樹狀結構的根節點。並父子關係通過路徑來關聯。

@Test
public void testLogParent() throws Exception {
    // 日誌記錄器物件父子關係
    Logger logger1 = Logger.getLogger("com.itheima.log");
    Logger logger2 = Logger.getLogger("com.itheima");
    System.out.println(logger1.getParent() == logger2);
    // 所有日誌記錄器物件的頂級父元素 class為java.util.logging.LogManager$RootLogger
    name為""
    System.out.println("logger2 parent:" + logger2.getParent() + ",name:" +
    logger2.getParent().getName());
    // 一、自定義日誌級別
    // a.關閉系統預設配置
    logger2.setUseParentHandlers(false);
    // b.建立handler物件
    ConsoleHandler consoleHandler = new ConsoleHandler();
    // c.建立formatter物件
    SimpleFormatter simpleFormatter = new SimpleFormatter();
    // d.進行關聯
    consoleHandler.setFormatter(simpleFormatter);
    logger2.addHandler(consoleHandler);
    // e.設定日誌級別
    logger2.setLevel(Level.ALL);
    consoleHandler.setLevel(Level.ALL);
    // 測試日誌記錄器物件父子關係
    logger1.severe("severe");
    logger1.warning("warning");
    logger1.info("info");
    logger1.config("config");
    logger1.fine("fine");
    logger1.finer("finer");
    logger1.finest("finest");
}

3.4 日誌的配置檔案

預設配置檔案路徑$JAVAHOME\jre\lib\logging.properties

@Test
public void testProperties() throws Exception {
    // 讀取自定義配置檔案
    InputStream in =
    JULTest.class.getClassLoader().getResourceAsStream("logging.properties");
    // 獲取日誌管理器物件
    LogManager logManager = LogManager.getLogManager();
    // 通過日誌管理器載入配置檔案
    logManager.readConfiguration(in);
    Logger logger = Logger.getLogger("com.itheima.log.JULTest");
    logger.severe("severe");
    logger.warning("warning");
    logger.info("info");
    logger.config("config");
    logger.fine("fine");
    logger.finer("finer");
    logger.finest("finest");
}

配置檔案:

## RootLogger使用的處理器(獲取時設定)
handlers= java.util.logging.ConsoleHandler
# RootLogger日誌等級
.level= INFO

## 自定義Logger
com.itheima.handlers= java.util.logging.FileHandler
# 自定義Logger日誌等級
com.itheima.level= INFO
# 忽略父日誌設定
com.itheima.useParentHandlers=false

## 控制檯處理器
# 輸出日誌級別
java.util.logging.ConsoleHandler.level = INFO
# 輸出日誌格式
java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter

## 檔案處理器
# 輸出日誌級別
java.util.logging.FileHandler.level=INFO
# 輸出日誌格式
java.util.logging.FileHandler.formatter = java.util.logging.SimpleFormatter
# 輸出日誌檔案路徑
java.util.logging.FileHandler.pattern = /java%u.log
# 輸出日誌檔案限制大小(50000位元組)
java.util.logging.FileHandler.limit = 50000
# 輸出日誌檔案限制個數
java.util.logging.FileHandler.count = 10
# 輸出日誌檔案 是否是追加
java.util.logging.FileHandler.append=true

3.5 日誌原理解析

  1. 初始化LogManager
    1. LogManager載入logging.properties配置
    2. 新增Logger到LogManager
  2. 從單例LogManager獲取Logger
  3. 設定級別Level,並指定日誌記錄LogRecord
  4. Filter提供了日誌級別之外更細粒度的控制
  5. Handler是用來處理日誌輸出位置
  6. Formatter是用來格式化LogRecord的

4. LOG4J 學習

Log4j是Apache下的一款開源的日誌框架,通過在專案中使用 Log4J,我們可以控制日誌資訊輸出到控制檯、檔案、甚至是資料庫中。我們可以控制每一條日誌的輸出格式,通過定義日誌的輸出級別,可以更靈活的控制日誌的輸出過程。方便專案的除錯。
官方網站: http://logging.apache.org/log4j/1.2/

4.1 Log4j入門

  1. 建立maven工程
  2. 新增依賴

    <dependencies>
        <dependency>
            <groupId>log4j</groupId>
            <artifactId>log4j</artifactId>
            <version>1.2.17</version>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
        </dependency>
    </dependencies>
  3. java程式碼

    public class Log4jTest {
        @Test
        public void testQuick() throws Exception {
            // 初始化系統配置,不需要配置檔案
            BasicConfigurator.configure();
            // 建立日誌記錄器物件
            Logger logger = Logger.getLogger(Log4jTest.class);
            // 日誌記錄輸出
            logger.info("hello log4j");
            // 日誌級別
            logger.fatal("fatal"); // 嚴重錯誤,一般會造成系統崩潰和終止執行
            logger.error("error"); // 錯誤資訊,但不會影響系統執行
            logger.warn("warn"); // 警告資訊,可能會發生問題
            logger.info("info"); // 程式執行資訊,資料庫的連線、網路、IO操作等
            logger.debug("debug"); // 除錯資訊,一般在開發階段使用,記錄程式的變數、引數等
            logger.trace("trace"); // 追蹤資訊,記錄程式的所有流程資訊
        }
    }
  4. 日誌的級別
    • 每個Logger都被了一個日誌級別(log level),用來控制日誌資訊的輸出。日誌級別從高到低分為:
      • fatal 指出每個嚴重的錯誤事件將會導致應用程式的退出。
      • error 指出雖然發生錯誤事件,但仍然不影響系統的繼續執行。
      • warn 表明會出現潛在的錯誤情形。
      • info 一般和在粗粒度級別上,強調應用程式的執行全程。
      • debug 一般用於細粒度級別上,對除錯應用程式非常有幫助。
      • trace 是程式追蹤,可以用於輸出程式執行中的變數,顯示執行的流程。
    • 還有兩個特殊的級別:
      OFF,可用來關閉日誌記錄。
      ALL,啟用所有訊息的日誌記錄。

      注:一般只使用4個級別,優先順序從高到低為 ERROR > WARN > INFO > DEBUG

4.2 Log4j元件

Log4J 主要由 Loggers (日誌記錄器)、Appenders(輸出端)和 Layout(日誌格式化器)組成。其中Loggers 控制日誌的輸出級別與日誌是否輸出;Appenders 指定日誌的輸出方式(輸出到控制檯、檔案等);Layout 控制日誌資訊的輸出格式。

4.2.1 Loggers

日誌記錄器,負責收集處理日誌記錄,例項的命名就是類“XX”的full quailied name(類的全限定名),Logger的名字大小寫敏感,其命名有繼承機制:例如:name為org.apache.commons的logger會繼承name為org.apache的logger。

Log4J中有一個特殊的logger叫做“root”,他是所有logger的根,也就意味著其他所有的logger都會直接或者間接地繼承自root。root logger可以用Logger.getRootLogger()方法獲取。

但是,自log4j 1.2版以來, Logger 類已經取代了 Category 類。對於熟悉早期版本的log4j的人來說,
Logger 類可以被視為 Category 類的別名。

4.2.2 Appenders

Appender 用來指定日誌輸出到哪個地方,可以同時指定日誌的輸出目的地。Log4j 常用的輸出目的地有以下幾種:

輸出端型別 作用
ConsoleAppender 將日誌輸出到控制檯
FileAppender 將日誌輸出到檔案中
DailyRollingFileAppender 將日誌輸出到一個日誌檔案,並且每天輸出到一個新的檔案
RollingFileAppender 將日誌資訊輸出到一個日誌檔案,並且指定檔案的尺寸,當檔案大小達到指定尺寸時,會自動把檔案改名,同時產生一個新的檔案
JDBCAppender 把日誌資訊儲存到資料庫中

4.2.3 Layouts
佈局器 Layouts用於控制日誌輸出內容的格式,讓我們可以使用各種需要的格式輸出日誌。Log4j常用的Layouts:

格式化器型別 作用
HTMLLayout 格式化日誌輸出為HTML表格形式
SimpleLayout 簡單的日誌輸出格式化,列印的日誌格式為(info - message)
PatternLayout 最強大的格式化期,可以根據自定義格式輸出日誌,如果沒有指定轉換格式,就是用預設的轉換格式

4.3 Layout的格式
在 log4j.properties 配置檔案中,我們定義了日誌輸出級別與輸出端,在輸出端中分別配置日誌的輸出
格式。

  • 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字元,就從左邊交遠銷出的字元截掉

4.4 Appender的輸出

控制檯,檔案,資料庫

#指定日誌的輸出級別與輸出端
log4j.rootLogger=INFO,Console

# 控制檯輸出配置
log4j.appender.Console=org.apache.log4j.ConsoleAppender
log4j.appender.Console.layout=org.apache.log4j.PatternLayout
log4j.appender.Console.layout.ConversionPattern=%d [%t] %-5p [%c] - %m%n

# 檔案輸出配置
log4j.appender.A = org.apache.log4j.DailyRollingFileAppender
#指定日誌的輸出路徑
log4j.appender.A.File = D:/log.txt
log4j.appender.A.Append = true
#使用自定義日誌格式化器
log4j.appender.A.layout = org.apache.log4j.PatternLayout
#指定日誌的輸出格式
log4j.appender.A.layout.ConversionPattern = %-d{yyyy-MM-dd HH:mm:ss} [%t:%r] -[%p] %m%n
#指定日誌的檔案編碼
log4j.appender.A.encoding=UTF-8

#mysql
log4j.appender.logDB=org.apache.log4j.jdbc.JDBCAppender
log4j.appender.logDB.layout=org.apache.log4j.PatternLayout
log4j.appender.logDB.Driver=com.mysql.jdbc.Driver
log4j.appender.logDB.URL=jdbc:mysql://localhost:3306/test
log4j.appender.logDB.User=root
log4j.appender.logDB.Password=root
log4j.appender.logDB.Sql=INSERT INTO
log(project_name,create_date,level,category,file_name,thread_name,line,all_category,message) values('itcast','%d{yyyy-MM-dd HH:mm:ss}','%p','%c','%F','%t','%L','%l','%m')
CREATE TABLE `log` (
    `log_id` int(11) NOT NULL AUTO_INCREMENT,
    `project_name` varchar(255) DEFAULT NULL COMMENT '目項名',
    `create_date` varchar(255) DEFAULT NULL COMMENT '建立時間',
    `level` varchar(255) DEFAULT NULL COMMENT '優先順序',
    `category` varchar(255) DEFAULT NULL COMMENT '所在類的全名',
    `file_name` varchar(255) DEFAULT NULL COMMENT '輸出日誌訊息產生時所在的檔名稱 ',
    `thread_name` varchar(255) DEFAULT NULL COMMENT '日誌事件的執行緒名',
    `line` varchar(255) DEFAULT NULL COMMENT '號行',
    `all_category` varchar(255) DEFAULT NULL COMMENT '日誌事件的發生位置',
    `message` varchar(4000) DEFAULT NULL COMMENT '輸出程式碼中指定的訊息',
    PRIMARY KEY (`log_id`)
);

4.5 自定義Logger

# RootLogger配置
log4j.rootLogger = trace,console
# 自定義Logger
log4j.logger.com.itheima = info,file
log4j.logger.org.apache = error
@Test
public void testCustomLogger() throws Exception {
    // 自定義 com.itheima
    Logger logger1 = Logger.getLogger(Log4jTest.class);
    logger1.fatal("fatal"); // 嚴重錯誤,一般會造成系統崩潰和終止執行
    logger1.error("error"); // 錯誤資訊,但不會影響系統執行
    logger1.warn("warn"); // 警告資訊,可能會發生問題
    logger1.info("info"); // 程式執行資訊,資料庫的連線、網路、IO操作等
    logger1.debug("debug"); // 除錯資訊,一般在開發階段使用,記錄程式的變數、引數等
    logger1.trace("trace"); // 追蹤資訊,記錄程式的所有流程資訊
    
    // 自定義 org.apache
    Logger logger2 = Logger.getLogger(Logger.class);
    logger2.fatal("fatal logger2"); // 嚴重錯誤,一般會造成系統崩潰和終止執行
    logger2.error("error logger2"); // 錯誤資訊,但不會影響系統執行
    logger2.warn("warn logger2"); // 警告資訊,可能會發生問題
    logger2.info("info logger2"); // 程式執行資訊,資料庫的連線、網路、IO操作等
    logger2.debug("debug logger2"); // 除錯資訊,一般在開發階段使用,記錄程式的變數、引數等
    logger2.trace("trace logger2"); // 追蹤資訊,記錄程式的所有流程資訊
}

5. JCL 學習

全稱為Jakarta Commons Logging,是Apache提供的一個通用日誌API。

它是為“所有的Java日誌實現”提供一個統一的介面,它自身也提供一個日誌的實現,但是功能非常常弱(SimpleLog)。所以一般不會單獨使用它。他允許開發人員使用不同的具體日誌實現工具: Log4j, Jdk自帶的日誌(JUL)

JCL 有兩個基本的抽象類:Log(基本記錄器)和LogFactory(負責建立Log例項)。

5.1 JCL入門

  1. 建立maven工程
  2. 新增依賴

    <dependency>
        <groupId>commons-logging</groupId>
        <artifactId>commons-logging</artifactId>
        <version>1.2</version>
    </dependency>
  3. 入門程式碼

    public class JULTest {
        @Test
        public void testQuick() throws Exception {
            // 建立日誌物件
            Log log = LogFactory.getLog(JULTest.class);
            // 日誌記錄輸出
            log.fatal("fatal");
            log.error("error");
            log.warn("warn");
            log.info("info");
            log.debug("debug");
        }
    }

我們為什麼要使用日誌門面:

  1. 面向介面開發,不再依賴具體的實現類。減少程式碼的耦合
  2. 專案通過匯入不同的日誌實現類,可以靈活的切換日誌框架
  3. 統一API,方便開發者學習和使用
  4. 統一配置便於專案日誌的管理

5.2 JCL原理

  1. 通過LogFactory動態載入Log實現類
  2. 日誌門面支援的日誌實現陣列

    private static final String[] classesToDiscover =
        new String[]{"org.apache.commons.logging.impl.Log4JLogger",
        "org.apache.commons.logging.impl.Jdk14Logger",
        "org.apache.commons.logging.impl.Jdk13LumberjackLogger",
        "org.apache.commons.logging.impl.SimpleLog"};
  3. 獲取具體的日誌實現

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