原文链接:基于SLF4J的MDC机制和Dubbo的Filter机制,实现分布式系统的日志全链路追踪

一、日志系统

1、日志框架

在每个系统应用中,我们都会使用日志系统,主要是为了记录必要的信息和方便排查问题。

而现在主流的就是 SLF4J + Logback。

当我们的系统是单体应用,日志做起来时非常简单的,直接使用 log.info,log.error,log.warn 等等方法。

而当我们的系统是分布式系统,服务之间通信通常都是使用 Dubbo 这个 RPC 框架。

此时做日志就不是这么简单了,因为不同服务都是不同的实例,日志就无法做到一起了,怎么将服务间的调用串联起来,也是一个问题。

但是呢,SLF4J 提供了一个 MDC 机制,它的设计目标就是为了应对分布式应用系统的审计和调试。

所以,我们可以利用 MDC ,然后配合 Dubbo 的 RpcContext 来做分布式系统的全链路日志功能。

2、搭建日志系统

前提:我们使用的是 Spring Boot 项目。

Spring Boot 引入日志依赖:

  1. <!-- log begin -->
  2. <dependency>
  3. <groupId>org.slf4j</groupId>
  4. <artifactId>slf4j-api</artifactId>
  5. </dependency>
  6. <dependency>
  7. <groupId>ch.qos.logback</groupId>
  8. <artifactId>logback-core</artifactId>
  9. </dependency>
  10. <dependency>
  11. <groupId>com.alibaba</groupId>
  12. <artifactId>fastjson</artifactId>
  13. <version>1.2.68</version>
  14. </dependency>
  15. <!-- log end -->

加入 Logback 的 xml 配置文件:

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <configuration debug="false">
  3. <!-- 程序服务名 -->
  4. <springProperty scope="context" name="SERVICE_NAME" source="spring.application.name" defaultValue="unknown"/>
  5. <!-- 定义日志的根目录 -->
  6. <springProperty scope="context" name="LOG_PATH" source="logging.file.path" defaultValue="/Users/howinfun/weblog/java/${SERVICE_NAME}"/>
  7. <!-- 日志输出格式 -->
  8. <springProperty scope="context" name="LOG_PATTERN" source="logging.pattern" defaultValue="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [%-5level] [%logger{5}] [%X{uri}] [%X{trace_id}] - %msg%n"/>
  9. <!-- 控制台输出 -->
  10. <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
  11. <layout class="ch.qos.logback.classic.PatternLayout">
  12. <!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符-->
  13. <pattern>${LOG_PATTERN}</pattern>
  14. </layout>
  15. <!--此日志appender是为开发使用,只配置最底级别,控制台输出的日志级别是大于或等于此级别的日志信息-->
  16. <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
  17. <level>debug</level>
  18. </filter>
  19. <encoder>
  20. <Pattern>${LOG_PATTERN}</Pattern>
  21. <!-- 设置字符集 -->
  22. <charset>UTF-8</charset>
  23. </encoder>
  24. </appender>
  25. // .... 还有各种 Info、Warn、Error 等日志配置
  26. <!-- 日志输出级别 -->
  27. <root level="INFO">
  28. <appender-ref ref="STDOUT" />
  29. </root>
  30. </configuration>

上面的注释已经写得非常的清晰了,而我们最主要关注的就是LOG_PATTERN 这个属性。它主要是规定了日志打印的规范,如何打印日志,日志该带上哪些关键信息。

  • [%X{uri}]:这里主要是记录接口的请求 uri。
  • [%X{trace_id}]:这里主要是记录此次请求的 TraceId,这个 TraceId 也会带到 Dubbo 的服务提供端,让整个链路都带上这个 TraceId。这样在日志记录的时候,全都可以利用 TraceId 了。

这样等到日志排查的时候,只需要前端或者测试给后端的同学提供一个 TraceId,我们就能非常快速的定位到问题所在了。

下面的项目都是引入上面的依赖和加入 xml 文件即可。

二、项目搭建

接下来我们会创建四个项目,分别是 dubbo-api(提供API和工具类)、dubbo-provider-one(Dubbo 服务提供者1)、dubbo-provider-two(Dubbo 服务提供者2)、dubbo-consumer(Dubbo 服务消费者,对外提供 HTTP 接口)。

1、dubbo-api:

这里面最重要的是 TraceUtil 工具类。

