详解Java中日志跟踪的简单实现

2022-09-03 15:32:37 66 0
魁首哥

目录

  • 一、前言
  • 二、MDC介绍
  • 三、实现方案
    • 1、基本思路
    • 2、实现(以SpringBoot为例)
  • 四、总结

    一、前言

    在编码过程中,常常需要写打印日志语句,我们期望的是同一个业务的日志都在一块,在出问题的时候好根据日志来排查问题。而现实是在应用运行中,日志的输出常常来自不同线程,甚至是在不同微服务中,各种日志记录往往彼此穿插,很难串起来。所以往往在日志中手动增加一些关键字,来对接口的调用链路来进行跟踪。但这种手动增加关键字或唯一标识的做法在微服务场景下,很难在上下游应用的开发人员的编码风格形成统一的规范,并且手动编写也很难称得上优雅。

    详解Java中日志跟踪的简单实现

    二、MDC介绍

    MDC​(Mapped Diagnostic Context,映射调试上下文)是log4j​、logback及log4j2​提供的一种方便在多线程条件下记录日志的功能。MDC​可以看成是一个与当前线程绑定的哈希表,MDC中包含的内容可以被同一线程中执行的代码所访问。

    MDC中的键值对是可以直接被日志框架所使用(即“打印”)的,只需要配置相应日志pattern。例如pattern如下:

    %d{HH:mm:ss.SSS} [%thread] [%X{TraceId}] %-5level %logger{50} - %msg%n

    代码如下:

    public class MDCTest {
    
        private static final Logger log = LoggerFactory.getLogger(MDCTest.class);
    
        @Test
        void test() {
            MDC.put("TraceId", "123456789");
            log.info("hello {}", "world");
        }
    }

    此时控制台将输出:

    21:16:04.342 [main] [123456789] INFO com.nk.MDCTest - hello world

    三、实现方案

    1、基本思路

    修改日志pattern,并在业务开始的时候将trace id放入到MDC,在业务结束时去除MDC的trace id。这样的好处便是代码简洁,不需要手动写trace id,日志风格也能保持统一。

    业务开始的时机一般是应用收到HTTP请求,所以可以用Filter或SpringMVC的Interceptor来对MDC中tracejs id进行初始化和清除。在Dubbo调用的时候也可以通过类似功能的Filter来对MDC中trace id进行操作,从而达到trace id传递的作用。

    2、实现(以SpringBoot为例)

    2.1 修改log pattern

    在SpringBoot中,直接修改application.properties即可:

    logging.pattern.console=%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [%X{TraceId}] %-5level %logger{50} - %msg%n

    重点在于%X{TraceId}​,其中TraceId​需要作为key出现在MDC里。

    2.2 业务开始

    TraceId工具类,封装MDC关于trace id的基础操作:

    public final class TraceIdUtil {
    
        private static final String TRACE_ID_KEY = "TraceId";
    
        private TraceIdUtil() {
        }
    
        public static void putIfAbsent() {
            if (StrUtil.isBlank(get())) {
                put(UUID.randomUUID().toString());
            }
        }
    
        public static void remove() {
            if (get() != null) {
                MDC.remove(MDhWedNOLTRACE_ID_KEY);
     MDhWedNOL       }
        }
    
        public static String get() {
            return MDC.get(TRACE_ID_KEY);
        }
    
        public static void put(String traceId) {
            MDC.put(TRACE_ID_KEY, traceId);
        }
    }

    Filter​方式和Interceptor二选其一既可,其基本思想是一样的。

    Filter方式

    @Component
    public class LogFilter implements Filter {
    
        @Override
        public void doFilter(ServletRequest servletRequest, 
                             ServletResponse servletResponse, 
                             FilterChain filterChain) throws IOException, ServletException {
            TraceIdUtil.putIfAbsent();//生成trace id放入MDC中
            try {
                filterChain.doFilter(servletRequest, servletResponse);
            } finally {
                TraceIdUtil.remove();//移除MDC中的trace id
            }
        }
    }

    Interceptor

    @Configuration
    public class LogInterceptor implements WebMvcConfigurer {
    
        @Override
        public void addInterceptors(InterceptorRegistry registry) {
            registry.addInterceptor(new AsyncHandlerInterceptor() {
                @Override
                public boolean preHandle(HttpServletRequest request, 
                                         HttpServletResponse response, 
                                         Object handler) throws Exception {
                    TraceIdUtil.putIfAbsent();//生成trace id放入MDC中
                    return AsyncHandlerInterceptor.super.preHandle(request, response, handler);
                }
                
                @Override
                public void afterCompletion(HttpServletRequest request, 
                                            HttpServletResponse response, 
                                            Object handler, Exception ex) throws Exception {
                    TraceIdUtil.remove();//移除MDC中的trace id
                    AsyncHandlerInterceptor.super.afterCompletion(request, response, handler, ex);
                }
            });
            WebMvcConfigurer.super.addInterceptors(registry);
        }
    }

    2.3 业务中使用

    正常使用logger,无需关心trace id。例如:

    @RestController
    @RequestMapping("/api/user")
    @Slf4j
    public class UserController {
    
        @Autowired
        private UserService userService;
    
        @GetMapping("/{userId}")
        public UserDto queryUser(@PathVariable Long userId) {
            log.info("query user by id:{}", userId);
            UserDto user = userService.query(userId);
            log.info("query user result:{}", user);
            return user;
        }
    }

    请求该接口将输出如下的日志样式:

    2022-04-05 09:40:17.638 [http-nio-8080-exec-1] [a02b13d81c224e49956afd4efbb85ca8] INFO com.nk.we恰卡编程网bapp.controller.UserController - ready to query user by id:1
    2022-04-05 09:40:17.670 [http-nio-8080-exec-1] [a02b13d81c224e49956afd4efbb85ca8] INFO com.nk.webapp.controller.UserController - query result:UserDto(userId=1, username=zhang3, age=23, email=abc@example.com)

    四、总结

    日志链路的跟踪核心是使用MDC作为trace id载体,在业务开始阶段一般通过拦截器就生成trace id并放入到MDC中,并根据MDC的相关特性将trace id投射到日志文本中,从而实现在同一个业务调用链路中的日志具有唯一标识。

    以上就是详解Java中日志跟踪的简单实现的详细内容,更多关于Java 日志跟踪的资料请关注我们其它相关文章!

    收藏
    分享
    海报
    0 条评论
    66
    上一篇:一文简单了解C# 中的DataSet类 下一篇:Java统计代码的执行时间的N种方法

    本站已关闭游客评论,请登录或者注册后再评论吧~

    忘记密码?

    图形验证码