java通过自定义TraceId实现简单的链路追踪

简介: 本文介绍了如何在Spring Boot项目中通过SLF4J的MDC实现日志上下文traceId追踪。内容涵盖依赖配置、拦截器实现、网关与服务间调用的traceId传递、多线程环境下的上下文同步,以及logback日志格式配置。适用于小型微服务架构的链路追踪,便于排查复杂调用场景中的问题。

1.整体思路

通过SLF4J 的日志上下文MDC保存traceId,并通过springboot请求拦截器在每次请求中从请求头中获取到traceId,并将其保存进上下文中。

2.所需依赖

xml

体验AI代码助手

代码解读

复制代码

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-logging</artifactId>
</dependency>
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-api</artifactId>
</dependency>

3.完整实现代码

3.1 traceId拦截切面

java

体验AI代码助手

代码解读

复制代码

import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.MDC;
import org.springframework.lang.NonNull;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.UUID;

/**
 * 日志拦截器
 *
 */
@Slf4j
public class LogInterceptor implements HandlerInterceptor {

    public static final String TRACE_ID = "traceId";

    @Override
    public boolean preHandle(HttpServletRequest request, @NonNull HttpServletResponse response,
                             @NonNull Object handler) {
        String traceId = request.getHeader(TRACE_ID);
        if (StringUtils.isBlank(traceId)) {
            traceId = createTraceId();
        }
        MDC.put(TRACE_ID, traceId);
        return true;
    }

    @Override
    public void afterCompletion(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response,
                                @NonNull Object handler, Exception ex) {
        MDC.remove(TRACE_ID);
    }

    public static String createTraceId() {
        return UUID.randomUUID().toString().replaceAll("-", "");
    }
}

注册到SpringMvc中

java

体验AI代码助手

代码解读

复制代码

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    private int connectTimeout = 500;

    private int readTimeout = 50000;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 注册
        registry.addInterceptor(new LogInterceptor()).addPathPatterns("/**");
    }

    @Bean
    RestTemplate restTemplate() {
        SimpleClientHttpRequestFactory httpRequestFactory = new SimpleClientHttpRequestFactory();
        httpRequestFactory.setConnectTimeout(connectTimeout);
        httpRequestFactory.setReadTimeout(readTimeout);
        return new RestTemplate(httpRequestFactory);
    }
}

3.2 网关代码修改

在网关中添加全局过滤器来自动添加traceId

java

体验AI代码助手

代码解读

复制代码

import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.MDC;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import java.util.UUID;

/**
 * traceId 全局过滤器
 */
@Component
@Slf4j
public class TraceIdGlobalFilter implements GlobalFilter, Ordered {

    private static final String TRACE_ID = "traceId";

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        // 获取请求头中的 traceId 字段
        String traceId = exchange.getRequest().getHeaders().getFirst(TRACE_ID);
        traceId = StringUtils.isNotBlank(traceId) ? traceId : createTraceId();


        // 将 traceId 存储到 MDC 中
        MDC.put(TRACE_ID, traceId);

        
        ServerHttpRequest mutableReq = exchange.getRequest().mutate().header(TRACE_ID, traceId).build();
        ServerWebExchange mutableExchange = exchange.mutate().request(mutableReq).build();

        // 转发请求
        return chain.filter(mutableExchange).doFinally(s -> {
            // 从 MDC 中清除 traceId
            MDC.remove(TRACE_ID);
        });
    }

    @Override
    public int getOrder() {
        return Ordered.HIGHEST_PRECEDENCE;
    }

    public static String createTraceId() {
        return UUID.randomUUID().toString().replaceAll("-", "").toLowerCase();
    }

}

3.3服务间请求调用带上traceId

下面简单给出OpenFeign的实现

java

体验AI代码助手

代码解读

复制代码

import feign.RequestInterceptor;
import feign.RequestTemplate;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.MDC;
import org.springframework.util.CollectionUtils;
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.HashMap;
import java.util.Map;
import java.util.Objects;

/**
 * 自定义的Feign拦截器
 *
 */