这个工具类提供了几个很重要的方法:

  • TraceId 的初始化:生成 TraceId,并利用 MDC 将 Trace 相关信息存放在当前线程(请求)的 ThreaLocal 中。
  • TraceId 的存放:将当前线程(请求)的 Trace 相关信息存放在 Dubbo 的 RPC 上下文 RpcContext 中,这样可以将当前请求的 Trace 信息传递到 Dubbo 的服务提供者。
  • TraceId 的获取:当然了,Dubbo 的服务提供者也可以利用这工具类,从 RpcContext 中获取 Trace 信息。

下面直接上代码:

  1. /**
  2. * Trace 工具
  3. * @author winfun
  4. * @date 2020/10/30 9:02 上午
  5. **/
  6. public class TraceUtil {
  7. public final static String TRACE_ID = "trace_id";
  8. public final static String TRACE_URI = "uri";
  9. /**
  10. * 初始化 TraceId
  11. * @param uri 请求uri
  12. */
  13. public static void initTrace(String uri) {
  14. if(StringUtils.isBlank(MDC.get(TRACE_ID))) {
  15. String traceId = generateTraceId();
  16. setTraceId(traceId);
  17. MDC.put(TRACE_URI, uri);
  18. }
  19. }
  20. /**
  21. * 从 RpcContext 中获取 Trace 相关信息,包括 TraceId 和 TraceUri
  22. * 给 Dubbo 服务端调用
  23. * @param context Dubbo 的 RPC 上下文
  24. */
  25. public static void getTraceFrom(RpcContext context) {
  26. String traceId = context.getAttachment(TRACE_ID);
  27. if (StringUtils.isNotBlank(traceId)){
  28. setTraceId(traceId);
  29. }
  30. String uri = context.getAttachment(TRACE_URI);
  31. if (StringUtils.isNotEmpty(uri)) {
  32. MDC.put(TRACE_URI, uri);
  33. }
  34. }
  35. /**
  36. * 将 Trace 相关信息,包括 TraceId 和 TraceUri 放入 RPC上下文中
  37. * 给 Dubbo 消费端调用
  38. * @param context Dubbo 的 RPC 上下文
  39. */
  40. public static void putTraceInto(RpcContext context) {
  41. String traceId = MDC.get(TRACE_ID);
  42. if (StringUtils.isNotBlank(traceId)) {
  43. context.setAttachment(TRACE_ID, traceId);
  44. }
  45. String uri = MDC.get(TRACE_URI);
  46. if (StringUtils.isNotBlank(uri)) {
  47. context.setAttachment(TRACE_URI, uri);
  48. }
  49. }
  50. /**
  51. * 从 MDC 中清除当前线程的 Trace信息
  52. */
  53. public static void clearTrace() {
  54. MDC.clear();
  55. }
  56. /**
  57. * 将traceId放入MDC
  58. * @param traceId 链路ID
  59. */
  60. private static void setTraceId(String traceId) {
  61. traceId = StringUtils.left(traceId, 36);
  62. MDC.put(TRACE_ID, traceId);
  63. }
  64. /**
  65. * 生成traceId
  66. * @return 链路ID
  67. */
  68. private static String generateTraceId() {
  69. return TraceIdUtil.nextNumber();
  70. }
  71. }

2、dubbo-consumer

项目结构如下:

项目是基于 Spring Boot 框架搭建的,使用 dubbo-spring-boot-starter 整合 Dubbo 框架。

WebRequestFilter:

首先,利用 @WebFilter,拦截所有 Http 请求,然后利用 TraceUtil 给这个请求初始化对应的 Trace 信息,然后将 Trace 信息利用 SLF4J 提供的 MDC 机制存放起来。之后利用 Logger 打日志的时候,会带上 Trace 信息。

下面上代码:

  1. /**
  2. * Web Request Filter
  3. * @author winfun
  4. * @date 2020/10/30 3:02 下午
  5. **/
  6. @Slf4j
  7. @Order(1)
  8. @WebFilter(urlPatterns = "/*")
  9. public class WebRequestFilter implements Filter {
  10. @Override
  11. public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
  12. HttpServletRequest request = (HttpServletRequest) servletRequest;
  13. HttpServletResponse response = (HttpServletResponse) servletResponse;
  14. String uri = request.getRequestURI();
  15. // 初始化 TraceId
  16. TraceUtil.initTrace(uri);
  17. filterChain.doFilter(request,response);
  18. // 清除 TraceId 和 TraceUri
  19. TraceUtil.clearTrace();
  20. }
  21. }

