最近在排查问题查询日志的的时候,有个很困扰的问题就是多线程调用的状况下,没办法查询一次调用的全链路调用日志,好比在使用elk查询日志的时候,我想看某个日志的先后调用的完整的日志,若是不作处理的话是不太好查询的,因此我经过查询资料发现确实有不少方式处理这种问题,如下是我我的认为比较好的处理方式,即经过MDC
处理.spring
MDC(Mapped Diagnostic Context,映射调试上下文)是 log4j 、logback及log4j2 提供的一种方便在多线程条件下记录日志的功能。 MDC 能够当作是一个 与当前线程绑定的哈希表,能够往其中添加键值对。MDC 中包含的内容能够 被同一线程中执行的代码所访问。当前线程的子线程会继承其父线程中的 MDC 的内容。当须要记录日志时,只须要从 MDC 中获取所需的信息便可。MDC 的内容则由程序在适当的时候保存进去。对于一个 Web 应用来讲,一般是在请求被处理的最开始保存这些数据
API说明:多线程
一,建立EnvironmentPostProcessor
实现类,配置日志格式化全局环境配置,如:"%1p [%X{X-B3-TraceId:-}]"
app
public class TraceEnvironmentPostProcessor implements EnvironmentPostProcessor { private static final String PROPERTY_SOURCE_NAME = "defaultProperties"; private static final String LEVEL_STR_PARENT = "%1p [%X{X-B3-TraceId:-}]"; private static final String LOG_PATTERN_LEVEL = "logging.pattern.level"; @Override public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) { Map<String, Object> map = new HashMap<>(1); map.put(LOG_PATTERN_LEVEL, LEVEL_STR_PARENT); MutablePropertySources propertySources = environment.getPropertySources(); MapPropertySource target = null; if (propertySources.contains(PROPERTY_SOURCE_NAME)) { PropertySource<?> source = propertySources.get(PROPERTY_SOURCE_NAME); if (source instanceof MapPropertySource) { target = (MapPropertySource) source; for (String key : map.keySet()) { if (!target.containsProperty(key)) { target.getSource().put(key, map.get(key)); } } } } if (target == null) { target = new MapPropertySource(PROPERTY_SOURCE_NAME, map); } if (!propertySources.contains(PROPERTY_SOURCE_NAME)) { propertySources.addLast(target); } } }
二,使用filter拦截每次请求,经过MDC
为当前线程添加惟一traceId
框架
@Component public class TraceFilter implements Filter { @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) throws IOException, ServletException { //String LEGACY_TRACE_ID_NAME = "X-B3-TraceId"; HttpServletRequest request = ((HttpServletRequest) servletRequest); String traceId = request.getParameter(LogConstant.LEGACY_TRACE_ID_NAME); // 若是为空,则表示第一次访问,即上游服务端的请求 if (StringUtils.isEmpty(traceId)) { //使用工具类生成uuid MDC.put(LogConstant.LEGACY_TRACE_ID_NAME, TraceIdUtil.traceId()); } else { MDC.put(LogConstant.LEGACY_TRACE_ID_NAME, traceId); } chain.doFilter(servletRequest, servletResponse); MDC.clear(); } }
三,在META-INF目录下添加spring.factories
文件,添加咱们建立的processor类,在项目启动的时候经过spring boot自动扫描,经过spring调用postProcessEnvironment
发放初始化配置ide
# Environment Post Processor org.springframework.boot.env.EnvironmentPostProcessor=\ com.example.log.trace.processor.TraceEnvironmentPostProcessor
四,使用简单的controller测试日志工具
@Slf4j @RestController public class TraceLogController { @GetMapping(value = "/trace") public Object trace() { log.info("---------trace log 1--------"); log.info("---------trace log 2--------"); log.info("---------trace log 3--------"); log.info("---------trace log 4--------"); return Boolean.TRUE; } }
发现打印的日志已经有咱们添加的惟一的 traceId
,而且屡次打印的都是同一个traceId
,这样咱们在elk中想要查询一次调用的全链路调用日志就很方便了,只要经过traceId
查询就很容易查询全链路的日志oop
2020-08-29 16:48:37.412 INFO [36d47a716d6d4891a264f050ff710d38] 20856 --- [nio-8080-exec-3] c.e.l.t.controller.TraceLogController : ---------trace log 1-------- 2020-08-29 16:52:11.284 INFO [36d47a716d6d4891a264f050ff710d38] 20856 --- [nio-8080-exec-3] c.e.l.t.controller.TraceLogController : ---------trace log 2-------- 2020-08-29 16:53:38.189 INFO [36d47a716d6d4891a264f050ff710d38] 20856 --- [nio-8080-exec-3] c.e.l.t.controller.TraceLogController : ---------trace log 3-------- 2020-08-29 17:23:37.527 INFO [36d47a716d6d4891a264f050ff710d38] 20856 --- [nio-8080-exec-3] c.e.l.t.controller.TraceLogController : ---------trace log 4--------
实现原理确定是log-back(spring boot默认日志框架)经过拼接的方式打印日志,咱们在全局添加了对应的key-value的匹配原则,在经过log打印日志的时候使用mdc
经过key替换咱们在添加的uuid
,所以很简单的实现了全链路日志源码分析
经过debug跟踪log链路,其调用链路仍是很长的(以下图),咱们只须要关心关键的逻辑就能够了,
其中打印日志拼接是经过ch.qos.logback.core.pattern.PatternLayoutBase#writeLoopOnConverters
进行处理的。post
writeLoopOnConverters
经过遍历调用converter的write方法拼接所须要打印的日志,以下流程:测试
DateConverter -----时间---- 2020-08-29 17:45:22.318 LiteralConverter -----空格---- 2020-08-29 17:45:22.318 有空格 CompositeConverter ----自定义---- 2020-08-29 17:45:22.318 INFO [1a3801d149bb4b5e99698ab53481e263] ......剩下的还有线程号,执行类等咱们只关心自定义的规则处理.....
再看看CompositeConverter
具体处理逻辑,发现其中还存在一层遍历处理以下图,
拼接了咱们自定义的[traceId]
的日志,其中核心的处理类就是MDCConverter
的处理,替换咱们定义的key
MDCConverter
核心处理方法以下,经过key:X-B3-TraceId
从 mdcPropertyMap
中获取traceId
,自此证明了咱们的猜测,经过key拿到value进行日志拼接,全链路日志原理简单分析到此为止!
@Override public String convert(ILoggingEvent event) { //X-B3-TraceId -> 1a3801d149bb4b5e99698ab53481e263 Map<String, String> mdcPropertyMap = event.getMDCPropertyMap(); if (mdcPropertyMap == null) { return defaultValue; } if (key == null) { return outputMDCForAllKeys(mdcPropertyMap); } else { //X-B3-TraceId String value = mdcPropertyMap.get(key); if (value != null) { return value; } else { return defaultValue; } } }