基于SLF4J MDC机制实现日志的链路追踪

https://my.oschina.net/xiaolyuh/blog/1823392

https://mp.weixin.qq.com/s/0pfm4_hpOats1CqdXDFJdg

问题描述

最近经常做线上问题的排查,而排查问题用得最多的方式是查看日志,但是在现有系统中,各种无关日志穿行其中,导致我没办法快速的找出用户在一次请求中所有的日志。

问题分析

我们没办法快速定位用户在一次请求中对应的所有日志,或者说是定位某个用户操作的所有日志,那是因为我们在输出的日志的时候没把请求的唯一标示或者说是用户身份标示输出到我们的日志中,导致我们没办法根据一个请求或者用户身份标示来做日志的过滤。所以我们在记录日志的是后把请求的唯一标示(sessionId)或者身份标示(userId) 记录到日志中这个问题就可以得到很好的解决了。

解决方案

  1. 在每次请求的时候,获取到请求的sessionId(或者自己生成一个伪sessionId),并在每次输出log的时候将这个sessionId输出到日志中。这个方式实现简单,代码侵入型强,每次输出都会多输出一个sessionId参数,工作量大,但是可控粒度高。

  2. 我们使用Logback的MDC机制,日志模板中加入sessionId格式。在日志输出格式中指定输出sessionId。如:

%d{yyyy-MM-dd HH:mm:ss.SSS}  [%X{sessionId}] -%5p ${PID:-} [%15.15t] %-40.40logger{39} : %m%n

这种方式工作量小,代码侵入小,易扩展,但是可控粒度低。

方案说明

第一种方案很简单,也很容易实现,就是在输出日志的时候多输出一个参数,如:

我们这里主要说一下第二种方式的实现。

实现思路,这里以Spring MVC为例:

  1. 新建一个日志拦截器,在拦截所有请求,在处理请求前将sessionId放到MDC中,在处理完请求后清除MDC的内容。这里就解决了80%的问题。

  2. 在原来版本中新起线程时MDC会自动将父线程的MDC内容复制给子线程,因为MDC内部使用的是InheritableThreadLocal,但是因为性能问题在最新的版本中被取消了,所以子线程不会自动获取到父线程的MDC内容。官方建议我们在父线程新建子线程之前调用MDC.getCopyOfContextMap()方法将MDC内容取出来传给子线程,子线程在执行操作前先调用MDC.setContextMap()方法将父线程的MDC内容设置到子线程。

  3. 设置日志输出格式

拦截器 LogInterceptor

注册拦截器

扩展ThreadPoolTaskExecutor线程池

扩展ThreadPoolTaskExecutor线程池的主要目的是实现将父线程的MDC内容复制给子线程。

扩展Hystrix

扩展Hystrix线程池隔离支持日志链路跟踪

配置Hystrix

Logback配置

测试类

## 日志输出示例

源码

https://github.com/wyh-spring-ecosystem-student/spring-boot-student/tree/releases

spring-boot-student-log 工程

Last updated

Was this helpful?