Spring Cloud 全链路日志traceId

Spring Cloud 全链路日志traceId

​ 随着业务量的增加,线上出现越来越多的bug,但是由于使用的是Spring Cloud,微服务之间调用,输出的日志没有固定上下文管理,定位具体问题存在诸多不便,因此相当有必要引入全链路日志traceId。

MDC(Mapped Diagnostic Context,映射调试上下文)是 log4j 、logback及log4j2 提供的一种方便在多线程条件下记录日志的功能。

借助MDC可以非常方便的将traceId自动输出到日志中,无需手动拼写traceId,代码简单,风格统一。

基本用法

这里使用spirngBoot进行测试

@RunWith(SpringRunner.class)
@Log4j2
public class Test {
    @Test
    public void testMDC() throws InterruptedException {
       MDC.put("traceId", UUID.randomUUID().toString());
        log.info("开始调用服务A,进行业务处理");
        log.info("业务处理完毕,可以释放空间了,避免内存泄露");
    	//MDC put 一定要对应一个 remove
        MDC.remove("traceId");
        log.info("traceId {}", MDC.get("traceId"));
    }
}

在了log4j.xml 或 logback.xml中进行配置,%X{traceId} 获取traceId信息。 以log4j2为例:

<?xml version="1.0" encoding="UTF-8"?>
<configuration status="DEBUG" monitorInterval="30">
    <Properties>
        <property name="log_pattern">[%t] [%X{traceId}] %m %n</property>
    </Properties>
    <appenders>
        <console name="Console" target="SYSTEM_OUT">
            <PatternLayout pattern="${log_pattern}"/>
        </console>
    </appenders>
</configuration>
[main] [08cf04bd-795f-4fb1-83d5-a137dc222af8] 开始调用服务A,进行业务处理 
[main] [08cf04bd-795f-4fb1-83d5-a137dc222af8] 业务处理完毕,可以释放空间了,避免内存泄露 
[main] [] traceId  

可以看到在执行MDC put 后,日志中输出同样的traceId,MDC remove后日志中不在输出。

下面再来看下另外一个场景

