写在前面的话
该篇文章采用SpringGateway实现traceId存在缺陷,思路可以参考,但是实现是有问题的,这个问题我通过SpringBoot自己实现了网关的功能,在下一篇文章中有说明【微服务-网关Spring Gateway进阶-网关参数设计与封装】,同时最新的源码中上传了基于SpringBoot实现网关的功能,可以参考。
一、概述
在微服务中,服务链路调用较多,如果日志没有明显的标识,很难排查关联的日志信息,因此在日志文件中,对于同一个请求,需要有同一个日志追踪ID(常称为traceId)。这里有两个层次的要求,第一个是同一个微服务中同一个请求的日志包含相同的traceId;第二个不同微服务之间的同一个请求,traceId也需要保持一致。
二、MDC简介
MDC(Mapped Diagnostic Context,映射调试上下文)是 log4j 和 logback 提供的一种方便在多线程条件下记录日志的功能。我采用的是log4j2,也支持这种功能。微服务也是一个多线程的应用,在MDC中添加每个线程(请求)的特殊参数,即traceId,能够区分不同线程的日志信息,有利于排查问题。
三、网关实现
在SpringGateWay中通过添加一个全局的TraceInfoFilter,通过MDC轻松实现。注意这里要将该Filter的Order设置低一些,这样他的优先级就比较高,其他的业务逻辑处理都必须在他之后,才能用到这里设置的traceId属性。
/**
* 生成日志唯一ID,并放到请求头信息中,以及集成slf4j
*/
@Component
public class TraceInfoFilter implements GlobalFilter, Ordered {
private Logger logger = LoggerFactory.getLogger(this.getClass());
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
logger.info("====== TraceInfoFilter start ======");
String traceId = TraceIdUtil.shortUUID();
// 1.将traceId传递给微服务
ServerHttpRequest request = exchange.getRequest().mutate().header("traceId", traceId).build();
// 2.将traceId设置到slf4j中,日志打印模板配置打印traceId
MDC.put("traceId", traceId);
return chain.filter(exchange.mutate().request(request).build());
}
@Override
public int getOrder() {
return WRITE_RESPONSE_FILTER_ORDER - 2;
}
}
第二步要调整log4j2.xml的输入模板配置,这里需要添加traceId属性,我这个例子只修改了控制台输出。注意traceId是自定义属性,其写法为%X{traceId}
<configuration status="INFO" monitorInterval="30">
<properties>
<Property name="log_path">${sys:user.home}/logs</Property>
</properties>
<!--先定义所有的appender -->
<appenders>
<console name="Console" target="SYSTEM_OUT">
<!--输出日志的格式 -->
<PatternLayout charset="UTF-8" pattern="[%d][%-5p][%t][%c:%L][%X{traceId}] %m%n"/>
<ThresholdFilter level="info" onMatch="ACCEPT" onMismatch="DENY"/>
</console>
</appenders>
<!--然后定义logger,只有定义了logger并引入的appender,appender才会生效 -->
<loggers>
<!--过滤掉spring和mybatis的一些无用的DEBUG信息 -->
<logger name="org.springframework" level="INFO"></logger>
<!--<root level="all">-->
<root level="ALL">
<appender-ref ref="Console"/>
</root>
</loggers>
</configuration>
测试如下,绿色部分就是增加的traceId打印
四、微服务实现
这里的思路是不改变原有的请求参数,传递给微服务的链路参数放在header里面。
// 1.将traceId传递给微服务
ServerHttpRequest request = exchange.getRequest().mutate().header("traceId", traceId).build();
// 2.将traceId设置到slf4j中,日志打印模板配置打印traceId
MDC.put("traceId", traceId);
return chain.filter(exchange.mutate().request(request).build());
微服务也需要进行相应的改造,需要添加一个RequestInterceptor,用于处理traceId
/**
* 请求拦截器
*/
public class RequestInterceptor extends HandlerInterceptorAdapter {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String traceId = request.getHeader("traceId");
MDC.put("traceId", traceId);
return super.preHandle(request, response, handler);
}
}
在原有的业务代码中打印日志,调试后,就可以发现,gateway中的traceId传递到微服务中,并且对业务代码没有侵入。
五、总结
- 工程启动的日志没有traceId,因为如上实现方式是请求类的信息才有添加traceId
- traceId是UUID,可以优化下加上时间戳、系统信息等,提升可读性
- 中间出现一个小插曲,在网关设置属性错误导致微服务无法获取traceId