利用Spring boot+LogBack+MDC实现链路追踪

目录
  • MDC介绍
  • API说明
  • MDC使用
    • 1.拦截器
    • 2.工具类
  • MDC 存在的问题
  • 子线程日志打印丢失traceId
  • HTTP调用丢失traceId
    • 1.接口调用方
    • 2.第三方服务需要添加拦截器

MDC介绍

MDC(Mapped Diagnostic Context,映射调试上下文)是 log4j 、logback及log4j2 提供的一种方便在多线程条件下记录日志的功能。MDC 可以看成是一个与当前线程绑定的哈希表,可以往其中添加键值对。MDC 中包含的内容可以被同一线程中执行的代码所访问。当前线程的子线程会继承其父线程中的 MDC 的内容。当需要记录日志时,只需要从 MDC 中获取所需的信息即可。

API说明

  • clear() => 移除所有MDC
  • get (String key) => 获取当前线程MDC中指定key的值
  • getContext() => 获取当前线程MDC的MDC
  • put(String key, Object o) => 往当前线程的MDC中存入指定的键值对
  • remove(String key) => 删除当前线程MDC中指定的键值对 。

MDC使用

1.拦截器

@Component
public class LogInterceptor implements HandlerInterceptor
{
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //如果有上层调用就用上层的ID
        String traceId = request.getHeader(TraceIdUtil.TRACE_ID);
        if (StringUtil.isEmpty(traceId))
        {
            TraceIdUtil.setTraceId(TraceIdUtil.generateTraceId());
        }
        else
        {
            TraceIdUtil.setTraceId(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 {
        //调用结束后删除
        TraceIdUtil.remove();
    }
}

2.工具类

public class TraceIdUtil
{
  public static final String TRACE_ID = "requestId";
  public static String getTraceId()
  {
     String traceId =(String) MDC.get(TRACE_ID);
     return traceId == null ? "" : traceId;
  }
  public static void setTraceId(String traceId)
  {
      MDC.put(TRACE_ID,traceId);
  }
  public static void remove()
  {
      MDC.remove(TRACE_ID);
  }
  public static void clear()
  {
      MDC.clear();
  }
  public static String generateTraceId() {
     return UUID.randomUUID().toString().replace("-", "");
  }
  • 日志文件配置
<property name="LOG_PATTERN" value="%date{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level [%X{requestId}] %logger{36} - %msg%n" />

重点是%X{requestId},requestId和MDC中的键名称保持一致。

MDC 存在的问题

至此基本的功能已经实现,但是存在一下几个问题

  • 多线程情况下,子线程中打印日志会丢失traceId.
  • HTTP跨服务之间的调用丢失traceId.

子线程日志打印丢失traceId

问题重现:

  @LogAnnotation(model="用户管理",func="查询用户信息",desc="根据用户名称")
    @GetMapping("getUserByName")
    public Result getUserByName(@RequestParam String name)
    {
        //主线程日志
        logger.info("getUserByName paramter name:"+name);
        for(int i=0;i<5;i++)
        {
           //子线程日志
            threadPoolTaskExecutor.execute(()->{
                logger.info("child thread:{}",name);
                userService.getUserByName(name);
            });
        }
        return Result.success();
    }

运行结果:

2022-03-13 12:45:44.156 [http-nio-8089-exec-1] INFO  [ec05a600ed1a4556934a3afa4883766a] c.s.fw.controller.UserController - getUserByName paramter name:1
2022-03-13 12:45:44.173 [Pool-A1] INFO  [] c.s.fw.controller.UserController - child thread:1

从运行的结果来看,子线程打印日志,日志中的traceId信息已经丢失。

解决方案:

子线程在打印日志的过程中traceId将丢失,解决方案为重写线程池(对于直接new Thread 创建线程的情况不考略),实际开发中也需要禁止这种情况。

public class ThreadPoolExecutorMdcWrapper extends ThreadPoolTaskExecutor
{
    private static final long serialVersionUID = 3940722618853093830L;
    @Override
    public void execute(Runnable task)
    {
        super.execute(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap()));
    }
    @Override
    public <T> Future<T> submit(Callable<T> task)
    {
        return super
                .submit(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap()));
    }
    @Override
    public Future<?> submit(Runnable task)
    {
        return super
                .submit(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap()));
    }
}

因为Spring Boot ThreadPoolTaskExecutor 已经对ThreadPoolExecutor进行封装,只需要继承ThreadPoolTaskExecutor重写相关的执行方法即可。

public class ThreadMdcUtil
{
    public static void setTraceIdIfAbsent() {
        if (MDC.get(TraceIdUtil.TRACE_ID) == null)
        {
            MDC.put(TraceIdUtil.TRACE_ID, TraceIdUtil.generateTraceId());
        }
    }
    public static <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 static Runnable wrap(final Runnable runnable, final Map<String, String> context) {
        return () -> {
            if (context == null) {
                MDC.clear();
            } else {
                MDC.setContextMap(context);
            }
            //设置traceId
            setTraceIdIfAbsent();
            try {
                runnable.run();
            } finally {
                MDC.clear();
            }
        };
    }
}

