天天看点

日志链路追踪-springboot/logback/线程传递

为什么要有日志的链路追踪?

每一次调用, 都打一个标签:traceId. 这个traceId贯穿整个调用过程. 如果出错. 可以通过这个traceId获取整个调用链路的日志. 帮助快速定位错误信息;

本次博客场景不是使用微服务架构, 而是简单的springboot项目. 通过http, oncall系统调用provide系统

默认你有springboot基础.

调用流程:

浏览器–>oncall系统–>provide系统.

首先看oncall系统.

先写一个过滤器, 对每次调用都添加一个traceId

package com.example.traceoncall.filter;

import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.StrUtil;
import org.slf4j.MDC;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Optional;

/**
 * 添加过滤器 将请求绑定 traceId
 *
 * @author: Lenovo
 * @date: 2021/8/4
 * @description:
 */
@Component
public class TraceIdFilter extends OncePerRequestFilter {


    @Override
    protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse,
                                    FilterChain filterChain) throws ServletException, IOException {

        try {
            fetchTraceId(httpServletRequest);
            filterChain.doFilter(httpServletRequest, httpServletResponse);
        } finally {
            MDC.clear();
        }
    }


    /**
     * 填充traceId
     * @param httpServletRequest
     */
    private void fetchTraceId(HttpServletRequest httpServletRequest) {
        String traceId =
                Optional.ofNullable(httpServletRequest).map(httpServletRequest1 -> httpServletRequest1.getHeader(CommonConstant.LOG_TRACE_ID)).orElse("");
        if (StrUtil.isBlank(traceId)) {
            traceId = IdUtil.fastSimpleUUID();
        }
        MDC.put(CommonConstant.LOG_TRACE_ID, traceId);
    }

}

           

其中 MDC 本质是一个ThreadLocal,线程局部副本. 这个MDC可以在logback.xml中使用. 下面看一下logback.xml部分细节

<!-- 时间滚动输出 level为 INFO 日志 -->
    <appender name="Info" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <!-- 正在记录的日志文件的路径及文件名 -->
        <file>${log.path}/info.log</file>
        <!--日志文件输出格式-->
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%X{traceId}] [%thread] %-5level %logger{50} - %msg%n</pattern>
            <charset>UTF-8</charset>
        </encoder>
        <!-- 日志记录器的滚动策略,按日期,按大小记录 -->
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <!-- 每天日志归档路径以及格式 -->
            <fileNamePattern>${log.path}/info-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
            <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                <maxFileSize>500MB</maxFileSize>
            </timeBasedFileNamingAndTriggeringPolicy>
            <!--日志文件保留天数-->
            <maxHistory>15</maxHistory>
        </rollingPolicy>
        <!-- 此日志文件只记录info级别的 -->
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>info</level>
            <onMatch>ACCEPT</onMatch>
            <onMismatch>DENY</onMismatch>
        </filter>
    </appender>
           

上面有一个 [%X{traceId}] 就是我们之前在filter中设置进去的.

在oncall的业务方法中. http调用provide系统时. 构建request时需要把traceId 放到Header中;

String traceId = MDC.get(CommonConstant.LOG_TRACE_ID);
                HttpRequest httpRequest = HttpRequest.post("localhost:8082/trace/provide");
                if (StrUtil.isNotBlank(traceId)) {
                    httpRequest.header(CommonConstant.LOG_TRACE_ID, traceId);
                }
           

再写一个Advice 统一处理返回值.

package com.example.traceoncall.filter;

import com.example.traceoncall.result.ResultJson;
import org.slf4j.MDC;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;

/**
 * 处理response统一返回的
 *
 * @author: Lenovo
 * @date: 2021/8/5
 * @description:
 */
@ControllerAdvice
public class MyControllerAdvice implements ResponseBodyAdvice<Object> {
    @Override
    public boolean supports(MethodParameter methodParameter, Class<? extends HttpMessageConverter<?>> aClass) {
        return true;
    }

    @Override
    public Object beforeBodyWrite(Object body, MethodParameter methodParameter, MediaType mediaType, Class<?
            extends HttpMessageConverter<?>> aClass, ServerHttpRequest serverHttpRequest,
                                  ServerHttpResponse serverHttpResponse) {
        if (body instanceof ResultJson) {
            ResultJson data = (ResultJson) body;
            data.setTraceId(getTraceId());
            return data;
        }

        return body;
    }

    private String getTraceId() {
        return MDC.get(CommonConstant.LOG_TRACE_ID);
    }
}

           

