场景
在系统中,多个用户的并发请求或者多个线程的日志会交缠在一起,使得对日志的分析造成较大的困难,若能够将单个用户的请求,或者单个线程的请求用以不同的标识,在日志中标识出来,则能够更好的分析日志。
思路
要对每个请求的日志加上唯一性标识,如果纯靠手动加入,工作量大,修改麻烦,出现遗漏等等问题。而通过MDC可以解决上述问题。
本文是通过MDC来对每一个请求所经过代码加入唯一性标识,其中包含父子线程标识统一,feign跨服务调用日志统一等,来打造该请求的完整日志链条。
日志过滤器
首先通过过滤器来对每个请求加入唯一性标识。
import cn.hutool.core.util.StrUtil;
import com.demo.util.RequestIdMdcUtil;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.MDC;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Order(Ordered.HIGHEST_PRECEDENCE)
@WebFilter(filterName = "LogFilter", urlPatterns = "/*")
@Slf4j
public class LogFilter implements Filter {
@Override
public void doFilter(final ServletRequest req, final ServletResponse res, final FilterChain chain) throws IOException, ServletException {
final HttpServletResponse response = (HttpServletResponse) res;
final HttpServletRequest reqs = (HttpServletRequest) req;
String requestId = reqs.getHeader(RequestIdMdcUtil.REQUEST_ID);
if (StrUtil.isBlank(requestId)) {
requestId = RequestIdMdcUtil.getRequestId();
}
MDC.put(RequestIdMdcUtil.REQUEST_ID, requestId);
log.info("LogFilter触发request_id:{}",MDC.get(RequestIdMdcUtil.REQUEST_ID));
try{
chain.doFilter(req, res);
}finally {
MDC.remove(RequestIdMdcUtil.REQUEST_ID);
}
}
@Override
public void init(final FilterConfig filterConfig) {
log.info("LogFilter初始化");
}
@Override
public void destroy() {
MDC.remove(RequestIdMdcUtil.REQUEST_ID);
}
}
唯一标识工具类
import org.slf4j.MDC;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.Callable;
public class RequestIdMdcUtil {
public static final String REQUEST_ID = "request_id";
// 获取唯一性标识
public static String getRequestId(){
return UUID.randomUUID().toString();
}
public static void setRequestIdIfAbsent() {
if (MDC.get(REQUEST_ID) == null) {
MDC.put(REQUEST_ID, getRequestId());
}
}
// 用于父线程向线程池中提交任务时,将自身MDC中的数据复制给子线程
public static <T> Callable<T> wrap(final Callable<T> callable, final Map<String, String> context) {
return () -> {
if (context == null) {
MDC.clear();
} else {
MDC.setContextMap(context);
}
setRequestIdIfAbsent();
try {
return callable.call();
} finally {
MDC.clear();
}
};
}
// 用于父线程向线程池中提交任务时,将自身MDC中的数据复制给子线程
public static Runnable wrap(final Runnable runnable, final Map<String, String> context) {
return () -> {
if (context == null) {
MDC.clear();
} else {
MDC.setContextMap(context);
}
setRequestIdIfAbsent();
try {
runnable.run();
} finally {
MDC.clear();
}
};
}
}
日志格式设置
logging.pattern.console=[REQUESTID:%X{request_id}]%d{yyyy-MM-dd HH:mm:ss} [%t] [%-5level] %logger{96} [%M:%L] - %msg%n
以上设置,即完成了简单场景的设置
使用了线程池的场景
使用了线程池的场景,主要问题是,MDC中的数据不同线程是不共享的,因此需要将父线程中的MDC数据传入子线程中。
构建一个ThreadPoolTaskExecutor的子类
import com.demo.util.RequestIdMdcUtil;
import org.slf4j.MDC;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.Callable;
import java.util.concurrent.Future;
/**
* @description 封装线程池任务执行器,在任务提交时,会将父线程的request_id,带入子线程
* @author
* @date 2022/3/3 17:35
*/
public class ThreadPoolMdcTaskExecutor extends ThreadPoolTaskExecutor {
public ThreadPoolMdcTaskExecutor() {
super();
}
@Override
public void execute(Runnable task) {
super.execute(RequestIdMdcUtil.wrap(task, MDC.getCopyOfContextMap()));
}
@Override
public <T> Future<T> submit(Callable<T> task) {
return super.submit(RequestIdMdcUtil.wrap(task, MDC.getCopyOfContextMap()));
}
@Override
public Future<?> submit(Runnable task) {
return super.submit(RequestIdMdcUtil.wrap(task, MDC.getCopyOfContextMap()));
}
}
在新建线程池的时候,new的是子类ThreadPoolMdcTaskExecutor即可。
跨服务feign调用场景
跨服务调用场景和父子线程场景类似,需要将MDC中的request_id加入到header中,然后在另一个服务中的过滤器会取出它,加入到MDC中。
@Configuration
@Slf4j
public class FeignConfig implements RequestInterceptor {
@Override
public void apply(final RequestTemplate template) {
//将MDC中request_id传入header
template.header(RequestIdMdcUtil.REQUEST_ID, MDC.get(RequestIdMdcUtil.REQUEST_ID));
}
}
网友评论