zl程序教程

您现在的位置是:首页 >  后端

当前栏目

Spring Cloud 全链路日志追踪实现

SpringCloud日志 实现 追踪 链路
2023-09-11 14:16:58 时间

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

基本实现原理:

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

1. 过滤并设置请求头

对于不携带Request-No的请求,则生成并添加请求头,添加请求头需要包装请求对象

package com.aimilin.common.security.filter;


import com.aimilin.common.core.consts.CommonConstant;
import com.aimilin.common.core.context.requestno.RequestNoContext;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.MDC;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.UUID;

/**
 * 对请求生成唯一编码
 *
 */
public class RequestNoFilter implements Filter {

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {

    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        try {
            // 生成唯一请求号uuid
            String 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 结果
     */
    private ServletRequest setRequestHeader(ServletRequest request, String requestNo) {
        // 没有包含请求号,则将请求号添加到请求头中
        if(request instanceof HttpServletRequest && StringUtils.isBlank(((HttpServletRequest) request).getHeader(CommonConstant.REQUEST_NO_HEADER_NAME))){
            HeaderMapRequestWrapper requestWrapper = new HeaderMapRequestWrapper((HttpServletRequest) request);
            requestWrapper.addHeader(CommonConstant.REQUEST_NO_HEADER_NAME, requestNo);
            return requestWrapper;
        }
        return request;
    }

    /**
     * 获取请求编号
     * @return String
     */
    private String getRequestNo(ServletRequest request){
        if(request instanceof HttpServletRequest){
            String requestNo = ((HttpServletRequest) request).getHeader(CommonConstant.REQUEST_NO_HEADER_NAME);
            if(StringUtils.isNotBlank(requestNo)){
                return requestNo;
            }
        }
        return UUID.randomUUID().toString();
    }

    @Override
    public void destroy() {

    }

}

包装请求对象:

package com.aimilin.common.security.filter;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.util.*;

/**
 * 添加请求头的请求对象
 *
 */
public class HeaderMapRequestWrapper extends HttpServletRequestWrapper {
    public HeaderMapRequestWrapper(HttpServletRequest request) {
        super(request);
    }

    /**
     * 请求头对象封装
     */
    private Map<String, String> headerMap = new HashMap<String, String>();

    /**
     * 自定义添加请求头
     * @param name key
     * @param value value
     */
    public void addHeader(String name, String value) {
        headerMap.put(name, value);
    }

    /**
     * 获取请求头,先获取原始请求头信息
     * @param name 名称
     * @return 结果
     */
    @Override
    public String getHeader(String name) {
        String headerValue = super.getHeader(name);
        if (headerMap.containsKey(name)) {
            headerValue = headerMap.get(name);
        }
        return headerValue;
    }

    /**
     * 获取所有请求头
     * @return Enumeration
     */
    @Override
    public Enumeration<String> getHeaderNames() {
        List<String> names = Collections.list(super.getHeaderNames());
        names.addAll(headerMap.keySet());
        return Collections.enumeration(names);
    }

    /**
     * 获取指定请求头
     * @param name 名称
     * @return 结果
     */
    @Override
    public Enumeration<String> getHeaders(String name) {
        List<String> values = Collections.list(super.getHeaders(name));
        if (headerMap.containsKey(name)) {
            values.add(headerMap.get(name));
        }
        return Collections.enumeration(values);
    }
}

经过请求头过滤器那么所有请求都会携带上Request-No请求头, 响应也会携带上Request-No

2. Feigin远程调用,自动复制请求头

package com.aimilin.common.feign.inteceptor;

import com.aimilin.common.core.utils.JsonUtil;
import feign.RequestInterceptor;
import feign.RequestTemplate;
import feign.Target;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.util.Enumeration;
import java.util.Objects;
import java.util.Optional;

/**
 * 模块间调用转移请求头
 * @classname : FeignRequestInterceptor
 * @description : Feign请求拦截器
 */
@Slf4j
public class FeignRequestInterceptor implements RequestInterceptor {

    @Override
    public void apply(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-length
                    if (name.equals("content-length")){
                        continue;
                    }

                    requestTemplate.header(name, value);
                    log.trace(">>> feign header set {}:{}", name, value);
                }
            });
        }
        printLog(requestTemplate);
    }

    private void printLog(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)
            );
        }
    }

    private String getParam(RequestTemplate requestTemplate) {
        if (RequestMethod.GET.name().equals(requestTemplate.method())) {
            return JsonUtil.toJson(requestTemplate.queries());
        }
        return Objects.isNull(requestTemplate.body()) ? "" : new String(requestTemplate.body());
    }
}