@RunWith(SpringRunner.class)
@Log4j2
public class Test {
    @Test
    public void testMDC() throws InterruptedException {
        new RunTest("A").start();
        new RunTest("B").start();
        Thread.sleep(20000);
    }
	class RunTest extends Thread {
        private String code;
        public RunTest(String code) {
            this.code = code;
        }
        @Override
        public void run() {
            MDC.put("traceId", UUID.randomUUID().toString());
            log.info("开始调用服务{}", code);
            try {
                Thread.sleep(10000);
            } catch (InterruptedException e) {
                log.info(e.getMessage());
            }
            log.info("服务{}处理完毕", code);
            MDC.remove("traceId");
        }
}
[Thread-1] [bdbe4aa7-60d4-4528-bc45-928aa94e3660] 开始调用服务A 
[Thread-2] [d3d0fa7d-6204-4f35-b6ed-01a9a8e2a42f] 开始调用服务B 
[Thread-2] [d3d0fa7d-6204-4f35-b6ed-01a9a8e2a42f] 服务B处理完毕 
[Thread-1] [bdbe4aa7-60d4-4528-bc45-928aa94e3660] 服务A处理完毕 

从上面结果可以看出,MDC是和线程绑定在一起的。

MDC的特性
优点:
  • 代码简洁,日志风格统一,不需要在log打印中手动拼写traceId,即LOGGER.info("traceId:{} ", traceId)
MDC 存在的问题
  • 子线程中打印日志丢失
  • 跨服务调用打印日志丢失
使用时需要注意点
  • MDC put 操作一定要对应有一个 remove操作
MDC 的实现原理

​ 要想知道 MDC为什么会有上面的表现,有必要了解下MDC的实现原理。

​ MDC 来源于slf4j包,SLF4J,全称是 Simple Logging Facade for Java,翻译过来就是「一套简单的日志门面」。是为了让研发人员在项目中切换日志组件的方便,特意抽象出的一层。其中 Logger 就来自于 SLF4J 的规范包,项目中一旦这样定义 Logger,在底层就可以无缝切换 logback、log4j等日志组件。

​ 查看源码可以看到, MDC 主要是通过 MDCAdapter 来完成 put、get、remove 等操作。

public class MDC {
    static final String NULL_MDCA_URL = "http://www.slf4j.org/codes.html#null_MDCA";
    static final String NO_STATIC_MDC_BINDER_URL = "http://www.slf4j.org/codes.html#no_static_mdc_binder";
    static MDCAdapter mdcAdapter;
    public static void put(String key, String val) throws IllegalArgumentException {
        if (key == null) {
            throw new IllegalArgumentException("key parameter cannot be null");
        }
        if (mdcAdapter == null) {
            throw new IllegalStateException("MDCAdapter cannot be null. See also " + NULL_MDCA_URL);
        }
        mdcAdapter.put(key, val);
    }
    public static String get(String key) throws IllegalArgumentException {
        if (key == null) {
            throw new IllegalArgumentException("key parameter cannot be null");
        }
        if (mdcAdapter == null) {
            throw new IllegalStateException("MDCAdapter cannot be null. See also " + NULL_MDCA_URL);
        }
        return mdcAdapter.get(key);
    }
    private static MDCAdapter bwCompatibleGetMDCAdapterFromBinder() throws NoClassDefFoundError {
        try {
            return StaticMDCBinder.getSingleton().getMDCA();
        } catch (NoSuchMethodError nsme) {
            // binding is probably a version of SLF4J older than 1.7.14
            return StaticMDCBinder.SINGLETON.getMDCA();
        }
    }

    static {
        try {
            mdcAdapter = bwCompatibleGetMDCAdapterFromBinder();
        } catch (NoClassDefFoundError ncde) {
            mdcAdapter = new NOPMDCAdapter();
            String msg = ncde.getMessage();
            if (msg != null && msg.contains("StaticMDCBinder")) {
                Util.report("Failed to load class \"org.slf4j.impl.StaticMDCBinder\".");
                Util.report("Defaulting to no-operation MDCAdapter implementation.");
                Util.report("See " + NO_STATIC_MDC_BINDER_URL + " for further details.");
            } else {
                throw ncde;
            }
        } catch (Exception e) {
            // we should never get here
            Util.report("MDC binding unsuccessful.", e);
        }
    }
 	// ....
}

在这里插入图片描述

​ MDCAdapter 使用到的实现类是 Log4jMDCAdapter

Log4jMDCAdapter.class

public class Log4jMDCAdapter implements MDCAdapter {

    @Override
    public void put(final String key, final String val) {
        ThreadContext.put(key, val);
    }

    @Override
    public String get(final String key) {
        return ThreadContext.get(key);
    }
   //...
}

可以看到 Log4jMDCAdapter所有的操作都是借助ThreadContext来实现的。

org.apache.logging.log4j.spi.DefaultThreadContextMap#createThreadLocalMap

ThreadContext默认情况下,使用纯ThreadLocal。

根据ThreadLocal的特性我们不难理解MDC的特性

如果你使用logback MDCAdapter 的实现是 LogbackMDCAdapter ,也是使用 TreadLocal 实现

Spring Cloud 引入MDC

​ Spring Cloud 使用 spring gateway + fegin + log4j2

​ 首先需要知道的几点

  • 子线程中打印日志丢失

  • 跨服务调用打印日志丢失

  • MDC put 操作一定要对应有一个 remove操作

我们大致梳理下那些地方需要传递 traceId。一般的微服务大致以如下架构(这个仅考虑需要传递 traceId的地方)
在这里插入图片描述

  1. http调入 ,调出

  2. 微服务调用

