微服务 日志追踪 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...

相关推荐
cjp560几秒前
009. ASP.NET WEB API 用户关联esp32设备
前端·后端·asp.net
贺国亚7 分钟前
Text-to-SQL与Analytics-Agent
后端
一只叫煤球的猫25 分钟前
ThreadForge 源码解读二:一个 Task 从 submit 到完成,内部到底发生了什么?
java·后端·面试
苏三说技术38 分钟前
AgentScope Java 2.0 正式发布了!
后端
ping某1 小时前
一个“日志备份”需求,为什么会牵出整个 Linux 日志系统?
后端·架构
血小溅1 小时前
Spring AI 对 Skill/MCP 的支持全景整理
后端
晓杰'1 小时前
从0到1实现Balatro游戏后端(8):Skip Blind与Tag奖励机制设计与实现
后端·websocket·typescript·项目实战·nestjs·状态管理·游戏服务器
叫我:松哥2 小时前
基于Flask框架的校园二手书籍交易平台,注重校园场景的特殊需求,通过学号认证保障用户真实性
后端·python·sqlite·flask·bootstrap
终将老去的穷苦程序员2 小时前
基于SpringBoot的餐饮管理系统
java·spring boot·后端
张忠琳2 小时前
【Go 1.26.4】Golang Map 深度解析
开发语言·后端·golang