一、Spring Cloud 全链路日志追踪实现
基本实现原理:
- 过滤所有请求:有Request-No请求头则获取请求号,没有请求头则设置请求头
- feign远程调用添加过滤器,自动赋值请求头
- 以上两步能够完成当前线程与子线程 参数传递,对于线程池则无能为力。对于线程池则需要手动设置于释放
1. 过滤并设置请求头
对于不携带Request-No的请求,则生成并添加请求头,添加请求头需要包装请求对象
packagecom.aimilin.common.security.filter;importcom.aimilin.common.core.consts.CommonConstant;importcom.aimilin.common.core.context.requestno.RequestNoContext;importorg.apache.commons.lang3.StringUtils;importorg.slf4j.MDC;importjavax.servlet.*;importjavax.servlet.http.HttpServletRequest;importjavax.servlet.http.HttpServletResponse;importjava.io.IOException;importjava.util.UUID;/**
* 对请求生成唯一编码
*
*/publicclassRequestNoFilterimplementsFilter{@Overridepublicvoidinit(FilterConfig filterConfig)throwsServletException{}@OverridepublicvoiddoFilter(ServletRequest request,ServletResponse response,FilterChain chain)throwsIOException,ServletException{try{// 生成唯一请求号uuidString requestNo=this.getRequestNo(request);
request=this.setRequestHeader(request, requestNo);// 增加响应头的请求号HttpServletResponse httpServletResponse=(HttpServletResponse) response;
httpServletResponse.addHeader(CommonConstant.REQUEST_NO_HEADER_NAME, requestNo);// 临时存储RequestNoContext.set(requestNo);
MDC.put(CommonConstant.TRACE_ID, requestNo);// 放开请求
chain.doFilter(request, response);}finally{// 清除临时存储的唯一编号RequestNoContext.clear();
MDC.remove(CommonConstant.TRACE_ID);}}/**
* 添加请求头请求号
* @param request 请求对象
* @param requestNo 编号
* @return 结果
*/privateServletRequestsetRequestHeader(ServletRequest request,String requestNo){// 没有包含请求号,则将请求号添加到请求头中if(requestinstanceofHttpServletRequest&&StringUtils.isBlank(((HttpServletRequest) request).getHeader(CommonConstant.REQUEST_NO_HEADER_NAME))){HeaderMapRequestWrapper requestWrapper=newHeaderMapRequestWrapper((HttpServletRequest) request);
requestWrapper.addHeader(CommonConstant.REQUEST_NO_HEADER_NAME, requestNo);return requestWrapper;}return request;}/**
* 获取请求编号
* @return String
*/privateStringgetRequestNo(ServletRequest request){if(requestinstanceofHttpServletRequest){String requestNo=((HttpServletRequest) request).getHeader(CommonConstant.REQUEST_NO_HEADER_NAME);if(StringUtils.isNotBlank(requestNo)){return requestNo;}}return UUID.randomUUID().toString();}@Overridepublicvoiddestroy(){}}
包装请求对象:
packagecom.aimilin.common.security.filter;importjavax.servlet.http.HttpServletRequest;importjavax.servlet.http.HttpServletRequestWrapper;importjava.util.*;/**
* 添加请求头的请求对象
*
*/publicclassHeaderMapRequestWrapperextendsHttpServletRequestWrapper{publicHeaderMapRequestWrapper(HttpServletRequest request){super(request);}/**
* 请求头对象封装
*/privateMap<String,String> headerMap=newHashMap<String,String>();/**
* 自定义添加请求头
* @param name key
* @param value value
*/publicvoidaddHeader(String name,String value){
headerMap.put(name, value);}/**
* 获取请求头,先获取原始请求头信息
* @param name 名称
* @return 结果
*/@OverridepublicStringgetHeader(String name){String headerValue=super.getHeader(name);if(headerMap.containsKey(name)){
headerValue= headerMap.get(name);}return headerValue;}/**
* 获取所有请求头
* @return Enumeration
*/@OverridepublicEnumeration<String>getHeaderNames(){List<String> names=Collections.list(super.getHeaderNames());
names.addAll(headerMap.keySet());returnCollections.enumeration(names);}/**
* 获取指定请求头
* @param name 名称
* @return 结果
*/@OverridepublicEnumeration<String>getHeaders(String name){List<String> values=Collections.list(super.getHeaders(name));if(headerMap.containsKey(name)){
values.add(headerMap.get(name));}returnCollections.enumeration(values);}}
经过请求头过滤器那么所有请求都会携带上Request-No请求头, 响应也会携带上Request-No
2. Feigin远程调用,自动复制请求头
packagecom.aimilin.common.feign.inteceptor;importcom.aimilin.common.core.utils.JsonUtil;importfeign.RequestInterceptor;importfeign.RequestTemplate;importfeign.Target;importlombok.extern.slf4j.Slf4j;importorg.springframework.web.bind.annotation.RequestMethod;importorg.springframework.web.context.request.RequestContextHolder;importorg.springframework.web.context.request.ServletRequestAttributes;importjavax.servlet.http.HttpServletRequest;importjava.util.Enumeration;importjava.util.Objects;importjava.util.Optional;/**
* 模块间调用转移请求头
* @classname : FeignRequestInterceptor
* @description : Feign请求拦截器
*/@Slf4jpublicclassFeignRequestInterceptorimplementsRequestInterceptor{@Overridepublicvoidapply(RequestTemplate requestTemplate){ServletRequestAttributes attributes=(ServletRequestAttributes)RequestContextHolder.getRequestAttributes();if(attributes!=null){HttpServletRequest request= attributes.getRequest();Enumeration<String> headerNames= request.getHeaderNames();Optional.ofNullable(headerNames).ifPresent(headers->{while(headers.hasMoreElements()){String name= headers.nextElement();String value= request.getHeader(name);// 跳过 content-lengthif(name.equals("content-length")){continue;}
requestTemplate.header(name, value);
log.trace(">>> feign header set {}:{}", name, value);}});}printLog(requestTemplate);}privatevoidprintLog(RequestTemplate requestTemplate){if(log.isInfoEnabled()){Target<?> target= requestTemplate.feignTarget();
log.info("Feign Request:\n app: {}\n class: {}\n method: {}\n url: {}\n param: {}\n",
target.name(), target.type().getName(), requestTemplate.method(),
requestTemplate.url(),this.getParam(requestTemplate));}}privateStringgetParam(RequestTemplate requestTemplate){if(RequestMethod.GET.name().equals(requestTemplate.method())){returnJsonUtil.toJson(requestTemplate.queries());}returnObjects.isNull(requestTemplate.body())?"":newString(requestTemplate.body());}}
对于线程池中执行的任务还是不能携带MDC和请求对象,因为RequestContextHolder也只能在当前线程与子线程中使用Request对象;
3. 线程池中使用MDC与Request对象
其原理就是任务执行前复制好变量,结束之后再删除变量。这里使用了alibaba TransmittableThreadLocal 线程池支持库;
importcom.alibaba.ttl.TtlCallable;importcom.alibaba.ttl.TtlRunnable;importlombok.NonNull;importorg.slf4j.MDC;importorg.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;importorg.springframework.util.concurrent.ListenableFuture;importorg.springframework.web.context.request.RequestAttributes;importorg.springframework.web.context.request.RequestContextHolder;importjava.util.Map;importjava.util.Optional;importjava.util.concurrent.Callable;importjava.util.concurrent.Future;importstaticjava.util.Optional.ofNullable;/**
* 线程池传递参数
*
*/publicclassTtlThreadPoolTaskExecutorextendsThreadPoolTaskExecutor{@NonNull@Overridepublicvoidexecute(@NonNullRunnable task){super.execute(wrap(task));}@NonNull@OverridepublicFuture<?>submit(@NonNullRunnable task){returnsuper.submit(wrap(task));}@NonNull@Overridepublic<T>Future<T>submit(@NonNullCallable<T> task){returnsuper.submit(wrap(task));}@NonNull@OverridepublicListenableFuture<?>submitListenable(@NonNullRunnable task){returnsuper.submitListenable(wrap(task));}@NonNull@Overridepublic<T>ListenableFuture<T>submitListenable(@NonNullCallable<T> task){returnsuper.submitListenable(wrap(task));}/**
* 保存MDC服务类
* @param task 任务
* @return 结果
* @param <T> 泛型
*/private<T>Callable<T>wrap(Callable<T> task){Optional<RequestAttributes> requestOptional=ofNullable(RequestContextHolder.getRequestAttributes());Optional<Map<String,String>> mdcOptional=ofNullable(MDC.getCopyOfContextMap());return()->{try{
requestOptional.ifPresent(RequestContextHolder::setRequestAttributes);
mdcOptional.ifPresent(MDC::setContextMap);returnTtlCallable.get(task).call();}finally{
MDC.clear();RequestContextHolder.resetRequestAttributes();}};}/**
* 包装MDC Runnable
* @param task 任务
* @return 结果
*/privateRunnablewrap(Runnable task){Optional<RequestAttributes> requestOptional=ofNullable(RequestContextHolder.getRequestAttributes());Optional<Map<String,String>> mdcOptional=ofNullable(MDC.getCopyOfContextMap());return()->{try{
requestOptional.ifPresent(RequestContextHolder::setRequestAttributes);
mdcOptional.ifPresent(MDC::setContextMap);TtlRunnable.get(task).run();}finally{
MDC.clear();RequestContextHolder.resetRequestAttributes();}};}}
异步现成初始化
/**
* 异步线程池构建
* @param builder 构建起
* @return 线程池执行器
*/@Lazy@Bean(name={TaskExecutionAutoConfiguration.APPLICATION_TASK_EXECUTOR_BEAN_NAME,AsyncAnnotationBeanPostProcessor.DEFAULT_TASK_EXECUTOR_BEAN_NAME})publicThreadPoolTaskExecutorapplicationTaskExecutor(TaskExecutorBuilder builder){ThreadPoolTaskExecutor threadPoolTaskExecutor= builder.configure(newTtlThreadPoolTaskExecutor());// CALLER_RUNS:调用者所在的线程来执行
threadPoolTaskExecutor.setRejectedExecutionHandler(newThreadPoolExecutor.CallerRunsPolicy());return threadPoolTaskExecutor;}
经过上面几部,基本完成链路日志追踪,接下来是设置MDC
4. 设置日志MDC
添加配置:主要是设置控制台日志格式与文件日志格式。
<?xml version="1.0" encoding="UTF-8"?><!--
Default logback configuration provided for import
--><included><propertyname="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}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(%3.3line){cyan} [%clr(%X{trace_id}){blue}] %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}}"/><propertyname="FILE_LOG_PATTERN"value