場景
在系統(tǒng)中译蒂,多個用戶的并發(fā)請求或者多個線程的日志會交纏在一起曼月,使得對日志的分析造成較大的困難,若能夠將單個用戶的請求柔昼,或者單個線程的請求用以不同的標識哑芹,在日志中標識出來,則能夠更好的分析日志岳锁。
思路
要對每個請求的日志加上唯一性標識绩衷,如果純靠手動加入,工作量大激率,修改麻煩咳燕,出現(xiàn)遺漏等等問題。而通過MDC可以解決上述問題乒躺。
本文是通過MDC來對每一個請求所經過代碼加入唯一性標識招盲,其中包含父子線程標識統(tǒng)一,feign跨服務調用日志統(tǒng)一等嘉冒,來打造該請求的完整日志鏈條曹货。
日志過濾器
首先通過過濾器來對每個請求加入唯一性標識。
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觸發(fā)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中的數(shù)據復制給子線程
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中的數(shù)據復制給子線程
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中的數(shù)據不同線程是不共享的,因此需要將父線程中的MDC數(shù)據傳入子線程中究驴。
構建一個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 封裝線程池任務執(zhí)行器镊绪,在任務提交時,會將父線程的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));
}
}