@Slf4j
public class FeignRequestInterceptor implements RequestInterceptor {
    /**
     * 这里可以实现对请求的拦截,对请求添加一些额外信息之类的
     *
     * @param requestTemplate
     */
    @Override
    public void apply(RequestTemplate requestTemplate) {
        // 1. obtain request
        final ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();

        // 2. 兼容hystrix限流后,获取不到ServletRequestAttributes的问题(使拦截器直接失效)
        if (Objects.isNull(attributes)) {
            log.warn("OpenFeignRequestInterceptor is invalid!");
            return;
        }
        HttpServletRequest request = attributes.getRequest();


        // 2. 透传请求头
        Enumeration<String> headerNames = request.getHeaderNames();
        if (Objects.nonNull(headerNames)) {
            while (headerNames.hasMoreElements()) {
                String name = headerNames.nextElement();
                // 跳过 content-length
                if (name.equals("content-length")) {
                    continue;
                }
                String value = request.getHeader(name);
                requestTemplate.header(name, value);
            }
        }
        // 添加traceId到请求头
        Map<String, String> copyOfContextMap = MDC.getCopyOfContextMap();
        if (CollectionUtils.isEmpty(copyOfContextMap)) {
            copyOfContextMap = new HashMap<>();
            copyOfContextMap.put(LogInterceptor.TRACE_ID, LogInterceptor.createTraceId());
        }
        for (Map.Entry<String, String> entry : copyOfContextMap.entrySet()) {
            requestTemplate.header(entry.getKey(), entry.getValue());
        }
    }
}

注册到容器中

java

体验AI代码助手

代码解读

复制代码

import feign.RequestInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class OpenFeignConfiguration {

    @Bean
    public RequestInterceptor requestInterceptor() {
        return new FeignRequestInterceptor();
    }
}

3.4 多线程间同步日志上下文

由于traceId存放于MDC,而MDC的实现是基于ThreadLocal,它是线程隔离的,所以在开启多线程时要在线程间同步MDC

java

体验AI代码助手

代码解读

复制代码

import org.slf4j.MDC;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

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

/**
 * MDC日志上下文包装器
 * 例 executorService.submit(MdcWrapper.createCallable(() -> "hello world");
 *
 */
public class MdcWrapper {


    public static RunnableWrapper createRunable(Runnable runable) {
        return new RunnableWrapper(runable);
    }

    public static <V> CallableWrapper<V> createCallable(Callable<V> callable) {
        return new CallableWrapper<V>(callable);
    }

    static class RunnableWrapper implements Runnable {
        private final Map<String, String> parentMdc;
        private final Runnable runnable;
        private ServletRequestAttributes attributes;

        public RunnableWrapper(Runnable runnable) {
            this.parentMdc = MDC.getCopyOfContextMap();
            this.runnable = runnable;
            this.attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        }

        @Override
        public void run() {
            try {
                // 将父线程的 MDC 数据传递给子线程
                if (parentMdc != null) {
                    MDC.setContextMap(parentMdc);
                }
                // 将父线程的请求头也复制给子线程
                RequestContextHolder.setRequestAttributes(attributes);
                // 执行线程任务
                runnable.run();
            } finally {
                // 清除子线程的 MDC 数据
                MDC.clear();
                RequestContextHolder.resetRequestAttributes();
            }
        }
    }

    static class CallableWrapper<V> implements Callable<V> {
        private final Map<String, String> parentMdc;

        private final Callable<V> callable;
        private ServletRequestAttributes attributes;


        public CallableWrapper(Callable<V> callable) {
            this.parentMdc = MDC.getCopyOfContextMap();
            this.callable = callable;
            this.attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        }

        @Override
        public V call() throws Exception {
            try {
                // 将父线程的 MDC 数据传递给子线程
                if (parentMdc != null) {
                    MDC.setContextMap(parentMdc);
                }
                // 将父线程的请求头也复制给子线程
                RequestContextHolder.setRequestAttributes(attributes);
                // 执行任务
                return callable.call();
            } finally {
                // 清除子线程的 MDC 数据
                MDC.clear();
                RequestContextHolder.resetRequestAttributes();
            }
        }
    }
}

3.5 logback.xml配置

xml

体验AI代码助手

代码解读

复制代码

