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
网关层做两件事,具体代码如下:
traceId
放入MDC中,用于日志追踪打印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 普通微服务
普通微服务主要有两处需要处理
- 请求进入微服务时,通过拦截器将
traceId
放入 MDC 中 - 请求即将离开微服务时,通过 ==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...