代码说明:

  • 判断当前线程对应MDC的Map是否存在,如果存在则设置
  • 设置MDC中的traceId值,不存在则新生成,如果是子线程,MDC中traceId不为null
  • 执行run方法

线程池配置

@Configuration
public class ThreadPoolTaskExecutorConfig
{
    //最大可用的CPU核数
    public static final int PROCESSORS = Runtime.getRuntime().availableProcessors();
    @Bean
    public ThreadPoolExecutorMdcWrapper getExecutor()
    {
        ThreadPoolExecutorMdcWrapper executor =new ThreadPoolExecutorMdcWrapper();
        executor.setCorePoolSize(PROCESSORS *2);
        executor.setMaxPoolSize(PROCESSORS * 4);
        executor.setQueueCapacity(50);
        executor.setKeepAliveSeconds(60);
        executor.setThreadNamePrefix("Task-A");
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());
        executor.initialize();
        return executor;
    }
}

重新运行结果发现子线程能够正常获取traceid信息进行跟踪。

2022-03-13 13:19:30.688 [Task-A1] INFO  [482929425cbc4476a4e7168615af7890] c.s.fw.controller.UserController - child thread:1
2022-03-13 13:19:31.003 [Task-A1] INFO  [482929425cbc4476a4e7168615af7890] c.s.fw.service.impl.UserServiceImpl - name:1

HTTP调用丢失traceId

HTTP调用第三方服务接口时traceId丢失,需要在发送请求时在Request Header中添加traceId,在被调用方添加拦截器获取header中的traceId添加到MDC中。

HTTP调用有多种方式,比较常见的有HttpClient、OKHttp、RestTemplate,以RestTemplate调用为例。

1.接口调用方

public class RestTemplateTraceIdInterceptor implements
        ClientHttpRequestInterceptor
{
    @Override
    public ClientHttpResponse intercept(HttpRequest request, byte[] body,
            ClientHttpRequestExecution clientHttpRequestExecution) throws IOException
    {
        String traceId=MDC.get("requestId");
        if(traceId!=null)
        {
            request.getHeaders().set("requestId", traceId);
        }
        else
        {
            request.getHeaders().set("requestId", UUID.randomUUID().toString().replace("-", ""));
        }
        return clientHttpRequestExecution.execute(request, body);
    }
}

RestTemplate添加拦截器即可。

restTemplate.setInterceptors(Arrays.asList(new RestTemplateTraceIdInterceptor()));
复制代码

2.第三方服务需要添加拦截器

