SpringBoo利用 MDC 机制过滤出单次请求相关的日志

 

🏷️个人主页:牵着猫散步的鼠鼠 

🏷️系列专栏:Java全栈-专栏

🏷️个人学习笔记,若有缺误,欢迎评论区指正

目录

1.前言

2.MDC 是什么

3.代码实战

4.总结


1.前言

在服务出现故障时,我们经常需要获取一次请求流程里的所有日志进行定位 。如果请求只在一个线程里处理,则我们可以通过线程ID来过滤日志 ,但如果请求包含异步线程的处理,那么光靠线程ID就显得捉襟见肘了。

比如华为IoT平台,提供了接收设备上报数据的能力, 当数据到达平台后,平台会进行一些复杂的业务逻辑处理,如数据存储,规则引擎,数据推送,命令下发等等。由于这个逻辑之间没有强耦合的关系,所以通常是异步处理。

如何将一次数据上报请求中包含的所有业务日志快速过滤出来,就是本文要介绍的。

2.MDC 是什么

SLF4J MDC(Mapped Diagnostic Context)是一个用于日志记录的实用工具,它提供了线程级别的日志上下文信息管理功能。SLF4J(Simple Logging Facade for Java)本身是一个抽象层,允许我们在使用不同的日志框架(如log4j, logback等),而MDC是SLF4J的一部分,主要在logback和log4j中使用。

MDC允许我们在一个线程的执行上下文中设置和获取键值对。这些键值对在日志输出中可以以占位符的形式被引用,从而在日志信息中输出这些上下文信息。这特别有用,例如,在Web应用中跟踪用户请求,或者在分布式系统中跟踪跨服务的事务。

在设置和清除MDC数据时,通常使用put和remove方法,如:

MDC.put("requestId", "12345");
// ... logging statements ...
MDC.remove("requestId");

这样,日志框架在记录日志时,就可以在日志格式中包含这个requestId的值。

3.代码实战

通过上门的介绍你可能还是有点难理解,我们可以先实战一把。

