链路追踪 | MDC+SpringBoot自动装配实现链路追踪组件

MDC+SpringBoot自动装配实现链路追踪组件

  • 一、前言
  • 二、项目
    • 1、项目结构
    • 2、sample-framework pom
    • 3、sample-trace-boot-starter pom 坐标
    • 4、TraceAutoConfiguration 自动装配配置类
    • 5、TraceProperties 配置变量
    • 6、MDCParam 自动装配配置类
    • 7、TraceConstants 常量类
    • 8、MDCUtil 工具类
    • 9、TraceLocalUtil 本地缓存
    • 10、RabbitTraceAspect
    • 11、HttpTraceIdFilter
    • 12、OpenFeignRequestInterceptor
    • 13、 resource
  • 三、测试
  • 四、总结

一、前言

        在项目中使用 Slf4jMDC 机制 可以做到链路追踪,但是项目的微服务化,很多项目都需要进行链路追踪,所以通过 SpringBoot的自动装配机制 将链路追踪 封装为 一个组件,其他项目直接 POM 坐标引用就可以直接做到日志的链路追踪。

       在封装组件的过程中最好成立一个单独的父子工程组件项目,不单单可以封装链路追踪的组件,还可以后续封装其他组件。
例如:工具类的组件 相信大家在开发过程中会使用到很多公用的工具类 可以封装到组件项目中,大家可以参考一下我的这种方式
链路追踪 | MDC+SpringBoot自动装配实现链路追踪组件_第1张图片

  • my-framework - 父项目
  • my-trace-boot-starter 链路追踪 (本次讲的)
  • my-commons 是我封装的一些工具类
  • 其他暂时忽略吧这是我个人玩着搞得

用到技术:

  • SpringBoot 自动装配
  • Slf4j MDC机制
  • 核心是日志中的 唯一id traceId 来做到日志追踪

二、项目

1、项目结构

        这里的代码也是以新建父子工程组件项目来演示公开的。

        这里代码有点多,如果不想费劲看的话可以直接拉到底部,直接git 将项目拉下来参考使用。
链路追踪 | MDC+SpringBoot自动装配实现链路追踪组件_第2张图片

2、sample-framework pom

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>sample-framework</artifactId>
        <groupId>com.su</groupId>
        <version>1.0.0</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>sample-trace-boot-starter</artifactId>
    <packaging>jar</packaging>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>

        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
        </dependency>

        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
        </dependency>

        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
            <scope>provided</scope> <!--私有 不影响引用项目-->
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <scope>provided</scope> <!--私有 不影响引用项目-->
        </dependency>
        
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-amqp</artifactId>
            <scope>provided</scope> <!--私有 不影响引用项目-->
        </dependency>

    </dependencies>


</project>

3、sample-trace-boot-starter pom 坐标

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>sample-framework</artifactId>
        <groupId>com.su</groupId>
        <version>1.0.0</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>sample-trace-boot-starter</artifactId>
    <packaging>jar</packaging>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>

        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
        </dependency>

        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
        </dependency>

        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

    </dependencies>


</project>

4、TraceAutoConfiguration 自动装配配置类

package com.sample.trace.config;

import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.web.servlet.ServletComponentScan;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

@Slf4j
@Configuration
//加载过滤器需要的注解
@ServletComponentScan(basePackages = "com.sample.trace")
@ComponentScan(basePackages = "com.sample.trace")
public class TraceAutoConfiguration {

    public TraceAutoConfiguration() {
        log.info("TraceAutoConfiguration---------create ok");
    }

}

5、TraceProperties 配置变量

package com.sample.trace.config;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

/**
 * @author 邓健
 * @version 1.0
 * @date 2020/9/8 10:22
 */
@Component(value = "traceProperties")
@ConfigurationProperties(prefix = "zohe.trace")
public class TraceProperties {

    /**
     * 服务名称
     */
    private String serverName="NO_NAME";

    public String getServerName() {
        return serverName;
    }

    public void setServerName(String serverName) {
        this.serverName = serverName;
    }
}

6、MDCParam 自动装配配置类

package com.sample.trace.bean;

import lombok.Data;

