一、前言
經常處理業務問題的同仁,一定都經常與日誌打交道。當併發量高、多線程編程時,日誌往往是一大堆,爲了快速精確的定位、處理問題,我們需要區分各個用戶的不同會話請求,需要從一坨坨日誌中做鏈路追蹤。
思路:在輸出日誌的時候,將每個線程的ID同時輸出,當然前提是保證每個線程的ID是唯一的。sl4j 提供的一個工具類MDC,支持 logback和log4j,作用就是擴展變量值到日誌中並輸出。
二、切面模式輸出線程ID
通過自定義切面,攔截有註解@LogId的請求,附加會話ID輸出到日誌。
- 加入POM引用
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> <exclusions> <exclusion> <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> </exclusion> </exclusions> </dependency>
- 自定義切點
package cn.wuhg.climbing.service.design.patterns.sessionid.aspect; import java.lang.annotation.*; /** * 自定義日誌註解 */ @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface LogId { String value() default ""; }
- 自定義日誌切面
package cn.wuhg.climbing.service.design.patterns.sessionid.aspect; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.annotation.*; import org.slf4j.MDC; import org.springframework.stereotype.Component; import java.util.UUID; /** * 自定義日誌切面 */ @Aspect @Component public class LogAspect { private final static String SESSION_ID = "sessionId"; /** * 自定義切點 */ @Pointcut("@annotation(cn.wuhg.climbing.service.design.patterns.sessionid.aspect.LogId)") public void pointCut() { } /** * 前置通知-記錄請求信息 * @param joinPoint */ @Before("pointCut()") public void doBeforeAdvice(JoinPoint joinPoint) { // MDC容器增加requestId String uuid = UUID.randomUUID().toString().replaceAll("-", ""); MDC.put(SESSION_ID, uuid); } /** * 後置通知-記錄返回信息 * @param joinPoint * @param result */ @AfterReturning(pointcut = "pointCut()", returning = "result") public void doAfterReturningAdvice(JoinPoint joinPoint, Object result) { MDC.remove(SESSION_ID); } /** * 後置異常通知-記錄返回出現異 * @param joinPoint * @param exception */ @AfterThrowing(value = "pointCut()", throwing = "exception") public void doAfterThrowingAdvice(JoinPoint joinPoint, Throwable exception) { MDC.remove(SESSION_ID); } }
- logback配置
<?xml version="1.0" encoding="UTF-8"?> <configuration> <contextName>SpringBootDemo</contextName> <property name="LOG_PATH" value="ToolLogs" /> <!--設置系統日誌目錄 --> <property name="APPDIR" value="design-patterns" /> <!-- 日誌記錄器,日期滾動記錄 --> <appender name="FILEALL" class="ch.qos.logback.core.rolling.RollingFileAppender"> <file>${LOG_PATH}/${APPDIR}/logs.log</file> <!-- 日誌記錄器的滾動策略,按日期,按大小記錄 --> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <!-- 歸檔的日誌文件的路徑,例如今天是2013-12-21日誌,當前寫的日誌文件路徑爲file節點指定,可以將此文件與file指定文件路徑設置爲不同路徑,從而將當前日誌文件或歸檔日誌文件置不同的目錄。 而2013-12-21的日誌文件在由fileNamePattern指定。%d{yyyy-MM-dd}指定日期格式,%i指定索引 --> <fileNamePattern>${LOG_PATH}/${APPDIR}/logs-%d{yyyy-MM-dd}.%i.log </fileNamePattern> <!-- 除按日誌記錄之外,還配置了日誌文件不能超過20M,若超過20M,日誌文件會以索引0開始, 命名日誌文件,例如log-error-2013-12-21.0.log --> <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP"> <maxFileSize>20MB</maxFileSize> </timeBasedFileNamingAndTriggeringPolicy> </rollingPolicy> <!-- 追加方式記錄日誌 --> <append>true</append> <!-- 日誌文件的格式 --> <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder"> <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%X{sessionId}] %-5level %class.%method Line:%-3L - %msg%n</pattern> <charset>utf-8</charset> </encoder> </appender> <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> <!--encoder 默認配置爲PatternLayoutEncoder --> <encoder> <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%X{sessionId}] %-5level %class.%method Line:%-3L - %msg%n</pattern> <charset>utf-8</charset> </encoder> <!--此日誌appender是爲開發使用,只配置最底級別,控制檯輸出的日誌級別是大於或等於此級別的日誌信息 --> <filter class="ch.qos.logback.classic.filter.ThresholdFilter"> <level>debug</level> </filter> </appender> <!-- 生產環境下,將此級別配置爲適合的級別,以免日誌文件太多或影響程序性能 --> <root level="INFO"> <appender-ref ref="FILEALL" /> <appender-ref ref="STDOUT" /> </root> </configuration>
- Controller接口
@LogId @GetMapping("/log") public void logId(){ log.info("測試slf4j日誌線程ID"); }
- 日誌截圖,紅框處是會話ID
- 整體切面過濾
可以通過制定路徑範圍,來整體過濾,去除了@LogId註解的限制,僅需將自定義切面替換即可@Pointcut("execution(public * cn.wuhg.climbing.service.design.patterns..*(..))") public void pointCut() { }
三、往期推薦
logback打印日誌輸出線程ID:mvc攔截器模式