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

相关推荐
码界奇点3 分钟前
基于Spring Boot与Vue的校园后台管理系统设计与实现
vue.js·spring boot·后端·毕业设计·源代码管理
爱编程的小庄6 分钟前
Rust 发行版本及工具介绍
开发语言·后端·rust
Apifox.1 小时前
测试用例越堆越多?用 Apifox 测试套件让自动化回归更易维护
运维·前端·后端·测试工具·单元测试·自动化·测试用例
sunnyday04262 小时前
Nginx与Spring Cloud Gateway QPS统计全攻略
java·spring boot·后端·nginx
康王有点困2 小时前
Link入门
后端·flink
海南java第二人2 小时前
Spring Boot全局异常处理终极指南:打造优雅的API错误响应体系
java·spring boot·后端
小楼v3 小时前
消息队列的核心概念与应用(RabbitMQ快速入门)
java·后端·消息队列·rabbitmq·死信队列·交换机·安装步骤
小北方城市网3 小时前
接口性能优化实战:从秒级到毫秒级
java·spring boot·redis·后端·python·性能优化
鸡蛋豆腐仙子3 小时前
Spring的AOP失效场景
java·后端·spring
小北方城市网3 小时前
SpringBoot 全局异常处理最佳实践:从混乱到规范
java·spring boot·后端·spring·rabbitmq·mybatis·java-rabbitmq