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

相关推荐
用户6757049885024 分钟前
告别数据库瓶颈!用这个技巧让你的程序跑得飞快!
后端
千|寻23 分钟前
【画江湖】langchain4j - Java1.8下spring boot集成ollama调用本地大模型之问道系列(第一问)
java·spring boot·后端·langchain
程序员岳焱36 分钟前
Java 与 MySQL 性能优化:MySQL 慢 SQL 诊断与分析方法详解
后端·sql·mysql
龚思凯42 分钟前
Node.js 模块导入语法变革全解析
后端·node.js
天行健的回响1 小时前
枚举在实际开发中的使用小Tips
后端
wuhunyu1 小时前
基于 langchain4j 的简易 RAG
后端
techzhi1 小时前
SeaweedFS S3 Spring Boot Starter
java·spring boot·后端
写bug写bug2 小时前
手把手教你使用JConsole
java·后端·程序员
苏三说技术2 小时前
给你1亿的Redis key,如何高效统计?
后端
JohnYan2 小时前
工作笔记- 记一次MySQL数据移植表空间错误排除
数据库·后端·mysql