对于线程池中执行的任务还是不能携带MDC和请求对象,因为RequestContextHolder也只能在当前线程与子线程中使用Request对象;

3. 线程池中使用MDC与Request对象

其原理就是任务执行前复制好变量,结束之后再删除变量。这里使用了alibaba TransmittableThreadLocal 线程池支持库;

import com.alibaba.ttl.TtlCallable;
import com.alibaba.ttl.TtlRunnable;
import lombok.NonNull;
import org.slf4j.MDC;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.util.concurrent.ListenableFuture;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;

import java.util.Map;
import java.util.Optional;
import java.util.concurrent.Callable;
import java.util.concurrent.Future;

import static java.util.Optional.ofNullable;

/**
 * 线程池传递参数
 *
 */
public class TtlThreadPoolTaskExecutor extends ThreadPoolTaskExecutor {
    @NonNull
    @Override
    public void execute(@NonNull Runnable task) {
        super.execute(wrap(task));
    }

    @NonNull
    @Override
    public Future<?> submit(@NonNull Runnable task) {
        return super.submit(wrap(task));
    }

    @NonNull
    @Override
    public <T> Future<T> submit(@NonNull Callable<T> task) {
        return super.submit(wrap(task));
    }

    @NonNull
    @Override
    public ListenableFuture<?> submitListenable(@NonNull Runnable task) {
        return super.submitListenable(wrap(task));
    }

    @NonNull
    @Override
    public <T> ListenableFuture<T> submitListenable(@NonNull Callable<T> task) {
        return super.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);
                return TtlCallable.get(task).call();
            } finally {
                MDC.clear();
                RequestContextHolder.resetRequestAttributes();
            }
        };
    }

    /**
     * 包装MDC Runnable
     * @param task 任务
     * @return 结果
     */
    private Runnable wrap(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 })
    public ThreadPoolTaskExecutor applicationTaskExecutor(TaskExecutorBuilder builder) {
        ThreadPoolTaskExecutor threadPoolTaskExecutor = builder.configure(new TtlThreadPoolTaskExecutor());
        // CALLER_RUNS:调用者所在的线程来执行
        threadPoolTaskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        return threadPoolTaskExecutor;
    }

经过上面几部,基本完成链路日志追踪,接下来是设置MDC

4. 设置日志MDC

添加配置:主要是设置控制台日志格式与文件日志格式。

<?xml version="1.0" encoding="UTF-8"?>

<!--
Default logback configuration provided for import
-->

<included>
    <property name="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}}"/>
    <property name="FILE_LOG_PATTERN" value="${FILE_LOG_PATTERN:-%d{${LOG_DATEFORMAT_PATTERN:-yyyy-MM-dd HH:mm:ss.SSS}} ${LOG_LEVEL_PATTERN:-%5p} ${PID:- } --- [%t] %-40.40logger{39} %3.3line [%X{trace_id}] : %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}}"/>
</included>

使用方式:
参考:https://docs.spring.io/spring-boot/docs/2.6.x/reference/htmlsingle/#using

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <!-- 我们自定义的日志输出格式 -->
    <include resource="com/aimilin/logging/logback/my-defaults.xml"/>
    <include resource="org/springframework/boot/logging/logback/defaults.xml"/>
    <include resource="org/springframework/boot/logging/logback/console-appender.xml" />
    <root level="INFO">
        <appender-ref ref="CONSOLE" />
    </root>
    <logger name="org.springframework.web" level="DEBUG"/>
</configuration>

打印出来日志格式:
在这里插入图片描述