SpringOne2023峰会总结-02-SpringBoot与Micrometer如何在WebFlux环境下实现的链路日志

个人创作公约:本人声明创作的所有文章皆为自己原创,如果有参考任何文章的地方,会标注出来,如果有疏漏,欢迎大家批判。如果大家发现网上有抄袭本文章的,欢迎举报,并且积极向这个 github 仓库 提交 issue,谢谢支持~
另外,本文为了避免抄袭,会在不影响阅读的情况下,在文章的随机位置放入对于抄袭和洗稿的人的“亲切”的问候。如果是正常读者看到,笔者在这里说声对不起,。如果被抄袭狗或者洗稿狗看到了,希望你能够好好反思,不要再抄袭了,谢谢。

02-SpringBoot与Micrometer如何在WebFlux环境下实现的链路日志

  • 视频原始地址:https://www.youtube.com/watch?v=xF7aZJlfTSw&list=PLgGXSWYM2FpPrAdQor9pi__EV1O69Qbom&index=25
  • 个人翻译地址:https://www.bilibili.com/video/BV12K421y7kF/
  • 个人总结代码与介绍地址:https://github.com/HashZhang/SpringOne-2023/blob/main/02-SpringBoot与Micrometer如何在WebFlux环境下实现的链路日志

我们可以在日志中加入链路信息,这样我们可以找到某个请求,某个事务所有的日志,这样就可以方便的进行问题排查。并且,我们还可以通过 traceId 找到不同微服务调用链路相关的日志。 在 Spring Boot 3.x 之前,我们一般用 spring-cloud-sleuth 去实现,但是在 Spring Boot 3.x 之后,已经去掉了对于 sleuth 的原生支持,全面改用了 micrometer。

首先,我们先思考下,这些链路日志是怎么实现的?我们知道,所有的日志框架,都带有 %X 这个日志格式占位符。这个占位符,就是从 MDC(Mapped Diagnostic Context)中取出对应 key 来实现的。MDC 是一个 ThreadLocal 的变量,它是一个 Map,我们可以在任何地方往里面放值,然后在任何地方取出来。这样,我们就可以在任何地方,把 traceId 放到 MDC 中,然后通过类似于下面的日志格式,就可以在日志中打印出来。

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

1. 不依赖任何框架,如何实现链路日志

如果我们不依赖任何框架,看看我们如何实现这个功能。首先在 web-mvc 环境下,我们可以通过拦截器,在请求进来的时候,把 traceId 放到 MDC 中,然后在请求结束的时候,把 traceId 从 MDC 中移除。

然后,我们添加一个普通的接口,这个接口里面,我们打印一下日志,看看 traceId 是否能够打印出来。这些都是在 web-mvc 环境下,我们可以很方便的实现。这里为了方便,我们把所有代码放在一个类里面:

import jakarta.servlet.Filter;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.MDC;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;@Slf4j
@SpringBootApplication
public class Main {public static void main(String[] args) {SpringApplication.run(Main.class);}/*** Returns an instance of the correlation filter.* This filter sets the Mapped Diagnostic Context (MDC) with the requested traceId parameter, if available, before executing the filter chain. After the filter chain is executed* , it removes the traceId from the MDC.** @return the correlation filter*/@Beanpublic Filter correlationFilter() {return (request, response, chain) -> {try {log.info("adding traceId");String name = request.getParameter("traceId");if (name != null) {MDC.put("traceId", name);}log.info("traceId added");chain.doFilter(request, response);} finally {log.info("removing traceId");MDC.remove("traceId");log.info("traceId removed");}};}/*** The ExampleController class is a REST controller that handles the "/hello" endpoint.* It is responsible for returning a greeting message with the provided traceId parameter,* and it logs the message "hello endpoint called" when the endpoint is called.*/@Slf4j@RestControllerpublic static class ExampleController {@GetMapping("/hello")String hello(@RequestParam String traceId) {log.info("hello endpoint called");return "Hello, " + traceId + "!";}}
}

启动后,我们调用 hello 接口,传入 traceId 参数(curl 'http://127.0.0.1:8080/hello?traceId=123456'),我们可以看到日志中打印出了 traceId。

