微服务 日志追踪 traceId 解决方案

1. 问题背景

随着项目的逐渐壮大,以及业务逻辑的日渐复杂,微服务项目的调用链路逐渐变长,与此同时,出现问题时排查原因就会需要根据代码追踪日志,并每个微服务追踪,每次排查问题都要先看代码,相对麻烦。

2. 解决方案

日志打印 traceId,用来追踪每次请求进来调用链路,traceId的生命周期贯穿整个调用链路

3. 详细设计

微服务架构

├── README.md

├── vita-api // 外部交互服务(微服务)

├── vita-auth-center // 认证中心服务 (微服务)

├── vita-common // 公共包(公共 starter)

├── vita-gateway // 网关服务 (网关)

本次用作演示,微服务架构如上图所示,主要调用链路 vita-gateway -> vita-auth-center -> vita-api三个服务用作测试 traceId 是否正常。以下解决方案根据如上架构设计,分为 ==网关==、==微服务== 两种情况下的处理。

3.1 网关

这里使用的是 spring-cloud-gateway

网关层,主要在前端或其他三方服务请求进来之后做拦截鉴权等,添加过滤器,优先级设置为最高,生成 traceId 网关层做两件事,具体代码如下:

  1. traceId 放入MDC中,用于日志追踪打印
  2. traceId 放入请求 header 中用于传递到后续微服务中
java 复制代码
package com.vita.gateway.filter;

import com.vita.common.domain.common.CommonConstant;
import com.vita.common.utils.TraceIdUtil;
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;

/**
 * @author Edward
 * @date 2023-12-25 14:20
 */
@Component
public class TraceFilter implements GlobalFilter, Ordered {
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        // traceId 生成
        String traceId = TraceIdUtil.generateTraceId();
        MDC.put(CommonConstant.TRACE_ID, traceId);

        final ServerHttpRequest finalRequest = exchange.getRequest()
                .mutate()
                .header(CommonConstant.TRACE_ID, traceId)
                .build();
        return chain.filter(exchange.mutate().request(finalRequest).build());
    }

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

3.2 普通微服务

普通微服务主要有两处需要处理

  1. 请求进入微服务时,通过拦截器将 traceId 放入 MDC 中
  2. 请求即将离开微服务时,通过 ==openfeign== 拦截器,将 traceId 传递给下一个微服务

3.2.1 请求进入微服务

java 复制代码
package com.vita.common.interceptor;

import com.vita.common.domain.common.CommonConstant;
import com.vita.common.utils.TraceIdUtil;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.MDC;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * traceId 拦截器
 *
 * @author Edward
 * @date 2023-12-22 18:19
 */
public class TraceIdHandlerInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String traceId = request.getHeader(CommonConstant.TRACE_ID);
        MDC.put(CommonConstant.TRACE_ID, StringUtils.isNotBlank(traceId) ? traceId : TraceIdUtil.generateTraceId());
        return true;
    }

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

3.2.2 请求离开微服务

java 复制代码
package com.vita.common.interceptor;

import com.vita.common.domain.common.CommonConstant;
import feign.RequestInterceptor;
import feign.RequestTemplate;
import org.slf4j.MDC;

/**
 * traceId feign 拦截器
 *
 * @author Edward
 * @date 2023-06-17 21:47
 */
public class TraceIdFeignInterceptor implements RequestInterceptor {
    @Override
    public void apply(RequestTemplate requestTemplate) {
        requestTemplate.header(CommonConstant.TRACE_ID, MDC.get(CommonConstant.TRACE_ID));
    }
}

3.3 公共拦截器抽出到 starter 中

由于这些属于公共功能,若每个微服务都需要将其写一遍,太过繁琐,因此将这些提取到公共的 ==common starter==中。

3.3.1 SPI 机制将 TraceId 相关配置导入自动装配

这里的 springboot 版本为 2.7.12,若为 2.7 之前的版本,则是在 spring.factories 下的 META-INF 中添加如下配置

3.3.2 自动装配相关配置

java 复制代码
package com.vita.common.config;

import com.vita.common.interceptor.TraceIdFeignInterceptor;
import com.vita.common.interceptor.TraceIdHandlerInterceptor;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.context.annotation.Import;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 * @author Edward
 * @date 2023-12-22 18:30
 */
@Import(value = TraceIdFeignInterceptor.class) // 请求离开的 openFeign 拦截器(具体实现参考 3.2.2)
@ConditionalOnClass(value = DispatcherServlet.class)
public class TraceIdAutoConfiguration implements WebMvcConfigurer {

    /**
     * 所有路径
     */
    private static final String ALL_PATH = "/**";

	// 请求进入微服务的 springmvc 拦截器(具体实现参考 3.2.1)
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new TraceIdHandlerInterceptor())
                .addPathPatterns(ALL_PATH);
    }
}

3.4 logback-spring.xml 配置

我这里使用的是 logback 作为日志框架,若是 log4j 还请自行百百度配置方式 %X{traceId} 即为获取 MDC 中的 traceId 的方式

xml 复制代码
<!--日志输出格式-->
<property name="LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss} [%-5level] [%X{traceId}] -- [%thread] %-40.50class:  %msg%n"/>

4. 最终效果

调用链路: vita-gateway -> vita-auth-center -> vita-api vita-gateway vita-auth-center vita-api

5. 相关源码

github 源码地址:github.com/edwarddamon...

相关推荐
桦说编程2 小时前
深入解析CompletableFuture源码实现(2)———双源输入
java·后端·源码
舒一笑3 小时前
大模型时代的程序员成长悖论:如何在AI辅助下不失去竞争力
后端·程序员·掘金技术征文
lang201509283 小时前
Spring Boot优雅关闭全解析
java·spring boot·后端
小羊在睡觉3 小时前
golang定时器
开发语言·后端·golang
用户21411832636023 小时前
手把手教你在魔搭跑通 DeepSeek-OCR!光学压缩 + MoE 解码,97% 精度还省 10-20 倍 token
后端
追逐时光者4 小时前
一个基于 .NET 开源、功能强大的分布式微服务开发框架
后端·.net
刘一说4 小时前
Spring Boot 启动慢?启动过程深度解析与优化策略
java·spring boot·后端
壹佰大多4 小时前
【spring如何扫描一个路径下被注解修饰的类】
java·后端·spring
间彧4 小时前
Java双亲委派模型的具体实现原理是什么?
后端
间彧4 小时前
Java类的加载过程
后端