很久没更新博客了。
这几天做traceid的时候,发现网上博客很多,但是大多都有一些点没讲很清楚,特别是SPI这块。这里写了一段亲测可用的代码贴出来让大家使用。兄弟们用完之后点歌收藏,点个赞啥的。也是我更新干货的动力。
开发排查系统问题用得最多的手段就是查看系统日志,但是在分布式环境下使用日志定位问题还是比较麻烦,需要借助 全链路追踪ID 把上下文串联起来,使用这种技术后,从前端的返回头中就可以确认某一条需要查错的请求的追踪id,以此为标识在海量日志中找到整个请求的调用链路,避免掉相同方法请求日志信息的影响,特别是高并发场景下尤为重要,能够快速的了解到调用信息的变化及准确的报错情况,很好的帮助开发解决问题。下面我们直接上干货
第一步,在消费者服务中创建一个普通http过滤器
/**
-
@author zhuxinji
-
@date 2021-12-29-10:54
-
@DESC
*/
@Slf4j
@WebFilter(urlPatterns = “/**”, filterName = “logTraceHttpFilter”)
@Order(1)
@Component
public class LogTraceHttpFilter implements Filter {@Override
public void init(FilterConfig filterConfig) {
// TODO Auto-generated method stub
}@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
try {
// 这个traceId也可以考虑从Http请求里面取,如果是null则默认用uuid
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
String traceId = TraceUtil.initMDCWithHttpFilter(getTraceId(request));
// 响应头里面添加一个traceId,方便f12debug调试;本来是每个ajax都由前端生成traceId,由于改动成本太高,就变成响应输出traceId
response.setHeader(TraceConstants.TRACE_ID, traceId);
if (log.isInfoEnabled()) {
log.info(“web端接收http请求[{}]----start”, request.getRequestURI());
}
filterChain.doFilter(request, response);
} finally {
// 最后清除掉MDC内容
TraceUtil.clearMDC();
}
}/**
- description: 由于考虑到和其他系统对接时,可能会用其他系统传递进来参数作为traceId
- 因此预留此方法方便后续Override
- @param servletRequest
- @return
*/
public String getTraceId(ServletRequest servletRequest) {
return null;
}
}
第2步,创建一个消费者的过滤器,注意这个Fiter的包是dubbo的,跟第一步那个fiter不是一个包。这里我们要用到dubbo的SPI机制,此处重写了dubbo的filter方法
/**
- @author zhuxinji
- @date 2021-12-29-11:00
- @DESC
*/
@Slf4j
@Activate(group = {Constants.CONSUMER_PROTOCOL})
public class LogTraceConsumerFilter implements Filter {
@Override
public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
// 消费者负责把调用链端的MDC信息传递到生产者,traceid如果不是被调用时是空的,例如调度任务,此时我们重新生成一个方便BIZ查询
String traceId = StringUtils.isNotBlank(MDC.get(TraceConstants.TRACE_ID)) ? MDC.get(TraceConstants.TRACE_ID) : UUID.randomUUID().toString();
String spanId = StringUtils.isNotBlank(MDC.get(TraceConstants.SPAN_ID)) ? MDC.get(TraceConstants.TRACE_ID) : UUID.randomUUID().toString();
String logicId = MDC.get(TraceConstants.LOGIC_ID);
if (StringUtils.isNotBlank(traceId) && StringUtils.isNotBlank(spanId) && StringUtils.isNotBlank(logicId)) {
// 逻辑Id+1
String newLogicId = TraceUtil.incrLogicId(logicId);
MDC.put(TraceConstants.LOGIC_ID, newLogicId);
// 传递traceId spanId logicId
Map<String, String> attachments = invocation.getAttachments();
attachments.put(TraceConstants.TRACE_ID, traceId);
attachments.put(TraceConstants.SPAN_ID, spanId);
attachments.put(TraceConstants.LOGIC_ID, newLogicId);
}
return invoker.invoke(invocation);
}
}
第三步,在服务这种创建如第2步一样的过滤器,代码如下
/**
- @author zhuxinji
- @date 2021-12-29-11:04
- @DESC
*/
@Slf4j
@Activate(group = {Constants.PROVIDER_PROTOCOL})
public class LogTraceProviderFilter implements Filter {
@Override
public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
try {
// 生产者负责把信息放进MDC里面
Map<String, String> attachments = invocation.getAttachments();
String traceId = MapUtils.getString(attachments, TraceConstants.TRACE_ID);
String spanId = MapUtils.getString(attachments,TraceConstants.SPAN_ID);
String logicId = MapUtils.getString(attachments,TraceConstants.LOGIC_ID);
if (StringUtils.isNotBlank(traceId) && StringUtils.isNotBlank(spanId) && StringUtils.isNotBlank(logicId)) {
// 生成新的spanId
String newSpanId = TraceUtil.getNewSpanId(spanId, logicId);
MDC.put(TraceConstants.TRACE_ID, traceId);
MDC.put(TraceConstants.SPAN_ID, newSpanId);
MDC.put(TraceConstants.LOGIC_ID, “0”);
}
if (log.isInfoEnabled()) {
log.info(“dubbo服务执行dubbo方法{}.{}”, invoker.getInterface().getName(), invocation.getMethodName());
}
Result result = invoker.invoke(invocation);
return result;
} finally {
TraceUtil.clearMDC();
}
}
}
第4步,在服务者中resource路径下创建一个这样路径和名称的文件夹和文件
注意了哦,这里文件夹要一级一级的创建,不要图快一次写入全路径
内容为这样子的,路径你们自己替换成自己的
logTraceProviderFilter=LogTraceProviderFilter的路径
同第4步,在消费者中也创建一个这样的文件夹及文件,把内容对应改成消费者过滤器的名字。
然后下一步配置logBack的文件
这个要看你项目在哪里配置的,我就是在resource下面配置的XML,这是比较多的配置方式
日志输出格式的内容可以自定义,我这里复制一个给大家
到这里一个完整的分布式链路追踪日志就好了。
至于原理,就是使用了MDC和dubbo的SPI。MDC又是使用了threadLocal。
这里是比较简单的实现了,再复杂点还需要在子线程上加上,那样可能就要重新下threadLocal。大家可以自行百度,我这里就不写了,就是在我们现有的基础上再搞点东西,有兴趣的可以玩玩。我这里就是傻瓜式记录一下,方便笔者快速实现。
走过路过点个收藏。