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 {
@Override
protected 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);