/**
 * MDC参数类
 *
 * @author su
 * @version 1.0
 * @date 2022/4/11 10:21
 */
@Data
public class MDCParam {

    /**
     * traceId
     */
    private String traceId;

    /**
     * 上层服务名称
     */
    private String upServer;

    /**
     * 当前服务名称
     */
    private String serverName;

}

7、TraceConstants 常量类

package com.sample.trace.consts;

/**
 * trace 常量
 */
public interface TraceConstants {

    /**
     * traceId
     */
    String TRACE_ID_KEY = "traceId";

    /**
     * 上级服务名称
     */
    String UP_SERVER = "upServer";

    /**
     * 当前服务名称
     */
    String SERVER_NAME = "serverName";
}

8、MDCUtil 工具类

package com.sample.trace.utils;

import cn.hutool.core.bean.BeanUtil;
import com.sample.trace.bean.MDCParam;
import org.slf4j.MDC;
import java.util.Map;

public class MDCUtil {

    public static void put(MDCParam param) {
        Map<String, Object> map = BeanUtil.beanToMap(param);
        TraceLocalUtil.setMap(map);
        //MDC.setContextMap(traceMap); 源码是,会存在覆盖问题 传入的Map 替换 原来MDC的中的map 决定使用put
        for (String key : map.keySet()) {
            if (map.get(key) != null) {
                MDC.put(key, map.get(key).toString());
            }
        }
    }

    public static void clear() {
        MDC.clear();
        TraceLocalUtil.clear();
    }
}

9、TraceLocalUtil 本地缓存

package com.sample.trace.utils;

import java.util.Map;
/**
 * ThreadLocal 存储 TraceId
 */
public class TraceLocalUtil {

    private static final ThreadLocal<Map<String, Object>> traceIdTreadLocal = new ThreadLocal<>();

    public static void setMap(Map<String, Object> traceMap) {
        traceIdTreadLocal.set(traceMap);
    }

    public static Map<String, Object> getMap() {
        return traceIdTreadLocal.get();
    }

    public static void clear() {
        traceIdTreadLocal.remove();
    }
}

10、RabbitTraceAspect

RabbitMQ使用AOP 的方式 接受 MDC插入traceId

package com.sample.trace.aop;

import com.sample.trace.bean.MDCParam;
import com.sample.trace.config.TraceProperties;
import com.sample.trace.utils.MDCUtil;
import com.sample.trace.utils.TraceLocalUtil;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.util.Map;
import java.util.UUID;

/**
 * RabbitMQ traceId 生成
 */
@Aspect
@Component
@Order(0)
public class RabbitTraceAspect {

    @Resource
    private TraceProperties traceProperties;

    @Pointcut("@annotation(org.springframework.amqp.rabbit.annotation.RabbitListener)")
    public void pointcut() {
    }

    @Around("pointcut()")
    public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
        try {
            Map<String, Object> localMap = TraceLocalUtil.getMap();
            if (localMap == null) {
                MDCUtil.clear();
                MDCParam param = new MDCParam();
                param.setTraceId(UUID.randomUUID().toString());
                param.setServerName(traceProperties.getServerName());
                //TODO upServer
                param.setUpServer(traceProperties.getServerName());
                MDCUtil.put(param);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        Object result = joinPoint.proceed();
        // 清除TreadLocal MDC
        MDCUtil.clear();
        return result;
    }

}


11、HttpTraceIdFilter

http 请求 使用过滤器 来 插入 traceId

package com.sample.trace.filter;

import com.sample.trace.bean.MDCParam;
import com.sample.trace.config.TraceProperties;
import com.sample.trace.consts.TraceConstants;
import com.sample.trace.utils.MDCUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.support.WebApplicationContextUtils;

import javax.servlet.*;
import javax.servlet.FilterConfig;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.UUID;

@Slf4j
@WebFilter(urlPatterns = "/*")
public class HttpTraceIdFilter implements Filter {

    public HttpTraceIdFilter() {
        log.info("HttpTraceIdFilter---------create ok");
    }

