1、爲什麼加日誌
1.1 日誌是什麼?
日誌文件提供精確的系統記錄,根據日誌最終定位到錯誤詳情和根源。日誌的特點是,它描述一些離散的(不連續的)事件。例如:應用通過一個滾動的文件輸出 INFO 或 ERROR 信息,並通過日誌收集系統,存儲到一些存儲引擎(Elasticsearch)中方便查詢。
1.2 日誌作用
-
打印調試:即可以用日誌來記錄變量或者某一段邏輯。記錄程序運行的流程,即程序運行了哪些代碼,方便排查邏輯問題。
-
問題定位:程序出異常或者出故障時快速的定位問題,方便後期解決問題。因爲線上生產環境無法 debug,在測試環境去模擬一套生產環境,費時費力。所以依靠日誌記錄的信息定位問題,這點非常重要。還可以記錄流量,後期可以通過 ELK(包括 EFK 進行流量統計)。
-
用戶行爲日誌:記錄用戶的操作行爲,用於大數據分析,比如監控、風控、推薦等等。這種日誌,一般是給其他團隊分析使用,而且可能是多個團隊,因此一般會有一定的格式要求,開發者應該按照這個格式來記錄,便於其他團隊的使用。當然,要記錄哪些行爲、操作,一般也是約定好的,因此,開發者主要是執行的角色。
-
根因分析(甩鍋必備*):即在關鍵地方記錄日誌。方便在和各個終端定位問題時,別人說時你的程序問題,你可以理直氣壯的拿出你的日誌說,看,我這裏運行了,狀態也是對的。這樣,對方就會乖乖去定位他的代碼,而不是互相推脫。
1.3 什麼時候記錄日誌
-
系統初始化:系統或者服務的啓動參數。核心模塊或者組件初始化過程中往往依賴一些關鍵配置,根據參數不同會提供不一樣的服務。務必在這裏記錄 INFO 日誌,打印出參數以及啓動完成態服務表述。
-
編程語言提示異常:如今各類主流的編程語言都包括異常機制,業務相關的流行框架有完整的異常模塊。這類捕獲的異常是系統告知開發人員需要加以關注的,是質量非常高的報錯。應當適當記錄日誌,根據實際結合業務的情況使用 WARN 或者 ERROR 級別。
-
業務流程預期不符:除開平臺以及編程語言異常之外,項目代碼中結果與期望不符時也是日誌場景之一,簡單來說所有流程分支都可以加入考慮。取決於開發人員判斷能否容忍情形發生。常見的合適場景包括外部參數不正確,數據處理問題導致返回碼不在合理範圍內等等。
-
系統核心角色,組件關鍵動作:系統中核心角色觸發的業務動作是需要多加關注的,是衡量系統正常運行的重要指標,建議記錄 INFO 級別日誌,比如電商系統用戶從登錄到下單的整個流程;微服務各服務節點交互;核心數據表增刪改;核心組件運行等等,如果日誌頻度高或者打印量特別大,可以提煉關鍵點 INFO 記錄,其餘酌情考慮 DEBUG 級別。
-
第三方服務遠程調用:微服務架構體系中有一個重要的點就是第三方永遠不可信,對於第三方服務遠程調用建議打印請求和響應的參數,方便在和各個終端定位問題,不會因爲第三方服務日誌的缺失變得手足無措。
2、日誌框架
SpringBoot工程自帶logback和slf4j的依賴
Slf4j 英文全稱爲 “ Simple Logging Facade for Java ”,爲 Java 提供的簡單日誌門面。Facade 門面,更底層一點說就是接口。它允許用戶以自己的喜好,在工程中通過 Slf4j 接入不同的日誌系統。
Logback 是 Slf4j 的原生實現框架,同樣也是出自 Log4j 一個人之手,但擁有比 Log4j 更多的優點、特性和更做強的性能,Logback 相對於 Log4j 擁有更快的執行速度。基於我們先前在 Log4j 上的工作,Logback 重寫了內部的實現,在某些特定的場景上面,甚至可以比之前的速度快上 10 倍。在保證 Logback 的組件更加快速的同時,同時所需的內存更加少。
配置
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<property resource="logback.properties"/>
<!--appender通過使用該標籤指定日誌的收集策略-->
<!--name屬性指定appender命名-->
<!-- class屬性指定輸出策略,通常有兩種,控制檯輸出和文件輸出-->
<appender name="CONSOLE-LOG" class="ch.qos.logback.core.ConsoleAppender">
<layout class="ch.qos.logback.classic.PatternLayout">
<pattern>[%d{yyyy-MM-dd' 'HH:mm:ss.sss}] [%C] [%t] [%L] [%-5p] %m%n</pattern>
</layout>
</appender>
<!--獲取比info級別高(包括info級別)但除error級別的日誌-->
<appender name="INFO-LOG" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!--filter標籤,通過使用該標籤指定過濾策略-->
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<!--level標籤指定過濾的類型-->
<level>ERROR</level>
<onMatch>DENY</onMatch>
<onMismatch>ACCEPT</onMismatch>
</filter>
<!--encoder標籤,使用該標籤下的標籤指定日誌輸出格式-->
<encoder>
<pattern>[%d{yyyy-MM-dd' 'HH:mm:ss.sss}] [%C] [%t] [%L] [%-5p] %m%n</pattern>
</encoder>
<!--滾動策略-->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!--路徑-->
<fileNamePattern>${LOG_INFO_HOME}//%d.log</fileNamePattern>
<maxHistory>30</maxHistory>
</rollingPolicy>
</appender>
<appender name="ERROR-LOG" class="ch.qos.logback.core.rolling.RollingFileAppender">
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>ERROR</level>
</filter>
<encoder>
<pattern>[%d{yyyy-MM-dd' 'HH:mm:ss.sss}] [%C] [%t] [%L] [%-5p] %m%n</pattern>
</encoder>
<!--rollingPolicy標籤指定收集策略,比如基於時間進行收集-->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!--fileNamePattern標籤指定生成日誌保存路徑-->
<fileNamePattern>${LOG_ERROR_HOME}//%d.log</fileNamePattern>
<maxHistory>30</maxHistory>
</rollingPolicy>
</appender>
<!--必填標籤,用來指定最基礎的日誌輸出級別-->
<root level="info">
<!--添加append-->
<appender-ref ref="CONSOLE-LOG" />
<appender-ref ref="INFO-LOG" />
<appender-ref ref="ERROR-LOG" />
</root>
</configuration>
異步日誌輸出
之前的日誌配置方式是基於同步的,每次日誌輸出到文件都會進行一次磁盤IO。採用異步寫日誌的方式而不讓此次寫日誌發生磁盤IO,阻塞線程從而造成不必要的性能損耗。異步輸出日誌的方式很簡單,添加一個基於異步寫日誌的appender,並指向原先配置的appender即可
<!-- 異步輸出 -->
<appender name="ASYNC-INFO" class="ch.qos.logback.classic.AsyncAppender">
<!-- 不丟失日誌.默認的,如果隊列的80%已滿,則會丟棄TRACT、DEBUG、INFO級別的日誌 -->
<discardingThreshold>0</discardingThreshold>
<!-- 更改默認的隊列的深度,該值會影響性能.默認值爲256 -->
<queueSize>256</queueSize>
<!-- 添加附加的appender,最多隻能添加一個 -->
<appender-ref ref="INFO-LOG"/>
</appender>
<appender name="ASYNC-ERROR" class="ch.qos.logback.classic.AsyncAppender">
<!-- 不丟失日誌.默認的,如果隊列的80%已滿,則會丟棄TRACT、DEBUG、INFO級別的日誌 -->
<discardingThreshold>0</discardingThreshold>
<!-- 更改默認的隊列的深度,該值會影響性能.默認值爲256 -->
<queueSize>256</queueSize>
<!-- 添加附加的appender,最多隻能添加一個 -->
<appender-ref ref="ERROR-LOG"/>
</appender>
多環境下的日誌配置
logback-spring.xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="60 seconds" debug="false">
<!--
簡要描述
日誌格式 => %d{HH:mm:ss.SSS}(時間) [%-5level](日誌級別) %logger{36}(logger名字最長36個字符,否則按照句點分割) - %msg%n(具體日誌信息並且換行)
開發環境 => ${basepackage}包下控制檯打印DEBUG級別及以上、其他包控制檯打印INFO級別及以上
演示(測試)環境 => ${basepackage}包下控制檯打印INFO級別及以上、其他包控制檯以及文件打印WARN級別及以上
生產環境 => 控制檯以及文件打印ERROR級別及以上
日誌文件生成規則如下:
文件生成目錄 => ${logdir}
當日的log文件名稱 => ${appname}.log
其他時候的log文件名稱 => ${appname}.%d{yyyy-MM-dd}.log
日誌文件最大 => ${maxsize}
最多保留 => ${maxdays}天
-->
<!--自定義參數 -->
<!--用來指定日誌文件的上限大小,那麼到了這個值,就會刪除舊的日誌-->
<property name="maxsize" value="30MB" />
<!--只保留最近90天的日誌-->
<property name="maxdays" value="90" />
<!--application.yml 傳遞參數 -->
<!--log文件生成目錄-->
<springProperty scope="context" name="logdir" source="resources.logdir"/>
<!--應用名稱-->
<springProperty scope="context" name="appname" source="resources.appname"/>
<!--項目基礎包-->
<springProperty scope="context" name="basepackage" source="resources.basepackage"/>
<!--輸出到控制檯 ConsoleAppender-->
<appender name="consoleLog" class="ch.qos.logback.core.ConsoleAppender">
<!--展示格式 layout-->
<layout class="ch.qos.logback.classic.PatternLayout">
<pattern>
<pattern>%d{HH:mm:ss.SSS} [%-5level] %logger{36} - %msg%n</pattern>
</pattern>
</layout>
</appender>
<!--輸出到文件 FileAppender-->
<appender name="fileLog" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!--
日誌名稱,如果沒有File 屬性,那麼只會使用FileNamePattern的文件路徑規則
如果同時有<File>和<FileNamePattern>,那麼當天日誌是<File>,明天會自動把今天
的日誌改名爲今天的日期。即,<File> 的日誌都是當天的。
-->
<File>${logdir}/${appname}.log</File>
<!--滾動策略,按照時間滾動 TimeBasedRollingPolicy-->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!--文件路徑,定義了日誌的切分方式——把每一天的日誌歸檔到一個文件中,以防止日誌填滿整個磁盤空間-->
<FileNamePattern>${logdir}/${appname}.%d{yyyy-MM-dd}.log</FileNamePattern>
<maxHistory>${maxdays}</maxHistory>
<totalSizeCap>${maxsize}</totalSizeCap>
</rollingPolicy>
<!--日誌輸出編碼格式化-->
<encoder>
<charset>UTF-8</charset>
<pattern>%d{HH:mm:ss.SSS} [%-5level] %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<!-- 開發環境-->
<springProfile name="dev">
<root level="INFO">
<appender-ref ref="consoleLog"/>
</root>
<!--
additivity是子Logger 是否繼承 父Logger 的 輸出源(appender) 的標誌位
在這裏additivity配置爲false代表如果${basepackage}中有INFO級別日誌則子looger打印 root不打印
-->
<logger name="${basepackage}" level="DEBUG" additivity="false">
<appender-ref ref="consoleLog"/>
</logger>
</springProfile>
<!-- 演示(測試)環境-->
<springProfile name="test">
<root level="WARN">
<appender-ref ref="consoleLog"/>
<appender-ref ref="fileLog"/>
</root>
<logger name="${basepackage}" level="INFO" additivity="false">
<appender-ref ref="consoleLog"/>
<appender-ref ref="fileLog"/>
</logger>
</springProfile>
<!-- 生產環境 -->
<springProfile name="prod">
<root level="ERROR">
<appender-ref ref="consoleLog"/>
<appender-ref ref="fileLog"/>
</root>
</springProfile>
</configuration>
application.yml
#應用配置
resources:
# log文件寫入地址
logdir: logs/
# 應用名稱
appname: spring-boot-example
# 日誌打印的基礎掃描包
basepackage: com.spring.demo.springbootexample
使用不同環境啓動測試logger配置是否生效,在開發環境下將打印DEBUG級別以上的四條logger記錄,在演示環境下降打印INFO級別以上的三條記錄並寫入文件,在生產環境下只打印ERROR級別以上的一條記錄並寫入文件
@RequestMapping("/logger")
@ResponseBody
public WebResult logger() {
logger.trace("日誌輸出 {}", "trace");
logger.debug("日誌輸出 {}", "debug");
logger.info("日誌輸出 {}", "info");
logger.warn("日誌輸出 {}", "warn");
logger.error("日誌輸出 {}", "error");
return "00";
}
切面日誌處理
- 定義註解
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface OutputLog {
boolean value() default true;
}
- 切面類
@Aspect
@Component
public class LoggerAspect {
private static final Logger logger = LoggerFactory.getLogger(LoggerAspect.class);
@Pointcut("@annotation(com.link.springmvc.logAop.OutputLog)")
public void weblog(){
}
@Around("weblog()")
public Object around(ProceedingJoinPoint point) throws Throwable {
long beginTime = System.currentTimeMillis();
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
List<Object> logArgs = Arrays.stream(point.getArgs())
.filter(arg -> (!(arg instanceof HttpServletRequest) && !(arg instanceof HttpServletResponse)))
.collect(Collectors.toList());
try {
logger.info("請求url={}, 請求參數={}", request.getRequestURI(), JSON.toJSONString(logArgs));
} catch (Exception e) {
logger.error("請求參數獲取異常", e);
}
Object result = point.proceed();
//執行時長(毫秒)
long time = System.currentTimeMillis() - beginTime;
try {
logger.info("請求耗時={}ms, 返回結果={}", time, JSON.toJSONString(result));
} catch (Exception e) {
logger.error("返回參數獲取異常", e);
}
return result;
}
}