public class Main {private static final String KEY = "requestId";private static final Logger logger = LoggerFactory.getLogger(Main.class);public static void main(String[] args) {// 入口传入请求IDMDC.put(KEY, UUID.randomUUID().toString());// 打印日志logger.debug("log in main thread 1");logger.debug("log in main thread 2");logger.debug("log in main thread 3");// 出口移除请求IDMDC.remove(KEY);}}

我们在main函数的入口调用MDC.put()方法传入请求ID,在出口调用MDC.remove()方法移除请求ID。配置好log4j2.xml 文件后,运行main函数,可以在控制台看到以下日志输出:

2018-02-17 13:19:52.606 {requestId=f97ea0fb-2a43-40f4-a3e8-711f776857d0} [main] DEBUG cn.wudashan.Main - log in main thread 1
2018-02-17 13:19:52.609 {requestId=f97ea0fb-2a43-40f4-a3e8-711f776857d0} [main] DEBUG cn.wudashan.Main - log in main thread 2
2018-02-17 13:19:52.609 {requestId=f97ea0fb-2a43-40f4-a3e8-711f776857d0} [main] DEBUG cn.wudashan.Main - log in main thread 3

从日志中可以明显地看到花括号中包含了 (映射的) 请求ID(requestId),这其实就是我们定位 (诊断) 问题的关键字 (上下文) 。

有了MDC工具,只要在接口或切面植入put()和remove()代码,在为服务定位问题时,我们就可以通过grep requestId=xxx *.log快速的过滤出某次请求的所有日志。

然而,MDC工具真的有我们所想的这么方便吗?

回到我们开头,一次请求可能涉及多线程异步处理,那么在多线程异步的场景下,它是否还能正常运作呢?

public class Main {private static final String KEY = "requestId";private static final Logger logger = LoggerFactory.getLogger(Main.class);public static void main(String[] args) {// 入口传入请求IDMDC.put(KEY, UUID.randomUUID().toString());// 主线程打印<font style="color: #1e6bb8;word-wrap: break-word;font-weight: bold;border-bottom: 1px solid">日志</font>logger.debug("log in main thread");// 异步线程打印<font style="color: #1e6bb8;word-wrap: break-word;font-weight: bold;border-bottom: 1px solid">日志</font>new Thread(new Runnable() {@Overridepublic void run() {logger.debug("log in other thread");}}).start();// 出口移除请求IDMDC.remove(KEY);}}

代码里我们新起了一个异步线程,并在匿名对象Runnable的run()方法打印日志。运行main函数,可以在控制台看到以下日志输出:

2018-02-17 14:05:43.487 {requestId=e6099c85-72be-4986-8a28-de6bb2e52b01} [main] DEBUG cn.wudashan.Main - log in main thread
2018-02-17 14:05:43.490 {} [Thread-1] DEBUG cn.wudashan.Main - log in other thread

不幸的是,请求ID在异步线程里不打印了。

这是怎么回事呢?要解决这个问题,我们就得知道MDC的实现原理。

由于篇幅有限,这里就暂不详细介绍,MDC之所以在异步线程中不生效是因为底层采用ThreadLocal 作为数据结构,我们调用MDC.put()方法传入的请求ID只在当前线程有效。

知道了原理那么解决这个问题就轻而易举了,我们可以使用装饰器模式 ,新写一个MDCRunnable类对Runnable接口进行一层装饰。

在创建MDCRunnable类时保存当前线程的MDC值,在执行run()方法时再将保存的MDC值拷贝到异步线程中去。代码实现如下:

public class MDCRunnable implements Runnable {private final Runnable runnable;private final Map<String, String> map;public MDCRunnable(Runnable runnable) {this.runnable = runnable;// 保存当前线程的MDC值this.map = MDC.getCopyOfContextMap();}@Overridepublic void run() {// 传入已保存的MDC值for (Map.Entry<String, String> entry : map.entrySet()) {MDC.put(entry.getKey(), entry.getValue());}// 装饰器模式,执行run方法runnable.run();// 移除已保存的MDC值for (Map.Entry<String, String> entry : map.entrySet()) {MDC.remove(entry.getKey());}}}

接着,我们需要对main函数里创建的Runnable实现类进行装饰:

public class Main {private static final String KEY = "requestId";private static final Logger logger = LoggerFactory.getLogger(Main.class);private static final ExecutorService EXECUTOR = Executors.newSingleThreadExecutor();public static void main(String[] args) {// 入口传入请求IDMDC.put(KEY, UUID.randomUUID().toString());// 主线程打印<font style="color: #1e6bb8;word-wrap: break-word;font-weight: bold;border-bottom: 1px solid">日志</font>logger.debug("log in main thread");// 异步线程打印<font style="color: #1e6bb8;word-wrap: break-word;font-weight: bold;border-bottom: 1px solid">日志</font>,用MDCRunnable装饰Runnablenew Thread(new MDCRunnable(new Runnable() {@Overridepublic void run() {logger.debug("log in other thread");}})).start();// 异步线程池打印日志,用MDCRunnable装饰RunnableEXECUTOR.execute(new MDCRunnable(new Runnable() {@Overridepublic void run() {logger.debug("log in other thread pool");}}));EXECUTOR.shutdown();// 出口移除请求IDMDC.remove(KEY);}}

执行main函数,将会输出以下日志 :

2018-03-04 23:44:05.343 {requestId=5ee2a117-e090-41d8-977b-cef5dea09d34} [main] DEBUG cn.wudashan.Main - log in main thread
2018-03-04 23:44:05.346 {requestId=5ee2a117-e090-41d8-977b-cef5dea09d34} [Thread-1] DEBUG cn.wudashan.Main - log in other thread
2018-03-04 23:44:05.347 {requestId=5ee2a117-e090-41d8-977b-cef5dea09d34} [pool-2-thread-1] DEBUG cn.wudashan.Main - log in other thread pool

Congratulations! 经过我们的努力,最终在异步线程和线程池中都有requestId打印了!

4.总结

本文讲述了如何使用MDC工具来快速过滤一次请求的所有日志 ,并通过装饰器模式使得MDC工具在异步线程里也能生效。利用MDC,再配合AOP技术对所有的切面植入requestId,就可以将整个系统的任意流程的日志过滤出来。使用MDC工具,在开发自测阶段,可以极大地节省定位问题的时间,提升开发效率;在运维维护阶段,可以快速地收集相关日志信息,加快分析速度。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/813249.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

springboot @ConditionalOnProperty注解

ConditionalOnProperty注解可以有条件的加载bean 比如有3个定时任务&#xff0c;想设置开关 1、配置中添加 config:schedule:enabled:aaaScheduler: truebbbScheduler: truecccScheduler: true 2、代码 Component ConditionalOnProperty(prefix "config.schedule.en…

【功能更新】强化知识库管理与AI问答机器人性能

三月HelpLook带来了3大类功能焕新&#xff0c;主要聚焦于&#xff1a;知识库的管理功能升级和AI问答机器人的优化&#xff0c;让我们看看更新了哪些新功能&#xff01; 那么&#xff0c;接下来就让我们来详细了解一下本次升级都带来了哪些新功能吧&#xff01; 知识库使用与管理…

【Linux】应用层协议:HTTP

URL 在之前的文章中我们实现了一个网络版本的计算器&#xff0c;在那个计算器中揉合了协议定制以及序列化反序列化的内容&#xff0c;我们当时也自己定制了一套协议标准&#xff0c;比如请求和响应的格式应该是什么&#xff1f;如何读到一个完整的报文&#xff1f;支持的运算符…

【路径规划】基于六次多项式的多关节机器人避障路径规划

最近迷上了机械臂避障轨迹规划&#xff0c;因为之前一直做的都是无障碍物轨迹规划&#xff0c;所以这次想试一下有障碍物的&#xff0c;把避障算法用在我的SimMechanics机械臂上&#xff0c;看看效果咋样。以下定义不区分路径规划和轨迹规划。   by the way&#xff0c;本文实…

探索CSS世界中的色彩艺术:从基础到实战

在网页设计与布局中&#xff0c;CSS&#xff08;Cascading Style Sheets&#xff09;赋予了我们无尽的创作可能。其中&#xff0c;色彩作为视觉传达的重要元素&#xff0c;直接影响着网站的整体风格与用户体验。本篇教程将带领您踏上CSS颜色的探索之旅&#xff0c;从基础概念到…

二叉树的定义和基本术语及性质

二叉树是一种特殊的树形数据结构&#xff0c;它对每个节点的子节点数进行了限制&#xff0c;即每个节点最多有两个子节点。这种结构使得二叉树成为了许多算法和数据结构的基础&#xff0c;如二叉搜索树、堆、哈夫曼编码等。本文将详细探讨二叉树的定义、基本术语和性质&#xf…

BGP扩展知识总结

一、BGP的宣告问题 在BGP协议中每台运行BGP的设备上&#xff0c;宣告本地直连路由在BGP协议中运行BGP协议的设备&#xff0c;来宣告通过IGP学习到的未运行BGP协议设备产生的路由&#xff1b;&#xff08;常见&#xff09; 在BGP协议中宣告本地路由表中路由条目时&#xff0c;将…

Unity构建详解(8)——SBP的Bundle生成

【WriteSerializedFiles】 这里将实际的写操作执行单独拎了出来共用&#xff0c;放在了IRunCachedCallbacks&#xff0c;但数据的传入和处理还是在Task中执行。 这一步会生成实际的SerializedFile文件&#xff0c;文件名就是之前的InternalName&#xff0c;但这还不是最终的B…

DNF手游攻略:2024新手攻略大全

在《DNF手游》的世界中&#xff0c;前期阶段对于新手玩家来说至关重要。以下是一份综合整理的新手攻略&#xff0c;帮助玩家快速适应游戏并取得进展。 1. 角色建立策略&#xff1a; 在前期&#xff0c;建议玩家建立3个角色&#xff0c;包括1个大号和2个小号。大号可以根据个人喜…

Vue3常见核心面试题(之二)

Q1&#xff1a;Vue3如何实现全局状态管理&#xff1f; A1: Vue3中使用provide&#xff08;&#xff09;和 inject()函数来实现全局状态管理 Q2&#xff1a;Vue3中的ref指令有哪些用途&#xff1f; A2&#xff1a;Vue3中的ref指令可以用来在组件内获取子组件的实例&#xff0c…

谷歌google浏览器无法更新Chrome至最新版本怎么办?浏览器Chrome无法更新至最新版本

打开谷歌google浏览器提示&#xff1a;无法更新Chrome&#xff0c;Chrome无法更新至最新版本&#xff0c;因此您未能获得最新的功能和安全修复程序。点击「重新安装Chrome」后无法访问此网站&#xff0c;造成谷歌浏览器每天提示却无法更新Chrome至最新版本。 谷歌google浏览器无…

SaaS知识库哪些比较好用?中小企业也能适用

在快节奏的商业世界中&#xff0c;拥有一个高效、易于使用的知识管理工具是提升工作效率的关键。对于中小企业来说&#xff0c;选择合适的SaaS&#xff08;软件即服务&#xff09;知识库平台尤为重要&#xff0c;因为它不仅能帮助员工快速找到信息&#xff0c;而且还能优化客户…

C语言运行时隐藏控制台

#include <windows.h> int main() {// 获取控制台窗口句柄HWND hwnd GetConsoleWindow();// 隐藏控制台窗口ShowWindow(hwnd, SW_HIDE);while (1){/* code */}return 0; }编译后运行&#xff0c;什么都没看到&#xff0c;打开任务管理器&#xff0c;在进程里能找到它。

数据结构进阶之堆

今天我们学习的是数据结构里面的堆&#xff0c;大家先看看我们今天要学习的内容 一、堆概念及认识 在学习堆之前我们得先明白完全二叉树是什么样子&#xff0c;因为堆是依据完全二叉树的结构来实现的&#xff0c;所以在这里我先告诉大家完全二叉树的是什么&#xff0c;如下图…

Excel中输入数字会改变怎么办?

一、数字显示不全&#xff0c;以“#”号代替 随着列宽的缩小&#xff0c;数字逐渐被“#”号代替&#xff08;首先数字的格式是“数值型&#xff0c;且只有整数”&#xff09; 原因分析&#xff1a;单元格中的数字无法完全显示&#xff0c;Excel会自动用“#”号填充剩余的空间 解…

00_如何使用国内镜像源下载QT

如何使用国内镜像源下载QT 如何使用国内镜像源下载QT 如何使用国内镜像源下载QT 第一步&#xff1a;下载下载qt online installer 网站&#xff1a;https://download.qt.io/official_releases/online_installers/ 添加链接描述 下载windows版本 第二步&#xff1a; 剪切放…

【LeetCode热题100】【链表】两数相加

题目链接&#xff1a;2. 两数相加 - 力扣&#xff08;LeetCode&#xff09; 基本思路同&#xff1a;【leetcode】大数相加-CSDN博客 数值的位置已经倒过来了&#xff0c;用一个进位记录进位&#xff0c;用一个数记录和&#xff0c;链表到空了就当成0 class Solution { publi…

搭建个人智能家居 4 -WS2812B-RGB灯

搭建个人智能家居 4 - WS2812B-RGB灯 前言说明ESPHomeHomeAssistant 前言 上一篇文章我们已经完成了第一个外设的添加&#xff08;一个LED灯&#xff09;&#xff0c;今天接着来“壮大”这个系统&#xff0c;添加第二个外设“RGB灯”。 环境搭建可以回顾前面的文章。前文回顾&…

Vue 的父组件和子组件生命周期钩子函数执行顺序

Vue 的父组件和子组件生命周期钩子函数执行顺序 1. 加载渲染过程 父 beforeCreate -> 父 created -> 父 beforeMount -> 子 beforeCreate -> 子 created -> 子 beforeMount -> 子 mounted -> 父 mounted 2. 子组件更新过程 父 beforeUpdate -> 子 bef…

AutoCAD 2024 安装注册教程

前言 大家好&#xff0c;我是梁国庆。 本篇分享的安装包是 AutoCAD 的全新版本——AutoCAD 2024&#xff0c;下文安装教程中简称 AutoCAD。 时间&#xff1a;2024年4月8日。 获取 AutoCAD 安装包 我已将本篇所使用的安装包打包上传至百度云&#xff0c;扫描下方二维码关注…