    private TraceProperties traceProperties;

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        //tomcat 容器启动 springboot 项目 关于servlet 的配置都会失效
        //filter 也是servlet 也是
        if (traceProperties == null) {
            WebApplicationContext applicationContext = WebApplicationContextUtils.getRequiredWebApplicationContext(filterConfig.getServletContext());
            traceProperties = applicationContext.getBean(TraceProperties.class);
        }
    }

    @Override
    public void destroy() {
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest httpRequest = (HttpServletRequest) request;

        //先清除本地缓存
        try {
            MDCUtil.clear();
            MDCParam param = new MDCParam();
            String traceId = httpRequest.getHeader(TraceConstants.TRACE_ID_KEY);
            String upServer = httpRequest.getHeader(TraceConstants.UP_SERVER)
            if (StringUtils.isBlank(traceId)) {
                traceId = UUID.randomUUID().toString();
            }
            param.setTraceId(traceId);
            if (StringUtils.isNotBlank(upServer)) {
                param.setUpServer(upServer);
            }
            param.setServerName(traceProperties.getServerName());
            MDCUtil.put(param);
        } catch (Exception e) {
            e.printStackTrace();
        }
        chain.doFilter(request, response);
    }

}

12、OpenFeignRequestInterceptor

这里使用OpenFegin 来作为交互组件 使用请求拦截器来传递 TraceId

package com.sample.trace.interce;

import com.sample.trace.consts.TraceConstants;
import feign.RequestInterceptor;
import feign.RequestTemplate;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.MDC;
import org.springframework.stereotype.Component;

@Slf4j
@Component
public class OpenFeignRequestInterceptor implements RequestInterceptor {

    @Override
    public void apply(RequestTemplate requestTemplate) {
        String traceId = MDC.get(TraceConstants.TRACE_ID_KEY);
        String serverName = MDC.get(TraceConstants.SERVER_NAME);
        requestTemplate.header(TraceConstants.TRACE_ID_KEY, traceId);
        requestTemplate.header(TraceConstants.UP_SERVER, serverName);
    }
}

13、 resource

additional-spring-configuration-metadata.json

{
  "properties": [
    {
      "name": "zohe.trace.serverName",
      "type": "java.lang.String",
      "defaultValue": "NO_NAME",
      "description": "trace 当前服务名称"
    }
  ]
}

spring.factories 自动装配

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.sample.trace.config.TraceAutoConfiguration

spring-configuration-metadata.json

{
  "groups": [
    {
      "name": "server",
      "type": "org.springframework.boot.autoconfigure.web.ServerProperties",
      "sourceType": "org.springframework.boot.autoconfigure.web.ServerProperties"
    }
  ],
  "properties": [
    {
      "name": "zohe.trace.server-name",
      "type": "java.lang.String",
      "sourceType": "org.springframework.boot.autoconfigure.web.ServerProperties"
    }
  ],
  "hints": [
    {
      "name": "spring.jpa.hibernate.ddl-auto",
      "values": [
        {
          "value": "none",
          "description": "Disable DDL handling."
        },
        {
          "value": "validate",
          "description": "Validate the schema, make no changes to the database."
        }
      ]
    }
  ]
}

三、测试

测试项目需要修改 logback.xml 加上 traceId 输出的位置

这里就简单截图一下测试项目 需要项目的可以看底部 最下边gitee 自己拉
链路追踪 | MDC+SpringBoot自动装配实现链路追踪组件_第3张图片