  3. 线程调用
    根据如上几点,我们需要一一进行配置、

1.http调入调出

http 调入情况

http调用traceId一般都是配置再header信息上。

gateway配置,增加 GlobalFilter,代码如下

@Log4j2
@Component
public class LogGlobalFilter implements GlobalFilter, Ordered {

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        Object traceId = request.getHeaders().get(AppConstant.TRACE_ID);
        if (traceId == null) {
            traceId = IdUtil.objectId();
            request = exchange.getRequest().mutate()
                        .header(AppConstant.TRACE_ID, traceId.toString())
                        .build();
        }
        ServerHttpResponse response = exchange.getResponse();
        response.getHeaders().add(AppConstant.TRACE_ID,traceId.toString());
        exchange = exchange.mutate().request(request).response(response).build();
        return chain.filter(exchange);
    }

    @Override
    public int getOrder() { return 0; }
}

微服务 spring boot 配置

需要添加拦截器拿到request header中的traceId并添加到MDC中

public class LogInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
       //如果有上层调用就用上层的ID
        String traceId = request.getHeader(AppConstant.TRACE_ID);
        if (traceId == null) {
            traceId = IdUtil.objectId();
        }
        MDC.put(AppConstant.TRACE_ID, traceId);
        return true;
    }
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView)
            throws Exception {
    }
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
            throws Exception {
        MDC.remove(AppConstant.TRACE_ID);
    }
}

对于http调出的情况

HttpClient: HttpClient拦截器

public class HttpClientTraceIdInterceptor implements HttpRequestInterceptor {
    @Override
    public void process(HttpRequest httpRequest, HttpContext httpContext) throws HttpException, IOException {
        String traceId = MDC.get(AppConstant.TRACE_ID);
        //当前线程调用中有traceId,则将该traceId进行透传
        if (traceId != null) {
            //添加请求体
            httpRequest.addHeader(AppConstant.TRACE_ID, traceId);
        }
    }
}

通过addInterceptorFirst方法为HttpClient添加拦截器

private static CloseableHttpClient httpClient = HttpClientBuilder.create()
              .addInterceptorFirst(new HttpClientTraceIdInterceptor())

OKHttp:实现OKHttp拦截器

public class OkHttpTraceIdInterceptor implements Interceptor {
      @Override
      public Response intercept(Chain chain) throws IOException {
          String traceId = MDC.get(AppConstant.TRACE_ID);
          Request request = null;
          if (StringUtil.isNotEmpty(traceId)) {
              //添加请求体
              request = chain.request().newBuilder().addHeader(AppConstant.TRACE_ID, traceId).build();
          }
          Response originResponse = chain.proceed(request);
          return originResponse;
      }
}

为OkHttp添加拦截器

private static OkHttpClient client = new OkHttpClient.Builder()
              .addNetworkInterceptor(new OkHttpTraceIdInterceptor())
              .build();

RestTemplate: 实现RestTemplate拦截器

  public class RestTemplateTraceIdInterceptor implements ClientHttpRequestInterceptor {
      @Override
      public ClientHttpResponse intercept(HttpRequest httpRequest, byte[] bytes, ClientHttpRequestExecution clientHttpRequestExecution) throws IOException {
          String traceId = MDC.get(AppConstant.TRACE_ID);
          if (traceId != null) {
              httpRequest.getHeaders().add(AppConstant.TRACE_ID, traceId);
          }
  
          return clientHttpRequestExecution.execute(httpRequest, bytes);
      }
}

为RestTemplate添加拦截器

  restTemplate.setInterceptors(Arrays.asList(new RestTemplateTraceIdInterceptor()));
2.微服务之间调用

由于是 fegin 进行微服务调用,可以通过Feign的拦截器RequestInterceptor实现,Feign调用实际上是requestTemplate http调用。