这时候就已经完成oncall.

provide系统中

写一个跟oncall里一样的过滤器;就ok了. 不用其他的配置;

普通的调用就已经完成了;

下面说一下线程之间的传递.

  1. 使用@Async注解的;

    需要我们自己配置下 线程池

package com.example.traceoncall.threadpool;

/**
 * @author: Lenovo
 * @date: 2021/8/4
 * @description:
 */

import lombok.extern.slf4j.Slf4j;
import org.apache.tomcat.util.threads.ThreadPoolExecutor;
import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.AsyncConfigurer;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

import java.util.concurrent.Executor;

/**
 * @description: 执行异步任务线程池
 * @author LinYa
 */
@Slf4j
@Configuration
public class AsyncConfiguration implements AsyncConfigurer {
    @Bean("kingAsyncExecutor")
    public ThreadPoolTaskExecutor executor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        int corePoolSize = 10;
        executor.setCorePoolSize(corePoolSize);
        int maxPoolSize = 50;
        executor.setMaxPoolSize(maxPoolSize);
        int queueCapacity = 10;
        executor.setQueueCapacity(queueCapacity);
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        String threadNamePrefix = "Async-Executor-";
        executor.setThreadNamePrefix(threadNamePrefix);
        executor.setTaskDecorator(new MDCTaskDecorator());
        executor.setWaitForTasksToCompleteOnShutdown(true);
        // 使用自定义的跨线程的请求级别线程工厂类19
        int awaitTerminationSeconds = 60;
        executor.setAwaitTerminationSeconds(awaitTerminationSeconds);
        executor.initialize();

        return executor;
    }

    @Override
    public Executor getAsyncExecutor() {
        return executor();
    }

    @Override
    public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
        return (ex, method, params) -> log.error(String.format("执行异步任务'%s'", method), ex);
    }
}
           
上面最主要的是executor.setTaskDecorator(new MDCTaskDecorator()); 给线程池设置一个装饰器
           

具体的装饰器代码:

package com.example.traceoncall.threadpool;

import org.slf4j.MDC;
import org.springframework.core.task.TaskDecorator;
import org.springframework.stereotype.Component;

import java.util.Map;

/**
 * @author: Lenovo
 * @date: 2021/8/4
 * @description:
 */
@Component
public class MDCTaskDecorator implements TaskDecorator {
    @Override
    public Runnable decorate(Runnable runnable) {
        Map<String, String> contextMap = MDC.getCopyOfContextMap();
        return new Runnable() {
            @Override
            public void run() {
                try {
                    MDC.setContextMap(contextMap);
                    runnable.run();
                } finally {
                    MDC.clear();
                }
            }
        };
    }
}

           

上面主要作用是,将父线程的mdc参数 传递当子线程中去;

使用@Async时:

@Async("kingAsyncExecutor")
    public void test() {
        Thread thread = Thread.currentThread();
        log.info("threadName===="+thread.getName());
        log.info("=======Async========");
    }
           
  1. 使用自定义线程池时.
int coreSize = threadNormalEnum.getCoreSize();
        int maxSize = threadNormalEnum.getMaxSize();
        int queueSize = threadNormalEnum.getQueueSize();
        ThreadPoolTaskExecutor threadPoolTaskExecutor = new ThreadPoolTaskExecutor();
        threadPoolTaskExecutor.setCorePoolSize(coreSize);
        threadPoolTaskExecutor.setMaxPoolSize(maxSize);
        threadPoolTaskExecutor.setQueueCapacity(queueSize);
        threadPoolTaskExecutor.setThreadFactory(new NamedThreadFactory(ThreadNormalEnum.NORMAL_THREAD.name(), false));
        threadPoolTaskExecutor.setTaskDecorator(new MDCTaskDecorator());
        threadPoolTaskExecutor.initialize();
        ThreadPoolExecutor threadPoolExecutor1 = threadPoolTaskExecutor.getThreadPoolExecutor();

           

需要先使用spring的ThreadPoolTaskExecutor. 然后再获取ThreadPoolExecutor;

主要还是之前的那个MDCTaskDecorator适配器.

这样再使用这个线程池执行任务时, 就可以传递线程了;

结果展示

日志链路追踪-springboot/logback/线程传递

oncall中log日志如下:

日志链路追踪-springboot/logback/线程传递

provide中日志如下:

线程池调用,看日志中线程名称

日志链路追踪-springboot/logback/线程传递