(二十八)基于MDC的日志追踪
基础项目地址:
https://gitee.com/springzb/admin-boot
ThreadLocal 适用于变量在线程间隔离,而在方法或类间共享的场景
这里基于拦截器(Intercptor)思想去做,进入之前前置处理preHandle将 ThreadLocal 本地变量中添加 traceId, 结束后afterCompletion删除对应线程的 traceId
ThreadLocal 适用于变量在线程间隔离,而在方法或类间共享的场景
利用ThreadLocal共享变量,存储 tranceId (即用uuid),在拦截器里面为每个请求设置请求id
创建tranceId工具类
package cn.mesmile.admin.common.log;
import java.util.UUID;
/**
* @author zb
* @Description
*/
public class RequestIdUtils {
private static final ThreadLocal<UUID> requestIdHolder = new ThreadLocal<>();
private RequestIdUtils() {
}
public static void generateRequestId() {
requestIdHolder.set(UUID.randomUUID());
}
public static void generateRequestId(UUID uuid) {
requestIdHolder.set(uuid);
}
public static UUID getRequestId() {
return (UUID)requestIdHolder.get();
}
public static void removeRequestId() {
requestIdHolder.remove();
}
}
新建拦截器,将请求id加入到MDC中去
程序运行在 Tomcat 中,执行程序的线程是 Tomcat 的工作线程,而 Tomcat 的工作线程是基于线程池的
线程池会重用固定的几个线程,一旦线程重用,那么很可能首次从 ThreadLocal 获取的值是之前其他用户的请求遗留的值。这时,ThreadLocal 中的用户信息就是其他用户的信息
因为线程的创建比较昂贵,所以 Web 服务器往往会使用线程池来处理请求,这就意味着线程会被重用。这时,使用类似 ThreadLocal 工具来存放一些数据时,需要特别注意在代码运行完后,显式地去清空设置的数据。如果在代码中使用了自定义的线程池,也同样会遇到这个问题。
package cn.mesmile.admin.common.log;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.MDC;
import org.springframework.util.StringUtils;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.UUID;
/**
* @author zb
* @Description
*/
@Slf4j
public class RequestLogInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String servletPath = request.getServletPath();
// 生成RequestId
String requestId = this.getRequestId();
// 配置日志文件打印 REQUEST_ID
MDC.put("REQUEST_ID", requestId);
log.info("servletPath:{}", servletPath);
log.info("preHandle 前置处理----------");
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
log.info("preHandle 处理中----------");
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
String servletPath = request.getServletPath();
// 最后显示的 清空 ThreadLocal 设置的值
log.info("preHandle 后置处理----------");
log.info("servletPath:{}", servletPath);
RequestIdUtils.removeRequestId();
MDC.clear();
}
/**
* 获取RequestId
* 优先从header头获取,如果没有则自己生成
* @return RequestId
*/
private String getRequestId(){
// 因为如果有网关,则一般会从网关传递过来,所以优先从header头获取
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if(attributes != null && StringUtils.hasText(attributes.getRequest().getHeader("x-request-id"))) {
HttpServletRequest request = attributes.getRequest();
String requestId = request.getHeader("x-request-id");
UUID uuid = UUID.fromString(requestId);
RequestIdUtils.generateRequestId(uuid);
return requestId;
}
UUID existUUID = RequestIdUtils.getRequestId();
if(existUUID != null){
return existUUID.toString();
}
RequestIdUtils.generateRequestId();
return RequestIdUtils.getRequestId().toString();
}
}
注册拦截器
package cn.mesmile.admin.common.repeat;
import cn.mesmile.admin.common.log.RequestLogInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* @author zb
* @Description
*/
@Configuration(
proxyBeanMethods = false
)
public class WebInterceptorConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 日志请求id
registry.addInterceptor(new RequestLogInterceptor()).addPathPatterns("/**");
}
}
解决线程异步场景下RequestId的打印问题
程序运行在 Tomcat 中,执行程序的线程是 Tomcat 的工作线程,而 Tomcat 的工作线程是基于线程池的
线程池会重用固定的几个线程,一旦线程重用,那么很可能首次从 ThreadLocal 获取的值是之前其他用户的请求遗留的值。这时,ThreadLocal 中的用户信息就是其他用户的信息
因为线程的创建比较昂贵,所以 Web 服务器往往会使用线程池来处理请求,这就意味着线程会被重用。这时,使用类似 ThreadLocal 工具来存放一些数据时,需要特别注意在代码运行完后,显式地去清空设置的数据。如果在代码中使用了自定义的线程池,也同样会遇到这个问题。
package cn.mesmile.admin.common.log;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.MDC;
import java.util.concurrent.Executor;
/**
* @author zb
* @Description 解决线程异步场景下RequestId的打印问题
*/
@Slf4j
public class MdcExecutor implements Executor {
private Executor executor;
public MdcExecutor(Executor executor) {
this.executor = executor;
}
@Override
public void execute(Runnable command) {
final String requestId = MDC.get("REQUEST_ID");
executor.execute(() -> {
MDC.put("REQUEST_ID", requestId);
try {
command.run();
} finally {
MDC.remove("REQUEST_ID");
}
});
}
}
以logback为例在日志配置文件中加入以下内容:
日志格式中加入以下格式: [%X{REQUEST_ID}]
<!--控制台日志格式-->
<property name="CONSOLE_LOG_PATTERN" value="${CONSOLE_LOG_PATTERN:-%clr(%d{${LOG_DATEFORMAT_PATTERN:-yyyy-MM-dd HH:mm:ss.SSS}}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) [%X{REQUEST_ID}] %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}}"/>
<!--文件日志格式-->
<property name="FILE_LOG_PATTERN" value="${FILE_LOG_PATTERN:-%d{${LOG_DATEFORMAT_PATTERN:-yyyy-MM-dd HH:mm:ss.SSS}} ${LOG_LEVEL_PATTERN:-%5p} [%X{REQUEST_ID}] ${PID:- } --- [%t] %-40.40logger{39} : %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}}"/>
日志示例, 可以看见日志中的 tranceId 为 uuid 值:
2022-10-15 17:00:34.670 INFO [466aed02-0527-4829-8782-252e10a50ead] 7184 --- [ XNIO-1 task-1] c.m.a.common.log.RequestLogInterceptor : servletPath:/doc.html
2022-10-15 17:00:34.670 INFO [466aed02-0527-4829-8782-252e10a50ead] 7184 --- [ XNIO-1 task-1] c.m.a.common.log.RequestLogInterceptor : preHandle 前置处理----------
2022-10-15 17:00:34.689 INFO [466aed02-0527-4829-8782-252e10a50ead] 7184 --- [ XNIO-1 task-1] c.m.a.common.log.RequestLogInterceptor : preHandle 处理中----------
2022-10-15 17:00:34.689 INFO [466aed02-0527-4829-8782-252e10a50ead] 7184 --- [ XNIO-1 task-1] c.m.a.common.log.RequestLogInterceptor : preHandle 后置处理----------
2022-10-15 17:00:34.689 INFO [466aed02-0527-4829-8782-252e10a50ead] 7184 --- [ XNIO-1 task-1] c.m.a.common.log.RequestLogInterceptor : servletPath:/doc.html