DubboTraceFilter:

接着,我们利用 Dubbo 提供的 Filter 机制,在每次进行 Dubbo 调用的前后,进行日志打印。

在过滤器的最开始,首先会处理 Trace 相关信息:

  • 如果是当前调用时消费者的话,主动从 MDC 中获取 Trace 信息并放入 RpcContext 中,这样可以将 Trace 信息传递到服务提供者那边。
  • 如果当前是服务提供者,则可以从 RpcContext 中获取 Trace 信息,接着利用 MDC 将 Trace 信息保存起来。

下面上代码:

  1. /**
  2. * Dubbo Trace Filter
  3. * @author winfun
  4. * @date 2020/10/30 9:46 上午
  5. **/
  6. @Slf4j
  7. @Activate(order = 100,group = {Constants.PROVIDER_PROTOCOL,Constants.CONSUMER_PROTOCOL})
  8. public class DubboTraceFilter implements Filter {
  9. @Override
  10. public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
  11. // 处理 Trace 信息
  12. printRequest(invocation);
  13. // 执行前
  14. handleTraceId();
  15. long start = System.currentTimeMillis();
  16. Result result = invoker.invoke(invocation);
  17. long end = System.currentTimeMillis();
  18. // 执行后
  19. printResponse(invocation,result,end-start);
  20. return result;
  21. }
  22. /***
  23. * 打印请求
  24. * @author winfun
  25. * @param invocation invocation
  26. * @return {@link }
  27. **/
  28. private void printRequest(Invocation invocation){
  29. DubboRequestDTO requestDTO = new DubboRequestDTO();
  30. requestDTO.setInterfaceClass(invocation.getInvoker().getInterface().getName());
  31. requestDTO.setMethodName(invocation.getMethodName());
  32. requestDTO.setArgs(getArgs(invocation));
  33. log.info("call Dubbo Api start , request is {}",requestDTO);
  34. }
  35. /***
  36. * 打印结果
  37. * @author winfun
  38. * @param invocation invocation
  39. * @param result result
  40. * @return {@link }
  41. **/
  42. private void printResponse(Invocation invocation,Result result,long spendTime){
  43. DubboResponseDTO responseDTO = new DubboResponseDTO();
  44. responseDTO.setInterfaceClassName(invocation.getInvoker().getInterface().getName());
  45. responseDTO.setMethodName(invocation.getMethodName());
  46. responseDTO.setResult(JSON.toJSONString(result.getValue()));
  47. responseDTO.setSpendTime(spendTime);
  48. log.info("call Dubbo Api end , response is {}",responseDTO);
  49. }
  50. /***
  51. * 获取 Invocation 参数,过滤掉大参数
  52. * @author winfun
  53. * @param invocation invocation
  54. * @return {@link Object[] }
  55. **/
  56. private Object[] getArgs(Invocation invocation){
  57. Object[] args = invocation.getArguments();
  58. args = Arrays.stream(args).filter(arg->{
  59. if (arg instanceof byte[] || arg instanceof Byte[] || arg instanceof InputStream || arg instanceof File){
  60. return false;
  61. }
  62. return true;
  63. }).toArray();
  64. return args;
  65. }
  66. /***
  67. * 处理 TraceId,如果当前对象是服务消费者,则将 Trace 信息放入 RpcContext中
  68. * 如果当前对象是服务提供者,则从 RpcContext 中获取 Trace 信息。
  69. * @author winfun
  70. * @return {@link }
  71. **/
  72. private void handleTraceId() {
  73. RpcContext context = RpcContext.getContext();
  74. if (context.isConsumerSide()) {
  75. TraceUtil.putTraceInto(context);
  76. } else if (context.isProviderSide()) {
  77. TraceUtil.getTraceFrom(context);
  78. }
  79. }
  80. }

ResponseBodyAdvice:

还有一个比较重要的点是,我们需要在接口返回时将 TraceId 返回给前端,我们当然不可能在每个接口那里植入返回 TraceId 的代码,而是利用 ResponseBodyAdvice,可以在接口结果返回前,对返回结果进行进一步的处理。