@Slf4j
@Component
public class FeignLogInterceptor implements RequestInterceptor {
    @Override
    public void apply(RequestTemplate template) {
 		String traceId = MDC.get(AppConstant.TRACE_ID);
        if(StringUtil.isNotEmpty(traceId)){
            requestTemplate.header(AppConstant.TRACE_ID, traceId);
        }
    }
}

其他微服务调用思路类似

grpc 可以通过 interceptor 实现

dubbo 和 motan 可以通过 filter 实现

3.服务内部线程间调用

服务间调用就是存在子线程的时候调用需要传递traceId。线程调用我们一般都是通过线程池调用,可以通过重写线程池传递traceId。

线程池封装:ThreadPoolExecutorMdcWrapper.java

public class ThreadPoolExecutorMdcWrapper extends ThreadPoolExecutor {
      public ThreadPoolExecutorMdcWrapper(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit,
                                          BlockingQueue<Runnable> workQueue) {
          super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
      }
  
      public ThreadPoolExecutorMdcWrapper(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit,
                                          BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory) {
          super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory);
      }
  
      public ThreadPoolExecutorMdcWrapper(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit,
                                          BlockingQueue<Runnable> workQueue, RejectedExecutionHandler handler) {
          super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, handler);
      }
  
      public ThreadPoolExecutorMdcWrapper(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit,
                                          BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory,
                                          RejectedExecutionHandler handler) {
          super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, handler);
      }
      @Override
      public void execute(Runnable task) {
          super.execute(wrap(task, MDC.getCopyOfContextMap()));
      }
      @Override
      public <T> Future<T> submit(Runnable task, T result) {
          return super.submit(wrap(task, MDC.getCopyOfContextMap()), result);
      }
      @Override
      public <T> Future<T> submit(Callable<T> task) {
          return super.submit(wrap(task, MDC.getCopyOfContextMap()));
      }
      @Override
      public Future<?> submit(Runnable task) {
          return super.submit(wrap(task, MDC.getCopyOfContextMap()));
      }
      public  <T> Callable<T> wrap(final Callable<T> callable, final Map<String, String> context) {
          return () -> {
              if (context == null) {
                  MDC.clear();
              } else {
                  MDC.setContextMap(context);
              }
              setTraceIdIfAbsent();
              try {
                  return callable.call();
              } finally {
                  MDC.clear();
              }
          };
      }
  
      public  Runnable wrap(final Runnable runnable, final Map<String, String> context) {
          return () -> {
              if (context == null) {
                  MDC.clear();
              } else {
                  MDC.setContextMap(context);
              }
              setTraceIdIfAbsent();
              try {
                  runnable.run();
              } finally {
                  MDC.clear();
              }
          };
      }
      public  void setTraceIdIfAbsent() {
          if (MDC.get(AppConstant.TRACE_ID) == null) {
              MDC.put(AppConstant.TRACE_ID, IdUtil.getTraceId());
          }
      }
}

线程运行前put TRACE_ID ,线程运行完成后 MDC.clear()

4.其他方式调用

​ 有些框架调用可能不在上面的场景上,这种可以通过 AOP实现。

​ 如 xxl-job 调用,这种可以通过AOP实现。

/**
 * 日志拦截器
 */
@Component
@Aspect
public class LogAspect {
    /**
     * 拦截入口
     */
    @Pointcut("@annotation(com.xxl.job.core.handler.annotation.XxlJob)")
    public void pointCut() {
    }
    /**
     * 拦截处理
     * @param point point 信息
     * @return result
     * @throws Throwable if any
     */
    @Around("pointCut()")
    public Object around(ProceedingJoinPoint point) throws Throwable{
        try {
             //添加 TRACE_ID
        	MDC.put(AppConstant.TRACE_ID, IdUtil.objectId());
            return point.proceed();
        }finally {
            //移除 TRACE_ID
            MDC.remove(AppConstant.TRACE_ID);
        }
    }
}

