Spring Cloud 全链路日志追踪实现

2022年11月8日11:26:13

一、Spring Cloud 全链路日志追踪实现

基本实现原理:

  1. 过滤所有请求:有Request-No请求头则获取请求号,没有请求头则设置请求头
  2. feign远程调用添加过滤器,自动赋值请求头
  3. 以上两步能够完成当前线程与子线程 参数传递,对于线程池则无能为力。对于线程池则需要手动设置于释放

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
  • 作者:java爱好者
  • 原文链接:https://lliujg.blog.csdn.net/article/details/125685852
    更新时间:2022年11月8日11:26:13 ,共 8605 字。