Spring Boot 中使用 MDC 追踪一次请求全过程(日志链路)
ControllerLogAspect
package com.yymt.common.trace;import cn.hutool.json.JSONUtil;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.lang.annotation.Annotation;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;/*** @description:* @author: xl* @version: 1.0.0* @date: 2024-05-24 14:22:02*/@Order(value = Ordered.HIGHEST_PRECEDENCE)
@Component
@Aspect
@Slf4j
public class ControllerLogAspect {// 参数类型是下面类型的也不会打印,可以扩展private static List<Class<?>> notLogTypes = Arrays.asList(ServletRequest.class, ServletResponse.class);/*** 拦截所有controller方法** @param joinPoint* @return* @throws Throwable*/@Around("execution(* com.yymt.controller..*(..))")public Object around(ProceedingJoinPoint joinPoint) throws Throwable {long l = System.currentTimeMillis();MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();Object result = null;try {// 打印处理当前请求的完整类名和方法log.info("接口方法,{},{}", methodSignature.getDeclaringTypeName(), methodSignature.getName());// 获取所有要打印的参数,丢到map中,key为参数名称,value为参数的值,然后会将这个mp以json的格式输出Map<String, Object> logParamsMap = new LinkedHashMap<>();String[] parameterNames = methodSignature.getParameterNames();Object[] args = joinPoint.getArgs();for (int i = 0; i < args.length; i++) {if (parameterIsLog(methodSignature, i)) {// 参数名称String parameterName = parameterNames[i];// 参数值Object parameterValue = args[i];logParamsMap.put(parameterName, parameterValue);}}log.info("方法参数列表:{}", JSONUtil.toJsonStr(logParamsMap));result = joinPoint.proceed();return result;} finally {if (this.resultIsLog(methodSignature)) {log.info("方法返回值:{}", JSONUtil.toJsonStr(result));}}}/*** 判断参数是否需要打印** @param methodSignature* @param paramIndex* @return*/private boolean parameterIsLog(MethodSignature methodSignature, int paramIndex) {if (methodSignature.getMethod().getParameterCount() == 0) {return false;}// 参数上有 @NoLog注解的不会打印Annotation[] paramAnnotation = methodSignature.getMethod().getParameterAnnotations()[paramIndex];if (paramAnnotation != null && paramAnnotation.length > 0) {for (Annotation annotation : paramAnnotation) {if (annotation.annotationType() == NoLog.class) {return false;}}}// 参数是下面类型的也不会打印Class parameterType = methodSignature.getParameterTypes()[paramIndex];for (Class<?> type : notLogTypes) {if (type.isAssignableFrom(parameterType)) {return false;}}return true;}/*** 判断方法的返回值是否需要打印? 方法上有@NoLog 注解的,表示结果不打印返回值** @param methodSignature* @return*/private boolean resultIsLog(MethodSignature methodSignature) {return methodSignature.getMethod().getAnnotation(NoLog.class) == null;}}
ResultTraceIdAspect
package com.yymt.common.trace;import com.yymt.common.utils.R;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;/*** @description: 在返回值中填充traceId,用于方便排查错误* @author: xl* @version: 1.0.0* @date: 2024-05-24 14:22:02*/@Order(value = Ordered.HIGHEST_PRECEDENCE)
@Component
@Aspect
@Slf4j
public class ResultTraceIdAspect {@Pointcut("execution(* com.yymt.controller..*(..)) || execution(* com.yymt.common.exception.RRExceptionHandler.*(..))")public void pointCut() {}@Around("pointCut()")public Object around(ProceedingJoinPoint joinPoint) throws Throwable {Object object = joinPoint.proceed();if (object instanceof R) {((R) object).put("traceId", TraceUtils.getTraceId());}return object;}}
TraceFilter
package com.yymt.common.trace;import cn.hutool.core.util.IdUtil;
import com.yymt.common.trace.TraceUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;/*** @description:* @author: xl* @version: 1.0.0* @date: 2024-05-24 15:01:46*/
@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
@WebFilter(urlPatterns = "/**", filterName = "TraceFilter")
@Slf4j
public class TraceFilter extends OncePerRequestFilter {@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)throws ServletException, IOException {String traceID = IdUtil.fastSimpleUUID();TraceUtils.setTraceId(traceID);log.info("请求start:{}", request.getRequestURL().toString());long st = System.currentTimeMillis();try {filterChain.doFilter(request, response);} finally {long et = System.currentTimeMillis();log.info("请求end:{},耗时(ms):{}", request.getRequestURL().toString(), (et - st));TraceUtils.removeTraceId();}}}
TraceUtils
package com.yymt.common.trace;import org.slf4j.MDC;/*** @description:* @author: xl* @version: 1.0.0* @date: 2024-05-24 15:33:04*/
public class TraceUtils {public static final String TRACE_ID = "traceId";public static ThreadLocal<String> traceThreadLocal = new ThreadLocal<>();public static String getTraceId() {return traceThreadLocal.get();}public static void setTraceId(String traceId) {traceThreadLocal.set(traceId);MDC.put(TRACE_ID, traceId);}public static void removeTraceId() {traceThreadLocal.remove();MDC.remove(TRACE_ID);}}
NoLog
package com.yymt.common.trace;import java.lang.annotation.*;/*** 不打印日志注解**/
@Target({ElementType.METHOD,ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface NoLog {}
logback-spring.xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration><include resource="org/springframework/boot/logging/logback/defaults.xml" /><logger name="org.springframework.web" level="INFO"/><logger name="org.springboot.sample" level="TRACE"/><springProperty scop="context" name="appName" source="spring.application.name" defaultValue="bbt"/><springProperty scop="context" name="rootLevel" source="bbt.logger.level" defaultValue="INFO"/><springProperty scop="context" name="log.path" source="logging.path" defaultValue=""/><springProperty scop="context" name="spring.profiles.active" source="spring.profiles.active" defaultValue="local"/><property name="LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} ${LOG_LEVEL_PATTERN:-%5p} [%thread][%X{traceId}]%logger{50} %3.3L - %msg%n"/><property name="CONSOLE_LOG_PATTERN" value="%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){magenta} %highlight(%-5level) %clr([%thread][%X{traceId}]){faint} %clr(%logger{50}){cyan} %clr(%3.3L) %clr(-){faint} %msg%n"/><appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender"><!-- 引用自定义输出模板 --><encoder><pattern>${CONSOLE_LOG_PATTERN}</pattern></encoder></appender><appender name="DEBUG_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"><!--记录的日志文件的路径及文件名--><file>${log.path}/debug.log</file><rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy"><!-- 滚动日志文件保存格式 --><fileNamePattern>${log.path}/%d{yyyy-MM,aux}/debug.%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern><MaxFileSize>50MB</MaxFileSize><totalSizeCap>10GB</totalSizeCap><MaxHistory>180</MaxHistory></rollingPolicy><filter class="ch.qos.logback.classic.filter.LevelFilter"><level>DEBUG</level></filter><encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder"><!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符--><pattern>${LOG_PATTERN}</pattern></encoder></appender><appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"><!--记录的日志文件的路径及文件名--><file>${log.path}/error.log</file><rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy"><!-- 滚动日志文件保存格式 --><fileNamePattern>${log.path}/%d{yyyy-MM,aux}/error.%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern><MaxFileSize>50MB</MaxFileSize><totalSizeCap>10GB</totalSizeCap><MaxHistory>180</MaxHistory></rollingPolicy><filter class="ch.qos.logback.classic.filter.LevelFilter"><level>ERROR</level><onMatch>ACCEPT</onMatch><onMismatch>DENY</onMismatch></filter><encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder"><!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符--><pattern>${LOG_PATTERN}</pattern></encoder></appender><appender name="FILE-TOTAL" class="ch.qos.logback.core.rolling.RollingFileAppender"><!--记录的日志文件的路径及文件名--><file>${log.path}/spring.log</file><!--记录的日志级别--><filter class="ch.qos.logback.classic.filter.LevelFilter"><level>DEBUG</level><onMatch>ACCEPT</onMatch><onMismatch>ACCEPT</onMismatch></filter><!--日志文件输出格式--><encoder><pattern>${LOG_PATTERN}</pattern></encoder><!--日志记录器的滚动策略,按日期,按大小记录--><rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy"><fileNamePattern>${log.path}/%d{yyyy-MM,aux}/spring.%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern><MaxFileSize>50MB</MaxFileSize><totalSizeCap>10GB</totalSizeCap><MaxHistory>180</MaxHistory></rollingPolicy></appender><root level="INFO"><appender-ref ref="CONSOLE"/>
<!-- <appender-ref ref="STDOUT"/>--><appender-ref ref="DEBUG_FILE"/><appender-ref ref="ERROR_FILE"/><appender-ref ref="FILE-TOTAL"/></root><!-- 开发、测试环境 --><springProfile name="dev,test,local,docker,report-test,test-gc"><logger name="org.springframework.web" level="INFO"/><logger name="org.springboot.sample" level="INFO" /><logger name="com.yymt" level="debug" /></springProfile><!-- 生产环境 : prod-rj表示蓉江新区--><springProfile name="prod,report-prod,prod-fu,prod-rj,prod-jjpcs,prod-hwsq"><logger name="org.springframework.web" level="INFO"/><logger name="org.springboot.sample" level="INFO" /><logger name="com.yymt" level="INFO"/></springProfile></configuration>
多线程使用
注意如果是开启子线程需要自己设置traceId进去,日志才会打印traceId
String traceId = TraceUtils.getTraceId();CompletableFuture.runAsync(() -> {// MDC.put(TRACE_ID, traceId);TraceUtils.setTraceId(traceId);try {log.info("runAsync");} finally {// MDC.clear();TraceUtils.removeTraceId();}}, executor);
参考: Spring Boot 中使用 MDC 追踪一次请求全过程