2024-02-02 17:58:14.727 [http-nio-8080-exec-1][INFO ][c.g.h.s.Example_01.Main][] - adding traceId
2024-02-02 17:58:14.728 [http-nio-8080-exec-1][INFO ][c.g.h.s.Example_01.Main][123456] - traceId added
2024-02-02 17:58:14.736 [http-nio-8080-exec-1][INFO ][c.g.h.s.E.Main$ExampleController][123456] - hello endpoint called
2024-02-02 17:58:14.740 [http-nio-8080-exec-1][INFO ][c.g.h.s.Example_01.Main][123456] - removing traceId
2024-02-02 17:58:14.740 [http-nio-8080-exec-1][INFO ][c.g.h.s.Example_01.Main][] - traceId removed

我们可以看出,随着 traceId 放入 MDC,日志中开始有了 traceId,然后随着 traceId 从 MDC 中移除,日志中的 traceId 也消失了。这就是链路日志的原理。

2. 遇到问题,链路信息丢失

由于 MDC 是一个 ThreadLocal 的变量,所以在 WebFlux 的环境下,由于每个操作符都可能会切换线程(在发生 IO 的时候,或者使用 subscribeOn 或者 publishOn 这种操作符),这就导致了我们在 WebFlux 环境下,无法通过 MDC 来实现链路日志。我们举个例子:

@GetMapping("/hello2")
Mono<String> hello2(@RequestParam String traceId) {return Mono.fromSupplier(() -> {log.info("hello2 endpoint called");return "Hello, " + traceId + "!";}).subscribeOn(Schedulers.boundedElastic()).map(s -> {log.info("map operator");return s + s;}).flatMap(s -> {log.info("flatMap operator");return Mono.just(s + s);});
}

这时候,我们调用 hello2 接口,传入 traceId 参数(curl 'http://127.0.0.1:8080/hello2?traceId=123456'),我们可以看到日志中并没有 traceId。

2024-02-02 18:09:08.398 [http-nio-8080-exec-1][INFO ][c.g.h.s.Example_01.Main][] - adding traceId
2024-02-02 18:09:08.398 [http-nio-8080-exec-1][INFO ][c.g.h.s.Example_01.Main][123456] - traceId added
2024-02-02 18:09:08.421 [boundedElastic-1][INFO ][c.g.h.s.E.Main$ExampleController][] - hello2 endpoint called
2024-02-02 18:09:08.421 [boundedElastic-1][INFO ][c.g.h.s.E.Main$ExampleController][] - map operator
2024-02-02 18:09:08.421 [boundedElastic-1][INFO ][c.g.h.s.E.Main$ExampleController][] - flatMap operator
2024-02-02 18:09:08.423 [http-nio-8080-exec-1][INFO ][c.g.h.s.Example_01.Main][123456] - removing traceId
2024-02-02 18:09:08.424 [http-nio-8080-exec-1][INFO ][c.g.h.s.Example_01.Main][] - traceId removed

同时,我们可以看到,我们在 hello2 方法中,使用了 subscribeOn 操作符,这就导致了我们的代码在 boundedElastic 线程中执行,而不是在 http-nio-8080-exec-1 线程中执行。这就导致了我们在 WebFlux 环境下,无法通过 MDC 来实现链路日志。

3. 解决方案,以及观察纯 Webflux 下的效果

Micrometer 社区做了很多兼容各种框架的工作,我们首先添加依赖:

<dependency><groupId>io.micrometer</groupId><artifactId>context-propagation</artifactId><version>1.0.4</version>
</dependency>

然后,通过以下代码启用 Project Reactor 的 ContextPropagation:

Hooks.enableAutomaticContextPropagation();

以上代码的作用是,在 WebFlux 的各种操作符的时候,会自动把当前的 Context 传递到下游中。

然后,添加 context-propagation 中从线程上下文获取信息的功能,同时,在这里将 MDC 中 traceId 信息提取:

ContextRegistry.getInstance().registerThreadLocalAccessor(//key"traceId",//提取什么信息,这里提取 MDC 中的 traceId() -> MDC.get("traceId"),//设置什么信息,这里设置 MDC 中的 traceIdtraceId -> MDC.put("traceId", traceId),//清理什么信息,这里清理 MDC 中的 traceId() -> MDC.remove("traceId"));

之后,重启我们的应用,我们调用 hello2 接口,传入 traceId 参数(curl 'http://http://127.0.0.1:8080/hello2?traceId=123456'):

2024-02-02 19:49:47.729 [http-nio-8080-exec-9][INFO ][c.g.h.s.Example_01.Main][] - adding traceId
2024-02-02 19:49:47.730 [http-nio-8080-exec-9][INFO ][c.g.h.s.Example_01.Main][123456] - traceId added
2024-02-02 19:49:47.730 [boundedElastic-3][INFO ][c.g.h.s.E.Main$ExampleController][123456] - hello2 endpoint called
2024-02-02 19:49:47.731 [boundedElastic-3][INFO ][c.g.h.s.E.Main$ExampleController][123456] - map operator
2024-02-02 19:49:47.731 [http-nio-8080-exec-9][INFO ][c.g.h.s.Example_01.Main][123456] - removing traceId
2024-02-02 19:49:47.731 [boundedElastic-3][INFO ][c.g.h.s.E.Main$ExampleController][123456] - flatMap operator
2024-02-02 19:49:47.731 [http-nio-8080-exec-9][INFO ][c.g.h.s.Example_01.Main][] - traceId removed

我们可以看到,我们在 hello2 方法中,使用了 subscribeOn 操作符,这就导致了我们的代码在 boundedElastic 线程中执行,而不是在 http-nio-8080-exec-1 线程中执行。但是,我们可以看到,我们的日志中,traceId 依然被打印出来了。这就是我们通过 Micrometer 实现链路日志的原理。

4. 框架自动实现链路日志

上面我们演示的工作,其实框架都会帮我们做了。我们只需要添加依赖:

<dependency><groupId>io.micrometer</groupId><artifactId>micrometer-tracing-bridge-brave</artifactId>
</dependency>

重新编写代码,依然需要启用 Project Reactor 的 ContextPropagation:

import jakarta.servlet.Filter;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.MDC;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Hooks;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Schedulers;@Slf4j
@SpringBootApplication
public class Main {public static void main(String[] args) {Hooks.enableAutomaticContextPropagation();SpringApplication.run(Main.class);}@Slf4j@RestControllerpublic static class ExampleController {@GetMapping("/hello")String hello() {log.info("hello endpoint called");return "Hello!";}@GetMapping("/hello2")Mono<String> hello2() {return Mono.fromSupplier(() -> {log.info("hello2 endpoint called");return "Hello2!";}).subscribeOn(Schedulers.boundedElastic()).map(s -> {log.info("map operator");return s + s;}).flatMap(s -> {log.info("flatMap operator");return Mono.just(s + s);});}}
}

之后,重启我们的应用,调用 hello 和 hello2 接口,可以看到日志中都有 traceId。


2024-02-02 20:32:11.263 [http-nio-8080-exec-5][INFO ][c.g.h.s.E.Main$ExampleController][65bce0cb58b130d852320a114ffa79d0] - hello endpoint called
2024-02-02 20:32:13.262 [boundedElastic-2][INFO ][c.g.h.s.E.Main$ExampleController][65bce0cd3ca3e1a311aaa81e13317436] - hello2 endpoint called
2024-02-02 20:32:13.262 [boundedElastic-2][INFO ][c.g.h.s.E.Main$ExampleController][65bce0cd3ca3e1a311aaa81e13317436] - map operator
2024-02-02 20:32:13.262 [boundedElastic-2][INFO ][c.g.h.s.E.Main$ExampleController][65bce0cd3ca3e1a311aaa81e13317436] - flatMap operator

微信搜索“hashcon”关注公众号,加作者微信
image
我会经常发一些很好的各种框架的官方社区的新闻视频资料并加上个人翻译字幕到如下地址(也包括上面的公众号),欢迎关注:

  • 知乎:https://www.zhihu.com/people/zhxhash
  • B 站:https://space.bilibili.com/31359187

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

如若内容造成侵权/违法违规/事实不符,请联系瞎胡闹网进行投诉反馈,一经查实,立即删除!

相关文章

Android 移动应用开发 创建第一个Android项目

文章目录 一、创建第一个Android项目1.1 准备好Android Studio1.2 运行程序1.3 程序结构是什么app下的结构res - 子目录&#xff08;所有图片、布局、字AndroidManifest.xml 有四大组件&#xff0c;程序添加权限声明 Project下的结构 二、开发android时&#xff0c;部分库下载异…

利用YOLOv8 pose estimation 进行 人的 头部等马赛克

文章大纲 马赛克几种OpenCV 实现马赛克的方法高斯模糊pose estimation 定位并模糊:三角形的外接圆与膨胀系数实现实现代码实现效果参考文献与学习路径之前写过一个文章记录,怎么对人进行目标检测后打码,但是人脸识别有个问题是,很多人的背影,或者侧面无法识别出来人脸,那…

【Python 千题 —— 基础篇】查找年龄

Python 千题持续更新中 …… 脑图地址 👉:⭐https://twilight-fanyi.gitee.io/mind-map/Python千题.html⭐ 题目描述 题目描述 班级中有 Tom、Alan、Bob、Candy、Sandy 五个人,他们组成字典 {Tom: 23, Alan: 24, Bob: 21, Candy: 22, Sandy: 21},字典的键是姓名,字典的…

C++模版(初阶)

&#x1f308;函数复用的两种不恰当方式 ☀️1.函数重载 以Swap函数为例&#xff0c;有多少种参数类型组合&#xff0c;就要重载多少个函数&#xff1a; void Swap(int& left, int& right) {int temp left;left right;right temp; } void Swap(double& left,…

[word] word如何打印背景和图片? #微信#其他#经验分享

word如何打印背景和图片&#xff1f; 日常办公中会经常要打印文件的&#xff0c;其实在文档的打印中也是有很多技巧的&#xff0c;可以按照自己的需求设定&#xff0c;下面给大家分享word如何打印背景和图片&#xff0c;一起来看看吧&#xff01; 1、打印背景和图片 在默认的…

Springboot拦截器中跨域失效的问题、同一个接口传入参数不同,一个成功,一个有跨域问题、拦截器和@CrossOrigin和@Controller

Springboot拦截器中跨域失效的问题 一、概述 1、具体场景 起因&#xff1a; 同一个接口&#xff0c;传入不同参数进行值的修改时&#xff0c;一个成功&#xff0c;另一个竟然失败&#xff0c;而且是跨域问题拦截器内的request参数调用getHeader方法时&#xff0c;获取不到前端…

【Spring源码分析】Spring的启动流程源码解析

阅读此需阅读下面这些博客先【Spring源码分析】Bean的元数据和一些Spring的工具【Spring源码分析】BeanFactory系列接口解读【Spring源码分析】执行流程之非懒加载单例Bean的实例化逻辑【Spring源码分析】从源码角度去熟悉依赖注入&#xff08;一&#xff09;【Spring源码分析】…

应用层 HTTP协议(1)

回顾 前面我们说到了数据链路层,网络层IP协议,传输层的TCP/UDP协议一些知识点,现在让我们谈谈 应用层的HTTP协议的知识点. 这篇我们先从大局入手,仍然是对总体报文进行全局分析,再对细节报文进行拆解分析 版本 首先我们谈谈HTTP协议的版本 HTTP 0.9 (1991) HTTP 1.0 (1992 - 1…

二、OpenAI开发者快速入门

启动并运行OpenAI API OpenAI API 为开发者提供一个简单的接口&#xff0c;使其能够在他们的应用中创建一个智能层&#xff0c;由OpenAI最先进的模型提供支持。聊天补全端点为ChatGPT提示支持&#xff0c;一种简单的方法是&#xff1a;输入文本&#xff0c;使用GPT-4模型输出。…

Spring Boot3统一结果封装

⛰️个人主页: 蒾酒 &#x1f525;系列专栏&#xff1a;《spring boot实战》 &#x1f30a;山高路远&#xff0c;行路漫漫&#xff0c;终有归途。 目录 前置条件 封装目的 常用格式 定义返回结果枚举类 定义返回结果封装类 对返回结果封装 测试封装 前置条件 已…

【Linux】Linux下的基本指令

Linux下的基本指令 Linux 的操作特点&#xff1a;纯命令行ls 指令文件 pwd命令Linux的目录结构绝对路径 / 相对路径&#xff0c;我该怎么选择&#xff1f; cd指令touch指令mkdir指令&#xff08;重要&#xff09;rmdir指令rm 指令&#xff08;重要&#xff09;man指令&#xff…

C语言分钟计算

一.题目描述 给你同一天的两个时间(24小时制),求这两个时间内有多少分钟,保证第一个时间在第二个时间之前. 二.输入描述 输入两行,每行包括两个整数表示小时和分钟. 三.输出描述 输出分钟数. 四.示例 输入 10 10 11 05 输出 55 五.代码

烟火可禁却难禁,灵境难及终将及

现实痛点 2024年1月30日&#xff0c;贵阳市发生了一件令人痛心的事&#xff0c;有人在小区内放烟花导致失火&#xff0c;一男子具备足够的消防安全知识&#xff0c;知道如何使用消防栓却因设施不合格接不上消防栓&#xff0c;接上了又没水&#xff0c;消防员来也束手无策&…

【Django】Django中间件

Django中间件 1 中间件的定义 中间件是Django请求/响应处理的钩子框架。它是一个轻量级的、低级的“插件”系统&#xff0c;用于全局改变Django的输入或输出。 中间件以类的形式体现。 每个中间件组件负责做一些特定的功能。例如&#xff0c;Django包含一个中间件组件Authen…

Redis -- 渐进式遍历

家&#xff0c;是心的方向。不论走多远&#xff0c;总有一盏灯为你留着。桌上的碗筷多了几双&#xff0c;笑声也多了几分温暖。家人团聚&#xff0c;是最美的风景线。时间&#xff1a;2024年 2月 8日 12:51:20 目录 前言 语法 示例 前言 试想一个场景,那就是在key非常多的…

前端vue学习

1.创建vue项目 2.绑定html属性值 3.v-bind &#xff1a; 4.v-if 5.v-for 6.更改html内容 7.e.preventDefault()阻止默认事件 8.事件修饰符号&#xff0c;使用.prevent可以达到和上面一样的效果 9.阻止事件冒泡 10.计算属性缓存与方法&#xff0c;计算属性存在缓存&#xff0c;…

Bean 的作用域

Bean 的作用域种类 在 Spring 中⽀持 6 种作⽤域&#xff0c;后 4 种在 Spring MVC 环境才⽣效 1. singleton&#xff1a;单例作⽤域 2. prototype&#xff1a;原型作⽤域&#xff08;多例作⽤域&#xff09; 3. request&#xff1a;请求作⽤域 4. session&#xff1a;会话作⽤…

Android:内存泄漏检查内存优化

3.17Android优化 手机移动设备的内存是有限的,需要避免内存泄漏,优化内存使用。 1.java中四种引用类型 强引用、软引用、弱引用、虚引用。 强引用:使用类构造方法,创建对象,当内存超出了,也不会释放对象所占内存空间; String str = new String(‘1223’); 切断引用str=…

Python pandas中read_csv函数的io参数

前言 在数据分析和处理中&#xff0c;经常需要读取外部数据源&#xff0c;例如CSV文件。Python的pandas库提供了一个强大的 read_csv() 函数&#xff0c;用于读取CSV文件并将其转换成DataFrame对象&#xff0c;方便进一步分析和处理数据。在本文中&#xff0c;将深入探讨 read…

Java与MySQL的精准结合:打造高效审批流程

1流程思路分析 审批流程&#x1f431;‍&#x1f4bb; 1.串行流程 当前节点审批完成后&#xff0c;下一次节点才能进行操作&#xff0c;例如经理通过之后&#xff0c;总监才能审批&#xff1b; 2.并行流程 一个审批节点需要多人联审。一般有两种方式&#xff1a;会签、或签…