下面上代码:

  1. /**
  2. * Response Advice
  3. * @author winfun
  4. * @date 2020/10/30 3:47 下午
  5. **/
  6. @RestControllerAdvice(basePackages = "com.winfun")
  7. public class WebResponseModifyAdvice implements ResponseBodyAdvice {
  8. @Override
  9. public boolean supports(final MethodParameter methodParameter, final Class converterType) {
  10. // 返回 class 为 ApiResult(带 TraceId 属性) & converterType 为 Json 转换
  11. return methodParameter.getMethod().getReturnType().isAssignableFrom(ApiResult.class)
  12. && converterType.isAssignableFrom(MappingJackson2HttpMessageConverter.class);
  13. }
  14. @Override
  15. public Object beforeBodyWrite(final Object body, final MethodParameter methodParameter, final MediaType mediaType, final Class aClass,
  16. final ServerHttpRequest serverHttpRequest, final ServerHttpResponse serverHttpResponse) {
  17. // 设置 TraceId
  18. ((ApiResult<?>) body).setTraceId(MDC.get(TraceUtil.TRACE_ID));
  19. return body;
  20. }
  21. }

3、dubbo-provider-one & dubbo-provider-two



服务提供者也是非常的简单,同样只需要使用上面消费者的 DubboTraceFiler 即可,里面会先从 RpcContext 中获取 Trace 信息,然后将 Dubbo 调用前的 Request 和调用后的 Response 都打印出来。就没有其他多余的动作了。

三、测试

1、接口如下:

接口非常简单,直接引入两个服务提供者的依赖,然后进行 Dubbo 接口的调用,最后将俩接口的返回值拼接起来返回给前端即可。

下面上代码:

  1. /**
  2. * Say Hello & Hi
  3. * @author winfun
  4. * @date 2020/10/29 5:12 下午
  5. **/
  6. @RequestMapping("/hello")
  7. @RestController
  8. public class HelloController {
  9. @DubboReference(check = false,lazy = true)
  10. private DubboServiceOne dubboServiceOne;
  11. @DubboReference(check = false,lazy = true)
  12. private DubboServiceTwo dubboServiceTwo;
  13. @GetMapping("/{name}")
  14. public ApiResult sayHello(@PathVariable("name") String name){
  15. String hello = dubboServiceOne.sayHello(name);
  16. String hi = dubboServiceTwo.sayHi(name);
  17. return ApiResult.success(hello+" "+hi);
  18. }
  19. }

2、接口返回:

我们可以看到接口已经成功返回,并且可以看到 TraceId 为16042841032799628772

接下来,我们看看消费者的后台打印是否是同一个 TraceId,无疑是一样的:

最后,我们要确定两个服务提供者是否能拿到对应的 Trace 信息:

服务提供者One:

服务提供者Two:



到此,我们可以发现:不管是前端,Dubbo 消费者,和 Dubbo 提供者,都是同一个 TraceId。这样的话,我们整个日志链路就跑通了。

四、最后

当然了,上面的日志全链路追踪只适合用于 Dubbo 作为 PRC 框架。假设我们使用 OpenFeign 的话,只能自己再做扩展了。

虽然项目代码不多,但是就不全部放上来了,如果大家感兴趣,可以去看看:全链路日志记录

