一、前言
经常处理业务问题的同仁,一定都经常与日志打交道。当并发量高、多线程编程时,日志往往是一大堆,为了快速精确的定位、处理问题,我们需要区分各个用户的不同会话请求,需要从一坨坨日志中做链路追踪。
思路:在输出日志的时候,将每个线程的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拦截器模式