<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="30 seconds">
    <contextName>zohe-video-struct</contextName>
    <property name="LOGPATH" value="/home/admin/logs"/>
    <property name="APP_NAME" value="zohe-video-struct"/>
    <property name="FILE_NAME" value="%d{yyyyMMdd}"/>
    <property name="STDOUT_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS}|%level|%thread|%class|Line:%line|%X{upServer}|%X{traceId}| %msg%n"/>
    <property name="PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS}|%level|%thread|%class|Line:%line|%X{traceId} %msg%n"/>    <!-- 输出到控制台 -->
    <appender name="stdout" class="ch.qos.logback.core.ConsoleAppender">
        <layout class="ch.qos.logback.classic.PatternLayout">
            <pattern>${STDOUT_PATTERN}</pattern>
        </layout>
    </appender>

    <!-- 输出到文件 -->
    <appender name="debug" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>${LOGPATH}/${APP_NAME}/${APP_NAME}.debug.log</file>
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>${LOGPATH}/${APP_NAME}/${FILE_NAME}.debug.bak</fileNamePattern>
            <maxHistory>30</maxHistory>
        </rollingPolicy>
        <append>true</append>
        <encoder>
            <pattern>${PATTERN}</pattern>
        </encoder>
        <filter class="ch.qos.logback.classic.filter.LevelFilter"><!-- 只打印DEBUG日志 -->
            <level>DEBUG</level>
            <onMatch>ACCEPT</onMatch>
            <onMismatch>DENY</onMismatch>
        </filter>
    </appender>
    <appender name="info" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>${LOGPATH}/${APP_NAME}/${APP_NAME}.info.log</file>
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>${LOGPATH}/${APP_NAME}/${FILE_NAME}.info.bak</fileNamePattern>
            <maxHistory>30</maxHistory>
        </rollingPolicy>
        <append>true</append>
        <encoder>
            <pattern>${PATTERN}</pattern>
        </encoder>
        <filter class="ch.qos.logback.classic.filter.LevelFilter"><!-- 只打印INFO日志 -->
            <level>INFO</level>
            <onMatch>ACCEPT</onMatch>
            <onMismatch>DENY</onMismatch>
        </filter>
    </appender>
    <appender name="error" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>${LOGPATH}/${APP_NAME}/${APP_NAME}.error.log</file>
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>${LOGPATH}/${APP_NAME}/${FILE_NAME}.error.bak</fileNamePattern>
            <maxHistory>30</maxHistory>
        </rollingPolicy>
        <append>true</append>
        <encoder>
            <pattern>${PATTERN}</pattern>
        </encoder>
        <filter class="ch.qos.logback.classic.filter.LevelFilter"><!-- 只打印ERROR日志 -->
            <level>ERROR</level>
            <onMatch>ACCEPT</onMatch>
            <onMismatch>DENY</onMismatch>
        </filter>
    </appender>
    <appender name="warn" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>${LOGPATH}/${APP_NAME}/${APP_NAME}.warn.log</file>
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>${LOGPATH}/${APP_NAME}/${FILE_NAME}.warn.bak</fileNamePattern>
            <maxHistory>30</maxHistory>
        </rollingPolicy>
        <append>true</append>
        <encoder>
            <pattern>${PATTERN}</pattern>
        </encoder>
        <filter class="ch.qos.logback.classic.filter.LevelFilter"><!-- 只打印WARN日志 -->
            <level>WARN</level>
            <onMatch>ACCEPT</onMatch>
            <onMismatch>DENY</onMismatch>
        </filter>
    </appender>

    <appender name="ASYNC" class="ch.qos.logback.classic.AsyncAppender">
        <!-- 不丢失日志.默认的,如果队列的80%已满,则会丢弃TRACT、DEBUG、INFO级别的日志 -->
        <discardingThreshold>0</discardingThreshold>
        <!-- 更改默认的队列的深度,该值会影响性能.默认值为256 -->
        <queueSize>256</queueSize>
        <!-- 添加附加的appender,最多只能添加一个 -->
        <appender-ref ref="info"/>
    </appender>

    <!-- 指定日志级别 -->
    <root level="INFO">
        <appender-ref ref="stdout"  />
        <!-- 本地测试时如果不用输出到文件,可以注释掉 -->
        <appender-ref ref="debug" />
        <appender-ref ref="info" />
        <appender-ref ref="warn" />
        <appender-ref ref="error" />
    </root>
</configuration>

链路追踪 | MDC+SpringBoot自动装配实现链路追踪组件_第4张图片

四、总结

项目gitee 地址: https://gitee.com/DianHaiShiYuDeMing/sample-framework

需要注意的事:

  • 需要修改logback.xml 的输出

有需要改进的地方请留言评论。共同学习,共同进步。

你可能感兴趣的:(SpringBoot,SpringCloud,Alibaba,spring,boot,java,spring)