@Component
public class LogInterceptor implements HandlerInterceptor
{
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //如果有上层调用就用上层的ID
        String traceId = request.getHeader(TraceIdUtil.TRACE_ID);
        if (StringUtil.isEmpty(traceId))
        {
            TraceIdUtil.setTraceId(TraceIdUtil.generateTraceId());
        }
        else
        {
            TraceIdUtil.setTraceId(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 {
        //调用结束后删除
        TraceIdUtil.remove();
    }
}

其他HttpClient、OKHttp的实现方式与RestTemplate基本相同,这里就不一一列举。 Spring boot +logback+MDC实现全链路跟踪内容已经讲完了

到此这篇关于利用Spring boot+LogBack+MDC实现链路追踪的文章就介绍到这了,更多相关Spring boot 链路追踪内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • SpringBoot+Logback实现一个简单的链路追踪功能

    最近线上排查问题时候,发现请求太多导致日志错综复杂,没办法把用户在一次或多次请求的日志关联在一起,所以就利用SpringBoot+Logback手写了一个简单的链路追踪,下面详细介绍下. 一.实现原理 Spring Boot默认使用LogBack日志系统,并且已经引入了相关的jar包,所以我们无需任何配置便可以使用LogBack打印日志. MDC(Mapped Diagnostic Context,映射调试上下文)是log4j和logback提供的一种方便在多线程条件下记录日志的功能. 实现思路

  • 利用Spring boot+LogBack+MDC实现链路追踪

    目录 MDC介绍 API说明 MDC使用 1.拦截器 2.工具类 MDC 存在的问题 子线程日志打印丢失traceId HTTP调用丢失traceId 1.接口调用方 2.第三方服务需要添加拦截器 MDC介绍 MDC(Mapped Diagnostic Context,映射调试上下文)是 log4j .logback及log4j2 提供的一种方便在多线程条件下记录日志的功能.MDC 可以看成是一个与当前线程绑定的哈希表,可以往其中添加键值对.MDC 中包含的内容可以被同一线程中执行的代码所访问.

  • SpringBoot 项目添加 MDC 日志链路追踪的执行流程

    目录 1. 线程池配置 2. 拦截器配置 3. 日志文件配置 4. 使用方法示例 4.1. 异步使用 4.2. 定时任务 日志链路追踪的意思就是将一个标志跨线程进行传递,在一般的小项目中也就是在你新起一个线程的时候,或者使用线程池执行任务的时候会用到,比如追踪一个用户请求的完整执行流程. 这里用到MDC和ThreadLocal,分别由下面的包提供: java.lang.ThreadLocal org.slf4j.MDC 直接上代码: 1. 线程池配置 如果你直接通过手动新建线程来执行异步任务,想

  • Spring Boot Logback配置日志过程解析

    这篇文章主要介绍了Spring Boot Logback配置日志过程解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 出于性能等原因,Logback 目前是springboot应用日志的标配: 当然有时候在生产环境中也会考虑和三方中间件采用统一处理方式. 配置时考虑点 支持日志路径,日志level等配置 日志控制配置通过application.yml下发 按天生成日志,当天的日志>50MB回滚 最多保存10天日志 生成的日志中Pattern自

  • 利用Spring Boot创建docker image的完整步骤

    前言 在很久很久以前,我们是怎么创建Spring Boot的docker image呢?最最通用的办法就是将Spring boot的应用程序打包成一个fat jar,然后写一个docker file,将这个fat jar制作成为一个docker image然后运行. 今天我们来体验一下Spring Boot 2.3.3 带来的快速创建docker image的功能. 传统做法和它的缺点 现在我们创建一个非常简单的Spring Boot程序: @SpringBootApplication @Res

  • 利用Spring Boot和JPA创建GraphQL API

    目录 一.生成项目 1. 添加依赖项 二.Schema 三.Entity 和 Repository 四.Queries & Exceptions 1. 查询 2. Mutator 3. Exceptions 前言: GraphQL既是API查询语言,也是使用当前数据执行这些查询的运行时.GraphQL让客户能够准确地要求他们所需要的东西,仅此而已,使API随着时间的推移更容易发展,并通过提供API中数据的清晰易懂的描述,支持强大的开发工具. 在本文中,我们将创建一个简单的机场位置应用程序. 一.

  • 利用spring boot+WebSocket实现后台主动消息推送功能

    目录 前言: 有个需求: WebSocket 主要能实现的场景: 总结 前言: 使用此webscoket务必确保生产环境能兼容/支持!使用此webscoket务必确保生产环境能兼容/支持!使用此webscoket务必确保生产环境能兼容/支持!主要是tomcat的兼容与支持. 有个需求: APP用户产生某个操作,需要让后台管理系统部分人员感知(表现为一个页面消息). 最早版本是后台管理系统轮训,每隔一段时间轮训一次,由于消息重要,每隔几秒就查一次.这样做明显很不雅!会消耗大量资源,并且大部分请求是

  • 如何利用Spring Boot 监控 SQL 运行情况

    目录 前言 1. 准备工作 2. 引入 Druid 3. 测试 4. 去广告 前言 今天想和大家聊一聊 Druid 中的监控功能. Druid 数据库连接池相信很多小伙伴都用过,个人感觉 Druid 是阿里比较成功的开源项目了,不像 Fastjson 那么多槽点,Druid 各方面一直都比较出色,功能齐全,使用也方便,基本的用法就不说了,今天我们来看看 Druid 中的监控功能. 1. 准备工作 首先我们来创建一个 Spring Boot 工程,引入 MyBatis 等,如下: 选一下 MyBa

  • 利用Spring boot如何创建简单的web交互应用

    关于页面渲染 其实在工作中,一直都是前后端分离,也就是说,我的工作从来都是提供接口,而不写 html css js 之类的,所以在这方面也没有经验. 这里为了给大家介绍下模板引擎,我将会写个非常非常简单的页面,如果不好看,请见谅~ Spring Boot 官方推荐的模板引擎是 Thymeleaf ,点击可以进入其官网了解详情. 本章目标 让 Spring Boot 应用可以访问到静态资源文件 创建用户登录表单,并对用户名.密码进行校验 校验失败,将返回登录页,并展示错误信息 校验成功,重定向到苹

  • 利用Spring Boot操作MongoDB的方法教程

    MongoDB MongoDB作为一种NoSQL数据库产品,其实已经非常著名了.去年,由于MongoDB安全认证的薄弱,上万家公司中招.虽然是一则负面新闻,但是也从侧面说明了MongoDB的流行程度. 下图是DB-Engines统计的2017年5月全球数据库引擎使用排名.从图中可以看出,mongoDB位列总榜第五,非关系数据库第一,非常靠前的排名. 我个人对mongoDB并不是非常熟悉,但是经过一段时间的了解,对mongoDB的特性还是有了一些简单的理解,这里记录一二. 首先,mongoDB作为

  • 利用Spring Boot如何开发REST服务详解

    REST服务介绍 RESTful service是一种架构模式,近几年比较流行了,它的轻量级web服务,发挥HTTP协议的原生的GET,PUT,POST,DELETE. REST模式的Web服务与复杂的SOAP和XML-RPC对比来讲明显的更加简洁,越来越多的web服务开始采用REST风格设计和实现.例如,Amazon.com提供接近REST风格的Web服务进行图书查找:雅虎提供的Web服务也是REST风格的.REST 并非始终是正确的选择. 它作为一种设计 Web 服务的方法而变得流行,这种方

随机推荐