## 一、Log簡介 作為一個完整的軟體,日誌是必不可少的。 程序從開發、測試、維護、運行等環節,都需要向控制台或文件等地方輸出大量訊息。這些訊息的輸出,在很多時候是使用 System.out.println() 無法完成的。 日誌訊息根據用途與記錄內容的不同,可分為:調試日誌、運行日誌、異常日誌等。 ## 幾種常見的log框架 ### log4j Log4j 的全稱為 Log for java,即專門用於 Java 語言的日誌記錄工具,也是Java中最早且最廣泛使用的日誌框架之一。 Log4j有三個主要的元件:Loggers、Appenders、Layouts。 **Loggers(寫Log)** 被分為五個級別:DEBUG < INFO < WARN < ERROR < FATAL 只輸出級別不低於設定級別的訊息 ex:Loggers為INFO,則INFO、WARN、ERROR和FATAL都會輸出 DEBUG則不會 **Appenders(log訊息的終點(目的地))** 常使用的類如下: org.apache.log4j.ConsoleAppender(控制台) org.apache.log4j.FileAppender(文件) org.apache.log4j.DailyRollingFileAppender(每天產生一個日誌文件) org.apache.log4j.RollingFileAppender(文件大小到達指定size的時候產生一個新的文件) org.apache.log4j.WriterAppender(將日誌訊息以輸出流格式發送到任意指定的地方) 如 System.out.println("Some message..."), 對Log4j 而言appenders 就是 console **Layouts(Log訊息的格式)** 常使用的類如下: org.apache.log4j.HTMLLayout(以HTML表格形式佈局) org.apache.log4j.PatternLayout(可以靈活地指定佈局模式) org.apache.log4j.SimpleLayout(包含日誌訊息的級別和訊息字符串) org.apache.log4j.TTCCLayout(包含日誌產生的時間、執行緒、類別等訊息) 它提供了靈活的配置選項,可以透過配置檔案 (properties 或 XML) 或程式碼設定來定義日誌輸出的格式、等級等。 過程式碼中記錄日誌時,使用 Logger 介面來完成。 雖然 log4j 在它的時間內是非常流行且有效的,但在更近期的版本中已經停止了更新,請改使用log4j2。 ### log4j2 log4j2 是 log4j 的後繼版本,它在設計上解決了一些 log4j 存在的性能和低效率問題。 它提供了異步日誌記錄機制,可以更有效地處理大量的日誌輸出。 log4j2 也支援配置文件和程式碼方式來進行日誌的配置。 與 log4j 相比,log4j2 在性能方面有顯著的改進,且在設計上更現代化。 ### slf4j slf4j 不是一個實際的日誌實作框架,而是一個日誌的抽象層(Facade)。 它提供了一個通用的日誌介面,使得程式碼可以與特定的日誌實作(如 log4j、log4j2、java.util.logging 等)解耦,從而達到更容易切換不同的日誌實作的目的。 使用 slf4j,您的應用程式會依賴 slf4j API,而實際的日誌實作則可以在部署時進行配置。 ### Commons Logging 它與 slf4j 的作用類似,也是提供了一個抽象的日誌介面,讓程式碼和日誌實作解耦。 Commons Logging 是 Apache Commons 專案提供的日誌介面。 在common-logging中,有一個Simple logger的簡單實現,但是它功能很弱,所以使用common-logging,通常都是配合著log4j來使用; Commons Logging 使用的是 "發現式" 的日誌實作方式,即在執行時尋找可用的日誌實作來處理輸出。 然而,因為 Commons Logging 的設計問題,有時候可能會引起類載入的問題,而在**新的應用程式中通常更推薦使用 slf4j**。 **小小結論:**slf4j 是使用得最廣泛且推薦的日誌抽象層,而 log4j2 是在性能方面較優秀的具體日誌實作。 ## 特別注意! log4j與log4j2最大的不同就是定義日誌紀錄器物件的方式,可以注意到兩者在實例化Logger時,使用的依賴與方式都有微小的差異。 log4j使用的是依賴是`org.apache.log4j.Logger` 實例化時則是`Logger.getLogger` 而 log4j2使用的依賴是 `import org.apache.logging.log4j.LogManager` `import org.apache.logging.log4j.Logger;` 實例化時則是`LogManager.getLogger` ### log4j ```java import org.apache.log4j.Logger; public class Log4jExample { // 取得 Logger 實例 private static final Logger logger = Logger.getLogger(Log4jExample.class); public static void main(String[] args) { // 使用 Logger 輸出日誌 logger.debug("This is a debug message"); logger.info("This is an info message"); logger.warn("This is a warning message"); logger.error("This is an error message"); logger.fatal("This is a fatal message"); } } ``` ### log4j2 ```java import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; public class Log4j2Example { // 取得 Logger 實例 private static final Logger logger = LogManager.getLogger(Log4j2Example.class); public static void main(String[] args) { // 使用 Logger 輸出日誌 logger.debug("This is a debug message"); logger.info("This is an info message"); logger.warn("This is a warning message"); logger.error("This is an error message"); logger.fatal("This is a fatal message"); } } ``` ## 特別注意2! springboot專案一般是整合了logback+slf4j 使用的是`spring-boot-starter-logging`此依賴 使用方式則是使用`LoggerFactory.getLogger`實例化 ```java import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class MyService { private static final Logger logger = LoggerFactory.getLogger(MyService.class); public void doSomething() { logger.info("Doing something..."); } } ``` ## log的坑 其實不是很重要,以目前電腦效能來說不太需要擔心這種資源消耗。 但如果你是精益求精的人,可以看看。 ```java // 格式1 log.debug("get user" + uid + " from DB is Empty!"); // 格式2 if (log.isdebugEnable()) { log.debug("get user" + uid + " from DB is Empty!"); } // 格式3 log.debug("get user {} from DB is Empty!", uid); ``` 以上三種log的寫法 格式1 依然還是要做字串拼接,即便它不输出。 格式2 需要加入額外的判斷邏輯,增加廢Code。 格式3,只有在執行時才會動態拼接,關閉相對應的log層級後,不會有任何性能消耗。 ## slf4j+log4j2 使用maven在pom.xml中加入以下依賴 ``` <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-api</artifactId> <version>2.9.1</version> </dependency> <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-core</artifactId> <version>2.9.1</version> </dependency> <!--與slf4j保持橋接--> <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-slf4j-impl</artifactId> <version>2.11.0</version> </dependency> <!-- slf4j核心包--> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>1.7.25</version> </dependency> ``` springboot2.0以上版本會自動偵測resource資料夾中的log4j2配置檔案,如log4j2.xml ## 配置介紹 以下會比較複雜一些,但基本上就是依照專案內的配置(log4j.xml、log4j2.xml、logback.xml或yml檔...等)微調即可, 有興趣再深入看具體每一個配置的作用。 ``` +-Configuration +-Properties +-Appenders: +-Appender +-Layout +-Policies +-Strategy +-Loggers +-Logger +-RootLogger ``` 根節點Configuration有兩個屬性:status和monitorinterval: 1. status:用來指定log4j本身的打印日誌的級別; 2. monitorinterval:用於指定log4j2自動重新配置的監測間隔時間,單位是秒,最小是5秒。 ### 節點說明: #### Properties:一般用來做屬性定義; #### Appender:可以理解為一個管道,定義了日誌內容的輸出位置: **Console:用來定義輸出到控制台的Appender;** name:指定Appender的名稱; target:SYSTEM_OUT或SYSTEM_ERR,一般只設置默認值:SYSTEM_OUT; PatternLayout:輸出格式,不設置默認值為:%m%n;Filter:配置日誌事件能否被輸出。過濾條件有三個值:ACCEPT(接受),DENY(拒絕),NEUTRAL(中立); **File:用來定義輸出到指定位置的文件Appender;** name:指定Appender的名稱; fileName:指定輸出日誌的目的文件帶全路徑的文件名; PatternLayout:輸出格式,不設置默認值為:%m%n; Filter:配置日誌事件能否被輸出。過濾條件有三個值:ACCEPT(接受),DENY(拒絕),NEUTRAL(中立); **RollingFile:用來定義超過指定大小自動刪除舊的創建新文件的Appender;** name:指定Appender的名稱; fileName:指定輸出日誌的目的文件帶全路徑的文件名; PatternLayout:輸出格式,不設置默認值為:%m%n; Filter:配置日誌事件能否被輸出。過濾條件有三個值:ACCEPT(接受),DENY(拒絕),NEUTRAL(中立); PatternLayout:輸出格式,不設置默認值為:%m%n; Policies:指定滾動日誌的策略,就是什麼時候進行新建日誌文件輸出日誌; Strategy:配置Strategy以控制日誌如何(How)進行滾動。 #### Loggers:簡單說Logger就是一個路由器,指定類、包中的日誌訊息流向哪個Appender,以及控制他們的日誌級別; **Root:必須要配置;用來指定項目的根日誌,如果沒有單獨指定Logger,那麼就會默認使用該Root日誌輸出;** level:屬性;用來指定日誌輸出級別; AppenderRef:Root的子節點,用來指定該日誌輸出到哪個Appender。 **Logger:用來單獨指定日誌的形式,比如要為指定包下的class指定不同的日誌級別等。** level:屬性;用來指定日誌輸出級別; name:屬性;用來指定該Logger所適用的類或者類所在的包全路徑,繼承自Root節點; AppenderRef:Logger的子節點,用來指定該日誌輸出到哪個Appender;如果沒有指定,就會默認繼承自Root;如果指定了,那麼會在指定的這個Appender和Root的Appender中都會輸出,此時我們可以設置Logger的additivity="false"只在自定義的Appender中進行輸出。 **上面提到的Filter一般常用的有以下三種:** LevelRangeFilter:輸出指定日誌級別範圍之內的日誌; TimeFilter:在指定時間範圍內才會輸出日誌; ThresholdFilter:輸出符合特定日誌級別及其以上級別的日誌。 **上面提到的Policies一般常用的有以下三種:** SizeBasedTriggeringPolicy:根據日誌文件的大小進行滾動;單位有:KB,MB,GB; CronTriggeringPolicy:使用Cron表達式進行日誌滾動,很靈活; TimeBasedTriggeringPolicy:這個配置需要和filePattern結合使用,注意filePattern中配置的文件重命名規則。滾動策略依賴於filePattern中配置的最具體的時間單位,根據最具體的時間單位進行滾動。這種方式比較簡潔。 ### xml配置 ```xml <?xml version="1.0" encoding="UTF-8"?> <!--Configuration後面的status,這個用於設置log4j2自身內部的信息輸出,可以不設置,當設置成trace時,你會看到log4j2內部各種詳細輸出--> <!--monitorInterval:Log4j能夠自動檢測修改配置文件和重新配置本身,設置間隔秒數--> <configuration monitorInterval="5"> <!--日誌級別以及優先級排序: OFF > FATAL > ERROR > WARN > INFO > DEBUG > TRACE > ALL --> <!--變量配置--> <Properties> <!-- 格式化輸出:%date表示日期,%thread表示線程名,%-5level:級別從左顯示5個字符寬度 %msg:日誌消息,%n是換行符--> <!-- %logger{36} 表示 Logger 名字最長36個字符 --> <property name="LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n" /> <!-- 定義日誌存儲的路徑,不要配置相對路徑 --> <property name="FILE_PATH" value="E:\logs\log4j2" /> <property name="FILE_NAME" value="springbootlog4j2" /> </Properties> <appenders> <console name="Console" target="SYSTEM_OUT"> <!--輸出日誌的格式--> <PatternLayout pattern="${LOG_PATTERN}"/> <!--控制台只輸出level及其以上級別的信息(onMatch),其他的直接拒絕(onMismatch)--> <ThresholdFilter level="info" onMatch="ACCEPT" onMismatch="DENY"/> </console> <!--文件會打印出所有信息,這個log每次運行程序會自動清空,由append屬性決定,適合臨時測試用--> <File name="Filelog" fileName="${FILE_PATH}/test.log" append="false"> <PatternLayout pattern="${LOG_PATTERN}"/> </File> <!-- 這個會打印出所有的info及以下級別的信息,每次大小超過size,則這size大小的日誌會自動存入按年份-月份建立的文件夾下面並進行壓縮,作為存檔--> <RollingFile name="RollingFileInfo" fileName="${FILE_PATH}/info.log" filePattern="${FILE_PATH}/${FILE_NAME}-INFO-%d{yyyy-MM-dd}_%i.log.gz"> <!--控制台只輸出level及以上級別的信息(onMatch),其他的直接拒絕(onMismatch)--> <ThresholdFilter level="info" onMatch="ACCEPT" onMismatch="DENY"/> <PatternLayout pattern="${LOG_PATTERN}"/> <Policies> <!--interval屬性用來指定多久滾動一次,默认是1 hour--> <TimeBasedTriggeringPolicy interval="1"/> <SizeBasedTriggeringPolicy size="10MB"/> </Policies> <!-- DefaultRolloverStrategy屬性如不設置,則默認為最多同一文件夾下7個文件開始覆蓋--> <DefaultRolloverStrategy max="15"/> </RollingFile> <!-- 這個會打印出所有的warn及以下級別的信息,每次大小超過size,則這size大小的日誌會自動存入按年份-月份建立的文件夾下面並進行壓縮,作為存檔--> <RollingFile name="RollingFileWarn" fileName="${FILE_PATH}/warn.log" filePattern="${FILE_PATH}/${FILE_NAME}-WARN-%d{yyyy-MM-dd}_%i.log.gz"> <!--控制台只輸出level及以上級別的信息(onMatch),其他的直接拒絕(onMismatch)--> <ThresholdFilter level="warn" onMatch="ACCEPT" onMismatch="DENY"/> <PatternLayout pattern="${LOG_PATTERN}"/> <Policies> <!--interval屬性用來指定多久滾動一次,默认是1 hour--> <TimeBasedTriggeringPolicy interval="1"/> <SizeBasedTriggeringPolicy size="10MB"/> </Policies> <!-- DefaultRolloverStrategy屬性如不設置,則默認為最多同一文件夾下7個文件開始覆蓋--> <DefaultRolloverStrategy max="15"/> </RollingFile> <!-- 這個會打印出所有的error及以下級別的信息,每次大小超過size,則這size大小的日誌會自動存入按年份-月份建立的文件夾下面並進行壓縮,作為存檔--> <RollingFile name="RollingFileError" fileName="${FILE_PATH}/error.log" filePattern="${FILE_PATH}/${FILE_NAME}-ERROR-%d{yyyy-MM-dd}_%i.log.gz"> <!--控制台只輸出level及以上級別的信息(onMatch),其他的直接拒絕(onMismatch)--> <ThresholdFilter level="error" onMatch="ACCEPT" onMismatch="DENY"/> <PatternLayout pattern="${LOG_PATTERN}"/> <Policies> <!--interval屬性用來指定多久滾動一次,默认是1 hour--> <TimeBasedTriggeringPolicy interval="1"/> <SizeBasedTriggeringPolicy size="10MB"/> </Policies> <!-- DefaultRolloverStrategy屬性如不設置,則默認為最多同一文件夾下7個文件開始覆蓋--> <DefaultRolloverStrategy max="15"/> </RollingFile> </appenders> <!--Logger節點用來單獨指定日誌的形式,比如要為指定包下的class指定不同的日誌級別等。--> <!--然後定義loggers,只有定義了logger並引入的appender,appender才會生效--> <loggers> <!--過濾掉spring和mybatis的一些無用的DEBUG信息--> <logger name="org.mybatis" level="info" additivity="false"> <AppenderRef ref="Console"/> </logger> <!--監控系統信息--> <!--若是additivity設為false,則 子Logger 只會在自己的appender裡輸出,而不會在 父Logger 的appender裡輸出。--> <Logger name="org.springframework" level="info" additivity="false"> <AppenderRef ref="Console"/> </Logger> <root level="info"> <appender-ref ref="Console"/> <appender-ref ref="Filelog"/> <appender-ref ref="RollingFileInfo"/> <appender-ref ref="RollingFileWarn"/> <appender-ref ref="RollingFileError"/> </root> </loggers> </configuration> ``` ## 總結 因為log4j在1.x時有很大的安全漏洞「Log4Shell」漏洞(CVE-2021-44228), 所以現在普遍都使用log4j2做日誌功能。 log是每個專案都會使用到的技術,所以妥善的安插log,對於開發上debug或有錯誤發生時,都可以非常有效的幫助工程師排錯。 進階的部分還可以用AOP中的aspect切面技術來實現log紀錄。 spring AOP有實現叫做AspectJ,由於是另一門技術就不在此多述。 ## 參考資料來源: https://medium.com/learning-from-jhipster/11-spring-boot-%E4%BD%BF%E7%94%A8-logback-%E6%88%96-log4j2-e655b320a2c8 https://segmentfault.com/a/1190000037598528 https://www.dataset.com/blog/maven-log4j2-project/ https://hackmd.io/GuX8ngltT2yUWHWeb5UV-g https://hackmd.io/@KaiChen/B1MjzutuL https://ithelp.ithome.com.tw/articles/10261299 https://killerbryant.pixnet.net/blog/post/44215393 https://dotblogs.azurewebsites.net/Leon-Yang/2021/01/06/155519 https://kknews.cc/zh-tw/code/yz9arja.html https://cloud.tencent.com/developer/article/2354562?areaId=106001 https://medium.com/%E7%A8%8B%E5%BC%8F%E8%A3%A1%E6%9C%89%E8%9F%B2/spring-boot-log-%E8%A8%98%E9%8C%84%E6%96%B9%E5%BC%8F-f91b8c067cdb Aspect 完整記錄(log) https://blog.typeart.cc/use-aspect-logging-req-res-in-spring-boot/