前言
記錄應用系統曰志主要有三個原因 記錄操作軌跡、監控系統運行狀況、回溯系統故障。記錄操作行為及操作軌跡數據,可以數據化地分析用戶偏好,有助於優化業務邏輯,為用戶提供個性化的服務。例如,通過 access.log 記錄用戶的操作頻度和跳轉鏈接,有助於分析用戶的后續行為。
全面有效的日志系統有助於建立完善的應用監控體系,由此工程師可以實時監控系統運行狀況,及時預警,避免故障發生。監控系統運行狀況,是指對服務器使用狀態,如內存、 CPU 等使用情況,應用運行情況 如響應時間 QPS 等交互狀態;應用錯誤信息,如空指針、 SQL 異常等的監控。例如,在 CPU 使用率大於 60%, 四核服務器中load 大於4時發出報警,提醒工程師及時處理,避免發生故障。
當系統發生線上問題時,完整的現場日志有助於工程師快速定位問題。例如當系統內存溢出時,如果日志系統記錄了問題發生現場的堆信息,就可以通過這個曰志分析是什么對象在大量產生並且沒有釋放內存,回溯系統故障,從而定位問題。
日志規范
推薦日志文件命名方式
推薦的日志文件命名方式為appName_logType_logName.log 其中 logType為日志類型,推薦分類有 stats monitor visit等, logName 為日志描述。這種命名的好處是通過文件名就可以知道曰志文件屬於什么應用,什么類型 ,什么目的,也有利於歸類查找。例如, mppserver 應用中單獨監控時區轉換異常的日志文件名定義為mppserver__monitor_timeZoneConvert.log
推薦曰志文件保存時間
代碼規約推薦曰志文件至少保存15天,可以根據日志文件的重要程度、文件大小及磁盤空間再自行延長保存時間。
預先判斷曰志級別
對DEBUG 、INFO 級別的日志,必須使用條件輸出或者使用占位符的方式打印。該約定綜合考慮了程序的運行效率和日志打印需求。例如 在某個配置了打印日志級別為WARN 的應用中,如果針對 DEBUG 級別的日志,僅僅在程序中寫出
logger.debug(”Processing trade with id:” + id + ” and symbol:"+ symbol);
,那么該日志不會被打印但是會執行字符串拼接操作,如果 symbol 是對象 還會執行 toString() 方法白白浪費了系統資源。如下示例代碼為正確的打印日志方式
//使明條件判斷形式
if (logger.isDebugEnabled()) {
logger.debug ("Processing trade with id:" + id + "and symlbol:" + symbol) ;
//使用占位符形式
logger.debug ("Processing trade with id: {} and symbol: {}",id, symbol);
避免無效日志打印
生產環境禁止輸出 DEBUG 曰志且有選擇地輸出 INFO日志。使用 INFO、WARN 級別來記錄業務行為信息時,一定要控制日志輸出量,以免磁盤空間不足。同時要為曰志文件設置合理的生命周期及時清理過期的日志。避免重復打印,務必在日志配置文件中設置 additivity=false
區別對待錯誤日志
WARN、ERROR 都是與錯誤有關的日志級別,但不要一發生錯誤就籠統地輸出ERROR 級別日志。 一些業務異常是可以通過引導重試就能恢復正常的,例如用戶輸入參數錯誤。在這種情況下,記錄日志是為了在用戶咨詢時可以還原現場,如果輸出ERROR 級別就表示一旦出現就需要人為介入,這顯然不合理。所以,ERROR只記錄系統邏輯錯誤、異常或者違反重要的業務規則,其他錯誤都可以歸為 WARN級別。
保證記錄內容完整
曰志記錄的內容包括現場上下文信息與異常堆棧信息,所以打印時需要注意以下兩點:
- 記錄異常時一定要輸出異常堆棧,例如
logger.error("xxx" +e.getMessage(),e)
- 曰志中如果輸出對象實例,要確保實例類重寫了 toString()方法,否則只會輸出對象的 hashCode 沒有實際意義。
日志框架分類與選擇
日志門面(日志的抽象層) | 日志實現 |
---|---|
JCL(Jakarta Commons Logging)(2014年后不再維護) jboss-logging (不適合企業項目開發使用) SLF4J(Simple Logging Facade for java) |
Log4j JUL(java.util.logging)(java.util.logging)(擔心被搶市場,推出的) Log4j2( apache開發的很強大,借了log4j的名,但很多框架未適配上) Logback(Log4j同一個人開發的新框架,做了重大升級) |
日志門面
門面設計模式是面向對象設計模式中的一種,日志框架采用的就是這種模式,類似JDBC 的設計理念。它只提供一套接口規范,自身不負責日志功能的實現。目的是讓使用者不需要關注底層具體是哪個日志庫來負責日志打印及具體的使用細節等。目前用得最為廣泛的曰志門面有兩種 slf4j和commons -logging
日志庫
負責實現日志相關功能,主流日志庫有三個,分別為:log4j、log-jdk(java.util.logging.Logger)、logback。logback是最晚出現的,與log4j同一個作者,是log4j的升級版且本身實現了slf4j的接口。
日志適配器
分為:日志門面適配器(日志庫適配slf4j),日志庫適配器(slf4j適配日志庫)。
-
日志門面適配器
老工程用的日志庫沒有實現slf4j接口,如log4j;這時候工程里想使用slf4j+log4j的模式,就額外需要一個適配器(slf4j+log4j12)來解決接口不兼容問題
-
日志庫適配器
老工程直接使用日志庫API完成日志打印,要改成業界標准的門面模式(如slf4j+logback),但是老工程代碼打印日志地方太多難以改動,這是就需要一個適配器來完成從舊日志庫的API到slf4j的路由,這樣在不改動原有代碼的情況下也能使用slf4j來統一管理日志(如:log4j-over-slf4j),后續自由替換具體日志庫也不成問題。
Spring Boot 采用了 slf4j+logback 的組合形式,Spring Boot也提供對JUL、log4j2、Logback提供了默認配置
SpringBoot默認日志配置
新建springboot項目,引入web啟動項,其他默認即可
package com.lzy.logdemo.controller;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @Author: Luzy
* @Date: 2020-07-17 10:16
* @Description:
*/
@RequestMapping
@RestController
public class LogController {
private static final Logger logger = LoggerFactory.getLogger(LogController.class);
@GetMapping("/log")
public String logTest1(String name) {
// 由低到高:trace < debug < info < warn < error
//2. Spring Boot默認設定的是 info 級別日志,(日志默認級別也稱為root級別)。可修改默認級別日志:logging.level.root=級別名
//3. 可以進行調整日志級別,設定某個級別后,就只打印設定的這個級別及后面高級別的日志信息。沒有指定級別的就用SpringBoot默認規定的級別:root級別
//4. 可修改指定包的日志級別:指定某個包下面的所有日志級別:logging.level.包名=級別名
logger.info("------------info--------------{}",name);
logger.error("------------error--------------{}",name);
logger.debug("------------debug--------------{}",name);
logger.trace("------------trace--------------{}",name);
logger.warn("------------warn--------------{}",name);
return "log test..."+name;
}
}
url輸入http://localhost:8080/log?name=lzy,控制台輸出
修改日志默認配置
修改日志文件生成路徑
logging.file.name | logging.file.path | 示例 | 說明 |
---|---|---|---|
(none) | (none) | 只在控制台輸出 | |
指定文件名 | (none) | demo.log | 輸出到當前項目根路徑下的 demo.log 文件中 |
(none) | 指定目錄 | logs/log_lzy | 輸出到當前項目所在磁盤根路徑下的/logs/log_lzy目錄中的 spring.log 文件中 |
指定文件名 | 指定目錄 | 當兩個同時指定時,采用的是logging.file.name 指定。推薦使用logging.file.name 設置即可,因為它可自定義文件名 |
logging:
file:
name: demo.log
# path: logs/log_lzy
修改日志輸出格式
logging:
file:
name: demo.log
pattern:
console: '%clr(%d{yyyy-MM-dd} [%thread] %-5level %logger{50} - %msg%n)'
file: '%d{yyyy-MM-dd HH:mm:ss.SSS} >>> [%thread] >>> %-5level >>> %logger{50} >>> %msg%n'
# path: logs/log_lzy
注意:如上,yml文件中首尾加上單引號可解決識別不了%的問題,properties不需要加
分析日志底層實現
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
spring-boot-starter-web 中引入了 spring-boot-starter 啟動器
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<version>2.3.1.RELEASE</version>
<scope>compile</scope>
</dependency>
spring-boot-starter 中引入了 spring-boot-starter-logging 日志啟動器
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
<version>2.3.1.RELEASE</version>
<scope>compile</scope>
</dependency>
spring-boot-starter-logging 日志啟動器 采用的是 logback 日志框架
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<scope>compile</scope>
</dependency>
總結:SpringBoot中默認日志啟動器為 spring-boot-starter-logging ,默認采用的是 logback 日志框架
在 spring-boot-2.3.1.RELEASE.jar! \org\springframework\boot\logging\logback\base.xml 做了日志的默認配置
<included>
<!--日志格式默認規定-->
<include resource="org/springframework/boot/logging/logback/defaults.xml" />
<!--日志文件默認生成路徑-->
<property name="LOG_FILE"value="${LOG_FILE:-${LOG_PATH:-${LOG_TEMP:-${java.io.tmpdir:-/tmp}}}/spring.log}"/>
<!--控制台日志信息默認配置-->
<include resource="org/springframework/boot/logging/logback/console-appender.xml" /> <!--文件中日志信息默認配置-->
<include resource="org/springframework/boot/logging/logback/file-appender.xml" /> <!--日志級別默認為: info -->
<root level="INFO">
<appender-ref ref="CONSOLE" />
<appender-ref ref="FILE" />
</root>
</included>
日志文件采用方式為:滾動文件追加器
在下面類中會讀取上面xml中配置的信息
如果spring boot的日志功能無法滿足我們的需求(比如異步日志記錄等),我們可以自已定義的日志配置文件
自定義日志配置
自定義Logback日志配置
在類路徑下,存放對應日志框架的自定義配置文件即可;SpringBoot就不會使用它默認的日志配置文件了
Logging System | Customization |
---|---|
Logback | logback-spring.xml , logback-spring.groovy , logback.xml , or logback.groovy |
Log4j2 | log4j2-spring.xml or log4j2.xml |
JDK (Java Util Logging) | logging.properties |
logback.xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration debug="false" scan="false" scanPeriod="60 seconds">
<property name="LOG_HOME" value="./logs/logback"/>
<property name="appName" value="lzy-logDemo"/>
<!-- 定義控制台輸出 -->
<appender name="stdout" class="ch.qos.logback.core.ConsoleAppender">
<layout class="ch.qos.logback.classic.PatternLayout">
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} - [%thread] - %-5level - %logger{50} - %msg%n</pattern>
</layout>
</appender>
<appender name="appLogAppender" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- 指定日志文件的名稱 -->
<file>${LOG_HOME}/${appName}.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${LOG_HOME}/${appName}-%d{yyyy-MM-dd}-%i.log</fileNamePattern>
<MaxHistory>30</MaxHistory>
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<MaxFileSize>10MB</MaxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
</rollingPolicy>
<layout class="ch.qos.logback.classic.PatternLayout">
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [ %thread ] - [ %-5level ] [ %logger{50} : %line ] - %msg%n</pattern>
</layout>
</appender>
<!-- 日志輸出級別 -->
<logger name="org.springframework" level="debug" additivity="false"/>
<logger name="com.lzy.logdemo" level="debug"/>
<root level="INFO">
<appender-ref ref="stdout"/>
<appender-ref ref="appLogAppender"/>
</root>
</configuration>
關於logback.xml與logback-spring.xml的說明
logback.xml :是直接就被日志框架加載了。
logback-spring.xml:配置項不會被日志框架直接加載,而是由 SpringBoot 解析日志配置文件
logback.xml加載早於application.properties,所以如果你在logback.xml使用了變量時,而恰好這個變量是寫在application.properties時,那么就會獲取不到,只要改成logback-spring.xml就可以解決。
官網上說明如下:
因為logback-spring.xml是由 SpringBoot 解析日志配置文件,故可以使用SpringBoot 的 Profifile 特殊配置
logback-spring.xml 使用 Profile 特殊配置
指定運行環境: --spring.profifiles.active=dev
注意
若使用 logback.xml 作為日志配置文件,還指定 Profifile 特殊配置,則會有以下錯誤,便也證明前面所說logback.xml是直接就被日志框架加載。
更換為log4j2日志實現
修改pom依賴
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions><!-- 去掉springboot默認配置 -->
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency> <!-- 引入log4j2依賴 -->
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>
配置文件
<?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="%date{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n" />
<!-- 定義日志存儲的路徑 -->
<property name="FILE_PATH" value="./logs/log4j2" />
<property name="FILE_NAME" value="lzy-log4j2-demo" />
</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>
運行結果如下
補充
若日志配置文件名字隨意取得,需要在springboot配置文件yml中指明:
logging:
file:
name: demo.log
pattern:
console: '%clr(%d{yyyy-MM-dd} [%thread] %-5level %logger{50} - %msg%n)'
file: '%d{yyyy-MM-dd HH:mm:ss.SSS} >>> [%thread] >>> %-5level >>> %logger{50} >>> %msg%n'
# path: logs/log_lzy
config: classpath:logconfig.xml
至此,SpringBoot整合日志已總結完畢,后續若有補充再更新。
參考資料:
Spring官網 https://docs.spring.io/spring-boot/docs/2.3.1.RELEASE/reference/htmlsingle/#boot-features-logging
《碼出高效》第五章節:異常與日志