开发过程中难免遇到需要查看日志来找出问题出在哪一环节的情况,而在实际情况中服务之间互相调用所产生的日志冗长且复杂,若是再加上同一时间别的请求所产生的日志,想要精准定位自己想要查看的日志就比较麻烦。为解决此问题,遂使用MDC日志追踪。
MDC(Mapped Diagnostic Context,映射调试上下文)是 log4j 和 logback 提供的一种方便在多线程条件下记录日志的功能。MDC 可以看成是一个与当前线程绑定的Map,可以往其中添加键值对。MDC 中包含的内容可以被同一线程中执行的代码所访问。当前线程的子线程会继承其父线程中的 MDC 的内容。当需要记录日志时,只需要从 MDC 中获取所需的信息即可。MDC 的内容则由程序在适当的时候保存进去。对于一个 Web 应用来说,通常是在请求被处理的最开始保存这些数据。
clear() :移除所有MDC
get (String key) :获取当前线程MDC中指定key的值
put(String key, Object o) :往当前线程的MDC中存入指定的键值对
getContext() :获取当前线程MDC的MDC
remove(String key) :删除当前线程MDC中指定的键值对
本次测试用的prs-business和prs-data服务,从business服务调data服务,分别调用了四次。可以看到每次请求输出的日志前面都缀有[TRACEID:UUID 数字 域名]的标记,每次调用UUID不变,数字会+1,因为两个服务都是本地起的所以都是localhost,放在云上会变成真正的域名。
首先创建过滤器,在第一次请求到达时生成traceId放入MDC中伴随这次请求的自始至终。或被其他服务调用时,拿到请求中传过来的traceId放入MDC中进行处理。
@Order(0)//告知spring创建对象的等级,0为最高级
@Component//声明component
@WebFilter(filterName = "requestIdFilter", urlPatterns = "/**")
public class RequestIdFilter implements Filter {//接收别的服务的请求时,拿出传过来的traceid,或者生成新的traceid,加入mdc,进行后续的链路追踪。
private static final String REQUEST_ID = "requestId";//traceId的key统一设为requestId。
@Override
public void init(FilterConfig filterConfig) {
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
try {
//获取并设置RequestId
MDC.put(REQUEST_ID, this.getRequestId(httpServletRequest));//向MDC存traceID
chain.doFilter(httpServletRequest, httpServletResponse);
} finally {
MDC.clear();
}
}
@Override
public void destroy() {
}
/**
* 获取请求RequestId
*
* @param httpServletRequest 请求
* @return requestId
*/
private String getRequestId(HttpServletRequest httpServletRequest) {
String requestId = httpServletRequest.getHeader(REQUEST_ID);
if (StringUtils.isBlank(requestId)) {//若请求中没有traceId,为空,意为是请求的最开始,则按照咱们咱们自己定的"UUID 0 域名"的格式生成一个全新的traceId,其中UUID就是以后用来查找日志的主要途径(关于这次请求的所有日志都会含有这个UUID,0表示是该次请求的最开始,域名的话可以区分是哪个服务,是测试版还是正式版。
requestId = UUID.randomUUID().toString().replace("-", "")+" "+0+" "+httpServletRequest.getServerName();
}else {//若请求中有traceId,不为空,意为是有别的服务调用了本服务发来了请求,需要对请求再处理一下。UUID不会变,承接传过来的UUID保证统一。中间的数字要+1,表示请求到达了新的接口。域名作用不变。
String[] s = requestId.split(" ");//按照空格分隔进行处理
s[1] = String.valueOf(Integer.parseInt(s[1]) + 1);
requestId = s[0]+" "+s[1]+" "+httpServletRequest.getServerName();//处理好后重新拼接。
}
return requestId;
}
}
HTTP调用第三方服务接口时traceId丢失,需要在发送请求时在Request Header中添加traceId,传给被调用方。
//调用别的服务时,给请求中加入已经生成好的traceid,发送请求,进行链路追踪
public class RestTemplateTraceIdInterceptor implements ClientHttpRequestInterceptor {
private static final String REQUEST_ID = "requestId";
@Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
String traceId= MDC.get(REQUEST_ID);
request.getHeaders().set(REQUEST_ID, traceId);
return execution.execute(request, body);
}
}
代码中proxy调用其他服务时,需要将拦截器添加入RestTemplate 中。
public DefaultResponse test() {
//String url = prsDataConfiguration.getEndpoint() + prsDataConfiguration.getTest();
String url = "http://localhost:8006/mdc/mdctest";
RestTemplate restTemplate = new RestTemplate();
restTemplate.setInterceptors(Collections.singletonList(new RestTemplateTraceIdInterceptor()));//在proxy调用的时,将拦截器添加进去。
return restTemplate.getForObject(url, DefaultResponse.class);
}
将下面标签添加到xml文件中的<Properties>Properties>标签中,次标签规定了日志中traceId的显示格式。注意:name="PATTERN" 看看名字是否有重复的,以防出现差错。
[TRACEID:%X{requestId}] %d{HH:mm:ss.SSS} %-5level %class{-1}.%M(%L) - %msg%xEx%n
由于MDC底层实现是基于Threadlocal 实现的,由于在Filter配置的requestId,只属于主线程的上下文,如果在主线程中使用了线程池,开启了子线程,由于主、子线程MDC是相互隔离的,所以子线程中X-X{requestId}将无法获取到主线程filter中设置的requestId的,所以采用了一种比较巧妙的方式,因为每个线程最终都会执行 Runable,run方法,所以这里只需包装一下Runable,run方法结束之后清理MDC。
//重写线程池,使用ThreadMdcUtil重新进行封装,并在启动类中调用。为了将traceid加入到线程中,并显示在滚动日志中。
public class ThreadPoolExecutorMdcWrapper extends ThreadPoolExecutor {
public ThreadPoolExecutorMdcWrapper(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue workQueue, RejectedExecutionHandler handler) {
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, handler);
}
@Override
public void execute(Runnable task) {
super.execute(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap()));
}
@Override
public Future submit(Runnable task, T result) {
return super.submit(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap()), result);
}
@Override
public Future submit(Callable task) {
return super.submit(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap()));
}
@Override
public Future> submit(Runnable task) {
return super.submit(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap()));
}
}
public class ThreadMdcUtil {
public static Callable wrap(final Callable callable, final Map context) {
return () -> {
if (context == null) {
MDC.clear();
} else {
MDC.setContextMap(context);
}
try {
return callable.call();
} finally {
MDC.clear();
}
};
}
public static Runnable wrap(final Runnable runnable, final Map context) {
return () -> {
if (context == null) {
MDC.clear();
} else {
MDC.setContextMap(context);
}
try {
runnable.run();
} finally {
MDC.clear();//最终清理MDC
}
};
}
}
@Bean(name = "global Executor")//启动类中给封装好的线程池创建bean对象启动。
public ExecutorService getVendorInfoExecutor() {
return new ThreadPoolExecutorMdcWrapper(10, 15, 0, TimeUnit.SECONDS,
new LinkedBlockingDeque<>(10), new ThreadPoolExecutor.CallerRunsPolicy());
}