01、前言
在现网出现故障时,我们经常需要获取一次请求流程里的所有日志进行定位。如果请求只在一个线程里处理,则我们可以通过线程ID来过滤日志,但如果请求包含异步线程的处理,那么光靠线程ID就显得捉襟见肘了。
华为IoT平台,提供了接收设备上报数据的能力, 当数据到达平台后,平台会进行一些复杂的业务逻辑处理,如数据存储,规则引擎,数据推送,命令下发等等。由于这个逻辑之间没有强耦合的关系,所以通常是异步处理。如何将一次数据上报请求中包含的所有业务日志快速过滤出来,就是本文要介绍的。
02、正文
SLF4J日志框架提供了一个MDC(Mapped Diagnostic Contexts)工具类,谷歌翻译为映射的诊断上下文,从字面上很难理解,我们可以先实战一把。
publicclassMain{
privatestaticfinalStringKEY="requestId";
privatestaticfinalLoggerlogger=LoggerFactory.getLogger(Main.class);
publicstaticvoidmain(String[]args){
//入口传入请求ID
MDC.put(KEY,UUID.randomUUID().toString());
//打印日志
logger.debug("loginmainthread1");
logger.debug("loginmainthread2");
logger.debug("loginmainthread3");
//出口移除请求ID
MDC.remove(KEY);
}
}
我们在main函数的入口调用MDC.put()方法传入请求ID,在出口调用MDC.remove()方法移除请求ID。配置好log4j2.xml文件后,运行main函数,可以在控制台看到以下日志输出:
2018-02-1713:19:52.606{requestId=f97ea0fb-2a43-40f4-a3e8-711f776857d0}[main]DEBUGcn.wudashan.Main-loginmainthread1
2018-02-1713:19:52.609{requestId=f97ea0fb-2a43-40f4-a3e8-711f776857d0}[main]DEBUGcn.wudashan.Main-loginmainthread2
2018-02-1713:19:52.609{requestId=f97ea0fb-2a43-40f4-a3e8-711f776857d0}[main]DEBUGcn.wudashan.Main-loginmainthread3
从日志中可以明显地看到花括号中包含了(映射的)*请求ID(requestId),这其实就是我们定位*(诊断)*问题的关键字*(上下文)。有了MDC工具,只要在接口或切面植入put()和remove()代码,在现网定位问题时,我们就可以通过grep requestId=xxx *.log快速的过滤出某次请求的所有日志。
03、进阶
然而,MDC工具真的有我们所想的这么方便吗?回到我们开头,一次请求可能涉及多线程异步处理,那么在多线程异步的场景下,它是否还能正常运作呢?Talk is cheap, show me the code。
publicclassMain{
privatestaticfinalStringKEY="requestId";
privatestaticfinalLoggerlogger=LoggerFactory.getLogger(Main.class);
publicstaticvoidmain(String[]args){
//入口传入请求ID
MDC.put(KEY,UUID.randomUUID().toString());
//主线程打印日志
logger.debug("loginmainthread");
//异步线程打印日志
newThread(newRunnable(){
@Override
publicvoidrun(){
logger.debug("loginotherthread");
}
}).start();
//出口移除请求ID
MDC.remove(KEY);
}
}
代码里我们新起了一个异步线程,并在匿名对象Runnable的run()方法打印日志。运行main函数,可以在控制台看到以下日志输出:
2018-02-1714:05:43.487{requestId=e6099c85-72be-4986-8a28-de6bb2e52b01}[main]DEBUGcn.wudashan.Main-loginmainthread
2018-02-1714:05:43.490{}[Thread-1]DEBUGcn.wudashan.Main-loginotherthread
不幸的是,请求ID在异步线程里不打印了。这是怎么回事呢?要解决这个问题,我们就得知道MDC的实现原理。由于篇幅有限,这里就暂不详细介绍,MDC之所以在异步线程中不生效是因为底层采用ThreadLocal作为数据结构,我们调用MDC.put()方法传入的请求ID只在当前线程有效。感兴趣的小伙伴可以自己深入一下代码细节。
知道了原理那么解决这个问题就轻而易举了,我们可以使用装饰器模式,新写一个MDCRunnable类对Runnable接口进行一层装饰。在创建MDCRunnable类时保存当前线程的MDC值,在执行run()方法时再将保存的MDC值拷贝到异步线程中去。代码实现如下:
publicclassMDCRunnableimplementsRunnable{
privatefinalRunnablerunnable;
privatefinalMapmap;
publicMDCRunnable(Runnablerunnable){
this.runnable=runnable;
//保存当前线程的MDC值
this.map=MDC.getCopyOfContextMap();
}
@Override
publicvoidrun(){
//传入已保存的MDC值
for(Map.Entryentry:map.entrySet()){
MDC.put(entry.getKey(),entry.getValue());
}
//装饰器模式,执行run方法
runnable.run();
//移除已保存的MDC值
for(Map.Entryentry:map.entrySet()){
MDC.remove(entry.getKey());
}
}
}
接着,我们需要对main函数里创建的Runnable实现类进行装饰:
publicclassMain{
privatestaticfinalStringKEY="requestId";
privatestaticfinalLoggerlogger=LoggerFactory.getLogger(Main.class);
privatestaticfinalExecutorServiceEXECUTOR=Executors.newSingleThreadExecutor();
publicstaticvoidmain(String[]args){
//入口传入请求ID
MDC.put(KEY,UUID.randomUUID().toString());
//主线程打印日志
logger.debug("loginmainthread");
//异步线程打印日志,用MDCRunnable装饰Runnable
newThread(newMDCRunnable(newRunnable(){
@Override
publicvoidrun(){
logger.debug("loginotherthread");
}
})).start();
//异步线程池打印日志,用MDCRunnable装饰Runnable
EXECUTOR.execute(newMDCRunnable(newRunnable(){
@Override
publicvoidrun(){
logger.debug("loginotherthreadpool");
}
}));
EXECUTOR.shutdown();
//出口移除请求ID
MDC.remove(KEY);
}
}
执行main函数,将会输出以下日志:
2018-03-042305.343{requestId=5ee2a117-e090-41d8-977b-cef5dea09d34}[main]DEBUGcn.wudashan.Main-loginmainthread
2018-03-042305.346{requestId=5ee2a117-e090-41d8-977b-cef5dea09d34}[Thread-1]DEBUGcn.wudashan.Main-loginotherthread
2018-03-042305.347{requestId=5ee2a117-e090-41d8-977b-cef5dea09d34}[pool-2-thread-1]DEBUGcn.wudashan.Main-loginotherthreadpool
Congratulations!经过我们的努力,最终在异步线程和线程池中都有requestId打印了!
04、总结
本文讲述了如何使用MDC工具来快速过滤一次请求的所有日志,并通过装饰器模式使得MDC工具在异步线程里也能生效。有了MDC,再通过AOP技术对所有的切面植入requestId,就可以将整个系统的任意流程的日志过滤出来。使用MDC工具,在开发自测阶段,可以极大地节省定位问题的时间,提升开发效率;在运维维护阶段,可以快速地收集相关日志信息,加快分析速度。
审核编辑:刘清
-
AOP
+关注
关注
0文章
41浏览量
11481
原文标题:Spring Boot 如何快速过滤出一次请求的所有日志?
文章出处:【微信号:芋道源码,微信公众号:芋道源码】欢迎添加关注!文章转载请注明出处。
发布评论请先 登录
Spring Boot如何实现异步任务
启动Spring Boot项目应用的三种方法
java 日志框架Spring Boot分析
Spring Boot从零入门1 详述
Spring Boot特有的实践
SpringBoot如何快速过滤出一次请求的所有日志?
怎样使用Kiuwan保护Spring Boot应用程序呢?
Spring Boot如何实现日志链路追踪
Spring Boot的日志框架使用
Spring Boot Actuator快速入门
Spring Boot启动 Eureka流程
Spring Boot的启动原理

请问Spring Boot是如何快速过滤出一次请求所有日志的?
评论