<appender name="INFO" class="ch.qos.logback.core.rolling.RollingFileAppender">
    <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
        <FileNamePattern>${catalina.home:-.}/logs/${appName}/${appName}_%d{yyyy-MM-dd}_info_%i.log.gz</FileNamePattern>
        <maxFileSize>100MB</maxFileSize>
        <maxHistory>30</maxHistory>
        <totalSizeCap>2GB</totalSizeCap>
        <cleanHistoryOnStart>true</cleanHistoryOnStart>
    </rollingPolicy>
    <encoder>
     <!-- 通过{traceId}引用MDC中的traceId-->
        <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [traceId:%X{traceId}] %-5level %logger - %msg%n</pattern>
        <charset>UTF-8</charset>
    </encoder>
    <filter class="ch.qos.logback.classic.filter.LevelFilter">
        <level>INFO</level>
        <onMatch>ACCEPT</onMatch>
        <onMismatch>DENY</onMismatch>
    </filter> 
</appender>

4完结撒花

这是一个简单的traceId方案,适合于服务只有五个以内的简单业务,对于服务数超过五的,调用链路上的复杂业务最好还是使用成熟的方案 例如 skywalking,它是基于字节码增强的实现,不需要业务的代码改动


转载来源:https://juejin.cn/post/7347207466818519079

目录
打赏
0
0
0
0
179
分享
相关文章
|
8月前
|
在 Java 中捕获和处理自定义异常的代码示例
本文提供了一个 Java 代码示例,展示了如何捕获和处理自定义异常。通过创建自定义异常类并使用 try-catch 语句,可以更灵活地处理程序中的错误情况。
269 1
|
8月前
|
在 Java 中,如何自定义`NumberFormatException`异常
在Java中,自定义`NumberFormatException`异常可以通过继承`IllegalArgumentException`类并重写其构造方法来实现。自定义异常类可以添加额外的错误信息或行为,以便更精确地处理特定的数字格式转换错误。
131 1
|
9月前
|
让星星⭐月亮告诉你,自定义定时器和Java自带原生定时器
定时器是一种可以设置多个具有不同执行时间和间隔的任务的工具。本文介绍了定时器的基本概念、如何自定义实现一个定时器,以及Java原生定时器的使用方法,包括定义定时任务接口、实现任务、定义任务处理线程和使用Java的`Timer`与`TimerTask`类来管理和执行定时任务。
289 3
【应用服务 App Service】App Service 中部署Java项目,查看Tomcat配置及上传自定义版本
【应用服务 App Service】App Service 中部署Java项目,查看Tomcat配置及上传自定义版本
113 0
|
7月前
|
java实现从HDFS上下载文件及文件夹的功能,以流形式输出,便于用户自定义保存任何路径下
java实现从HDFS上下载文件及文件夹的功能,以流形式输出,便于用户自定义保存任何路径下
233 34
|
9月前
|
如何在 Java 中创建自定义安全管理器
在Java中创建自定义安全管理器需要继承SecurityManager类并重写其方法,以实现特定的安全策略。通过设置系统安全属性来启用自定义安全管理器,从而控制应用程序的访问权限和安全行为。
221 1
大数据-58 Kafka 高级特性 消息发送02-自定义序列化器、自定义分区器 Java代码实现
大数据-58 Kafka 高级特性 消息发送02-自定义序列化器、自定义分区器 Java代码实现
189 3
数据结构 —— Java自定义代码实现顺序表,包含测试用例以及ArrayList的使用以及相关算法题
文章详细介绍了如何用Java自定义实现一个顺序表类,包括插入、删除、获取数据元素、求数据个数等功能,并对顺序表进行了测试,最后还提及了Java中自带的顺序表实现类ArrayList。
193 0
|
9月前
|
java实现从HDFS上下载文件及文件夹的功能,以流形式输出,便于用户自定义保存任何路径下
java实现从HDFS上下载文件及文件夹的功能,以流形式输出,便于用户自定义保存任何路径下
136 2
java实现从HDFS上下载文件及文件夹的功能,以流形式输出,便于用户自定义保存任何路径下
Java注解,元注解,自定义注解的使用
本文讲解了Java中注解的概念和作用,包括基本注解的用法(@Override, @Deprecated, @SuppressWarnings, @SafeVarargs, @FunctionalInterface),Java提供的元注解(@Retention, @Target, @Documented, @Inherited),以及如何自定义注解并通过反射获取注解信息。
Java注解,元注解,自定义注解的使用
AI助理

你好,我是AI助理

可以解答问题、推荐解决方案等

登录插画

登录以查看您的控制台资源

管理云资源
状态一览
快捷访问