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

相关推荐
Csvn22 分钟前
📊 SQL 入门 Day 11:CASE 表达式:SQL 里的 if-else 魔法
后端·sql
QQ_216962909624 分钟前
Spring Boot 养老院管理系统:从入住、护理到费用结算的全流程实现(源码可领)
java·spring boot·后端
万少3 小时前
DeepSeek-V4-Flash 正式版上线了,但这 3 个坑我帮你提前踩了
前端·javascript·后端
明月_清风3 小时前
🚀 Palantir Foundry 本体论实战:当 Ontology 从"知识图谱"进化为"企业操作系统"
前端·后端
明月_清风3 小时前
从概念到代码:用 Ontology 构建你的第一个知识图谱
前端·后端
Python私教3 小时前
Django 6.1 邮件配置大改:旧项目如何平稳升级?
后端·python·django
Python私教3 小时前
Django 6.1 升级避坑:数据库版本不兼容怎么解决?
后端·python·django
Python私教3 小时前
Django 接口开发实测:新手还需要使用 REST 框架吗?
后端·python·django
techdashen3 小时前
Go设计取舍之三: 0.3ns每次的错误Benchmark
开发语言·后端·golang
techdashen5 小时前
Go设计取舍之四: map不变时能否并发修改不同value
开发语言·后端·golang