基于SLF4J的MDC机制和Dubbo的Filter机制,实现分布式系统的日志全链路追踪的更多相关文章

  1. Dubbo 全链路追踪日志的实现

    微服务架构的项目,一次请求可能会调用多个微服务,这样就会产生多个微服务的请求日志,当我们想要查看整个请求链路的日志时,就会变得困难,所幸的是我们有一些集中日志收集工具,比如很热门的ELK,我们需要把这 ...

  2. Net和Java基于zipkin的全链路追踪

    在各大厂分布式链路跟踪系统架构对比 中已经介绍了几大框架的对比,如果想用免费的可以用zipkin和pinpoint还有一个忘了介绍:SkyWalking,具体介绍可参考:https://github. ...

  3. 聊聊Dubbo - Dubbo可扩展机制实战

    1. Dubbo的扩展机制 在Dubbo的官网上,Dubbo描述自己是一个高性能的RPC框架.今天我想聊聊Dubbo的另一个很棒的特性, 就是它的可扩展性. 如同罗马不是一天建成的,任何系统都一定是从 ...

  4. Dubbo的Filter实战--整合Oval校验框架

    前言: 其实很早之前就想写一篇关于oval和具体服务相整合的常见做法, 并以此作为一篇笔记. 趁现在项目中间空闲期, 刚好对dubbo的filter有一些了解. 因此想结合两者, 写一下既结合校验框架 ...

  5. Dubbo的Filter链梳理---分组可见和顺序调整

    前言: 刚刚写了篇博文: Dubbo透传traceId/logid的一种思路, 对dubbo的filter机制有了一个直观的理解. 同时对filter也多了一些好奇心, 好奇filter链是如何组织的 ...

  6. 基于Dapper的分布式链路追踪入门——Opencensus+Zipkin+Jaeger

    微信搜索公众号 「程序员白泽」,进入白泽的编程知识分享星球 最近做了一些分布式链路追踪有关的东西,写篇文章来梳理一下思路,或许可以帮到想入门的同学.下面我将从原理到demo为大家一一进行讲解,欢迎评论 ...

  7. (Dubbo架构)基于MDC+Filter的跨应用分布式日志追踪解决方案

    在单体应用中,日志追踪通常的解决方案是给日志添加 tranID(追踪ID),生成规则因系统而异,大致效果如下: 查询时只要使用 grep 命令进行追踪id筛选即可查到此次调用链中所有日志,但是在 du ...

  8. Dubbo的SPI机制与JDK机制的不同及原理分析

    从今天开始,将会逐步介绍关于DUbbo的有关知识.首先先简单介绍一下DUbbo的整体概述. 概述 Dubbo是SOA(面向服务架构)服务治理方案的核心框架.用于分布式调用,其重点在于分布式的治理. 简 ...

  9. dubbo traceId透传实现日志链路追踪(基于Filter和RpcContext实现)

    一.要解决什么问题: 使用elk的过程中发现如下问题: 1.无法准确定位一个请求经过了哪些服务 2.多个请求线程的日志交替打印,不利于查看按时间顺序查看一个请求的日志. 二.期望效果 能够查看一个请求 ...

随机推荐

  1. Java 内功修炼 之 数据结构与算法(一)

    一.基本认识 1.数据结构与算法的关系? (1)数据结构(data structure): 数据结构指的是 数据与数据 之间的结构关系.比如:数组.队列.哈希.树 等结构. (2)算法: 算法指的是 ...

  2. Linux安装软件方法总结

    相比于windows系统,Linux安装程序就比较复杂了,很多需要root用户才能安装.常见的有以下几种安装方法 源码安装 rpm包安装 yum安装 (RedHat.CentOS) apt-get安装 ...

  3. kubernetes1.15极速部署prometheus和grafana

    关于prometheus和grafana prometheus负责监控数据采集,grafana负责展示,下图来自官网: 环境信息 硬件:三台CentOS 7.7服务器 kubernetes:1.15 ...

  4. Arduino 跑马灯

    参考: 1. https://blog.csdn.net/hunhun1122/article/details/70254606 2. http://www.51hei.com/arduino/392 ...

  5. LPCTSTR的含义

    LPCTSTR: LP代表指针.C代表不可改变.T代表根据是否定义UNICODE宏而分别define为char或wchar_t.STR代表字符串. 例如: LPCTSTR lp="BMP F ...

  6. Oracle Database XE 11gR2 SQL 命令行的显示调整

    操作系统:Windows 10 x64 Oracle Database XE 11gR2 参考:在cmd命令行使用sqlplus时的页面显示问题 通过 cmd 命令行或运行 SQL 命令行查看一张表的 ...

  7. hasura的golang反向代理

    概述 反向代理代码 对请求的处理 对返回值的处理 遇到的问题 概述 一直在寻找一个好用的 graphql 服务, 之前使用比较多的是 prisma, 但是 prisma1 很久不再维护了, 而 pri ...

  8. day46 Pyhton 数据库Mysql 03

    一内容回顾 存储引擎:主要描述的是数据存储的不同方式 innodb 支持事务\支持外键\行级锁\聚焦索引 myisam 不支持事务\不支持外键\表级锁\非聚焦索引 memory 只能在内存中存储表数据 ...

  9. vue知识点11

    1. Vue.js 是什么       Vue是一套用于构建用户界面的渐进式框架 2. vue的环境搭建(Vue2 ) 3. 经典的hello world         new Vue({      ...

  10. 手把手教你如何制作和使用lib和dll

    本文的内容经过本人亲自调试,确保可用,实用,测试环境为win10+vs2015+C++ 目录 静态库 什么是静态库? 怎么创建 如何使用 静态库的第一种使用方法 静态库的第二种使用方法 动态链接库 动 ...