参考:https://cloud.tencent.com/developer/article/1951233

​ https://cloud.tencent.com/developer/article/1617911

  • 3
    点赞
  • 43
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: Spring Cloud Sleuth是一个分布式跟踪解决方案,它可以帮助开发人员在微服务架构中追踪请求的流程和调用链路。它通过为每个请求生成唯一的跟踪ID和跨服务的调用ID来实现这一目标。这些ID可以用于跟踪请求的流程和调用链路,从而帮助开发人员快速诊断和解决问题。Spring Cloud Sleuth还提供了一些可视化工具,如Zipkin,可以帮助开发人员更好地理解和分析跟踪数据。 ### 回答2: SpringCloud Sleuth是一个基于日志的分布式跟踪方案,它可以用于解决微服务架构下的分布式系统的链路追踪问题。在分布式系统中,一个请求经常会穿越多个服务,从而会形成一条复杂的链路,如果有一台或多台机器对此进行记录,那么将能够轻松地查看和理解一个请求的完整路径。这些信息能够帮助我们更快地定位问题所在,提高系统可靠性和稳定性。 Sleuth使用了Zipkin的架构和数据模型,通过在每个请求的Header中添加Trace Id和Span Id来实现链路追踪。Trace Id表示整个请求链路,Span Id表示每一个服务的一个简单步骤。使用这两个 Id,我们就可以将整个链路追踪下来,使得对请求的监测、记录和分析变得更加容易。 Sleuth结合了Spring Cloud日志管理和Zipkin的功能,能够自动收集各个微服务的请求跟踪信息,并将其发送到Zipkin服务器进行聚合分析,视图展现。通过Sleuth的ChainInvoker,可以实现对所有链路的统一管理。当一条请求跨越多个服务时,Sleuth会为每个服务实例生成唯一的spanId,并将这个spanId沿用到下一个服务实例,从而使得整条链路保留了完整的信息。此外,Sleuth还支持基于日志的采样策略和数据比较高效的存储,保证了高性能的分布式链路追踪。 Sleuth的主要应用场景是微服务架构下的链路跟踪和性能监控。微服务架构中有大量的服务,服务之间的关系错综复杂,因此链路追踪对于排查问题、优化性能非常重要。Sleuth能够方便地实现链路追踪和监测,并帮助我们快速定位问题所在,提高系统的可靠性和稳定性。 ### 回答3: Spring Cloud Sleuth 链路追踪是 Spring Cloud 微服务架构中的一项重要的功能模块。通过 Sleuth 链路追踪,我们可以跟踪整个分布式系统中的请求链路,从而了解每个操作所花费的时间、调用的服务以及调用顺序。在微服务架构中,服务调用会涉及到多个服务之间的协作,使用 Sleuth 链路追踪可以帮助我们很好地理解系统在内部的调用过程。 Sleuth 链路追踪的原理是在每个服务的请求中添加唯一的追踪 ID,通过这个追踪 ID,Sleuth 可以实现将每个请求相关的服务调用串联起来,形成完整的请求链路。追踪 ID 通常被称为 Trace ID,它作为请求的一部分,从前端发起请求的服务开始一直传递到最后一个服务。 通过 Sleuth 链路追踪,我们可以了解每个调用的服务名和 IP 地址,以及请求的耗时情况,在调试分布式系统时非常实用。此外,Sleuth 还支持将链路追踪信息集成到日志系统中,从而更好地协助开发人员进行故障排查。 Sleuth 链路追踪还提供了 Zipkin 集成,Zipkin 是一个开源的分布式链路追踪系统,可以将链路数据可视化显示,并提供了一些分析工具,帮助开发人员更好地理解系统的调用情况。 总之,Spring Cloud Sleuth 链路追踪是一个非常实用的工具,可以帮助我们更好地理解分布式系统中服务调用的情况,有效地解决微服务架构中的复杂度和故障排查的问题。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值