一、背景

最近在持续改造项目,想通过日志查看用户所有的接口链路日志。在原来基于SpirngMVC的时候,那是比较好处理的,通过ThreadLocal,放入TraceId,就可以把一个TraceId传到各个地方,然后再需要的地方取出来,相当简单。但是在换了WebFlux,老实说,真还是有些麻烦。但这并不能难倒我们,发车!

现在把使用过程中问题和解决方式列出来,供大家参考。参考原文链接

SpringBoot 版本号: 2.6.10

二、 正文

2.1 实现方案

要实现用户调用链路所有的日志,那么我们就得通过唯一的ID去追踪。大致可以通过在请求的header中携带token,或者通过cookie这样的方式。考虑到大多数的使用场景,我们就使用在header中携带token的方式来实现。

2.2 实现方式

既然我们采取的是在header在添加token的方式,那么如何取出来,然后又在打印日志中获取到,这才是关键点。我们在SpringMVC中通常采用AOP的方式打印日志,那我们在WebFlux中是否也可以这样做呢?

2.2.1 步骤1 - 过滤器

当然可以了。要实现拦截,当然还是先实现WebFilter,代码如下。

/**
* <p>记录traceId</p>
*
* @author fattycal@qq.com
* @since 2022/8/8
*/
@Slf4j
@Configuration
public class TraceIdWebFilter implements WebFilter { private static final String TRACE_ID = ConstantsFields.TRACE_ID; @Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) { return chain.filter(exchange)
// 放入当前上下文,类似于ThreadLocal
.contextWrite(context -> {
// header 中是否有TRACE-ID
String traceId = exchange.getRequest().getHeaders().getFirst(TRACE_ID); traceId = Optional.ofNullable(traceId).orElse("");
if (!StringUtils.hasText(traceId)) {
log.warn("TRACE_ID not present in header: {}", exchange.getRequest().getURI());
} Context contextTmp = context.put(TRACE_ID, traceId);
exchange.getAttributes().put(TRACE_ID, traceId); return contextTmp;
});
}
}

实现WebFilter,通过contextWrite方法,把Header中的trace-id存入到上下文中。这个ContextWrite很重要,它是类似于ThreadLocal的东西,如果有老铁不知道,可以参考Context翻译文章,这里我们就不在一一赘述啦。

实现了WebFilter后,并且放入了Context中,这样我们是不是想ThreadLocal一样,取出来直接用就可以了?of course!

2.2.2 步骤2 - 切面

直接贴代码,如下(方式一)。

/**
* <br>日志切面</br>
*
* @author fattyca1@qq.com
* @since 2022/8/10
*/ @Aspect
@Configuration
@Slf4j
public class LoggerAspect { @Around("@annotation(com.fattycal.demo.webflux.annotation.Loggable)")
public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable { long start = System.currentTimeMillis();
Object result = joinPoint.proceed();
if (result instanceof Mono) {
Mono monoResult = (Mono) result;
AtomicReference<String> traceId = new AtomicReference<>("");
return monoResult.flatMap(ret -> Mono.subscriberContext().map(ctx -> {
// 从Context中取出traceId, 放入到了AtomicReference,正常变量没办法操作(内部类)。
traceId.set(ctx.getOrDefault(ConstantsFields.TRACE_ID, ""));
return ret;
}))
.doOnSuccess(o -> {
String response = "";
if (Objects.nonNull(o)) {
response = o.toString();
}
log.info("【{}】,Enter: {}.{}() with argument[s] = {}", traceId,
joinPoint.getSignature().getDeclaringTypeName(), joinPoint.getSignature().getName(),
joinPoint.getArgs());
log.info("【{}】,Exit: {}.{}() had arguments = {}, with result = {}, Execution time = {} ms", traceId,
joinPoint.getSignature().getDeclaringTypeName(), joinPoint.getSignature().getName(),
joinPoint.getArgs()[0],
response, (System.currentTimeMillis() - start));
});
} return result;
}
}

我们直接通过切面,来判断响应结果是否是属于Mono,如果是,则通过flatmap结合Mono.subscriberContext()拿到traceId,然后在doOnSuccess中打印日志。这样的好处是,不用自己订阅Mono.subscriberContext()

有的哥们就会问,为啥不在doOnSuccess()中去订阅呢? 好问题,我们的尝试一下。代码如下(方式二)。

@Aspect
@Configuration
@Slf4j
public class LoggerAspect { @Around("@annotation(com.fattycal.demo.webflux.annotation.Loggable)")
public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable { long start = System.currentTimeMillis();
Object result = joinPoint.proceed();
if (result instanceof Mono) {
Mono monoResult = (Mono) result;
// 错误的实现方式
return monoResult.doOnSuccess(obj -> Mono.subscriberContext()
.map(ctx -> {
String traceId = ctx.getOrDefault(ConstantsFields.TRACE_ID, "");
String response = "";
if (Objects.nonNull(obj)) {
response = obj.toString();
}
log.info("【{}】,Enter: {}.{}() with argument[s] = {}", traceId,
joinPoint.getSignature().getDeclaringTypeName(), joinPoint.getSignature().getName(),
joinPoint.getArgs());
log.info("【{}】,Exit: {}.{}() had arguments = {}, with result = {}, Execution time = {} ms", traceId,
joinPoint.getSignature().getDeclaringTypeName(), joinPoint.getSignature().getName(),
joinPoint.getArgs()[0],
response, (System.currentTimeMillis() - start));
return ctx;
})
); }
return result;
}
}

一激动,马上唰唰写出来了,但是这样写可不可以呢(文章已标记是错误的写法)?为啥说是错误的写法呢,那是因为在Reactor3中,有一个至理名言,那就是nothing happens until you subscribe()。我们没有订阅,所以Mono.subscriberContext().map()这一个流不会被执行的(点完餐付完钱店家才确定要做)。

所以我们稍微动一下代码,如下。

/**
* <br>日志切面</br>
*
* @author fattyca1@qq.com
* @since 2022/8/10
*/
@Aspect
@Configuration
@Slf4j
public class LoggerAspect { @Around("@annotation(com.fattycal.demo.webflux.annotation.Loggable)")
public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable { long start = System.currentTimeMillis();
Object result = joinPoint.proceed();
if (result instanceof Mono) {
Mono monoResult = (Mono) result;
// 把doOnSuccess这个操作放到单独线程池里做
return monoResult.publishOn(Schedulers.newElastic("fattyca1-thread-pool")).doOnSuccess(obj -> Mono.subscriberContext()
.map(ctx -> {
String traceId = ctx.getOrDefault(ConstantsFields.TRACE_ID, "");
String response = "";
if (Objects.nonNull(obj)) {
response = obj.toString();
}
log.info("【{}】,Enter: {}.{}() with argument[s] = {}", traceId,
joinPoint.getSignature().getDeclaringTypeName(), joinPoint.getSignature().getName(),
joinPoint.getArgs());
log.info("【{}】,Exit: {}.{}() had arguments = {}, with result = {}, Execution time = {} ms", traceId,
joinPoint.getSignature().getDeclaringTypeName(), joinPoint.getSignature().getName(),
joinPoint.getArgs()[0],
response, (System.currentTimeMillis() - start));
return ctx;
}).subscribe()
); }
return result;
}
}

我们在map方法后面又增加了subscribe()方法,这个时候,付钱了,餐馆才给你做饭。当然,我们又添加了publishOn这个方法,那是因为subscribe()是阻塞的,为了不阻塞,我们放进了一个新的线程池中处理。这样我们就大功告成啦! 马上动手测试一下

2.2.3 品尝果实

我们直接来一个朴实无华的测试,代码如下。

@RestController
public class WebfluxController { @RequestMapping("/hi/{name}")
@Loggable
public Mono<String> helloWorld(@PathVariable("name") String name) {
return Mono.fromSupplier(() -> "hi, " + name);
}
}
2.2.3.1 (方式一)

先按照方式一的方式来测试,结果如图所示。

方式一测试出来,的确没问题,我们把Header中的Trace-id打印出来了。那接下来试试方式二。

2.2.3.2 (方式二)

方式一实现方式测试,结果如图所示。

擦,扑街了~,从测试结果看,这样的方式是取不到Context中的值,我在尝试去掉线程池后,也还是取不到Context中的值。为什么没有取到这一点,还没研究透,后面研究透了给大家补上

从结果看,我们还是通过FlatMap的方式,提前拿到Trace-id还是靠谱一点。

2.2.3.2 测试三

我们修改了一下Controller中的代码,通过flatMap,从Context中获取traceId。再测试一下。结果如图所示。

通过测试我们发下,通过flatmap,然后再从Context中获取traceId是完全可行的,所以我们在实际使用的时候还是要注意下方式。

三、总结

先实践,实操,在理解原理。以上为实际实践,如果发现有问题,欢迎指出,交流!

(WebFlux)002、如何打印日志与链路ID的更多相关文章

  1. 基于SLF4J的MDC机制和Dubbo的Filter机制,实现分布式系统的日志全链路追踪

    原文链接:基于SLF4J的MDC机制和Dubbo的Filter机制,实现分布式系统的日志全链路追踪 一.日志系统 1.日志框架 在每个系统应用中,我们都会使用日志系统,主要是为了记录必要的信息和方便排 ...

  2. Mybatis框架基于映射文件和配置文件的方式,实现增删改查,可以打印日志信息

    首先在lib下导入: 与打印日志信息有关的架包 log4j-1.2.16.jar mybatis架包:mybatis-3.1.1.jar 连接数据库的架包:mysql-connector-java-5 ...

  3. android112 c代码打印日志,c反编译调用java

    activity: package com.itheima.ccalljava; import android.os.Bundle; import android.app.Activity; impo ...

  4. 以打印日志为荣之logging模块详细使用

    啄木鸟社区里的Pythonic八荣八耻有一条: 以打印日志为荣 , 以单步跟踪为耻; 很多程序都有记录日志的需求,并且日志中包含的信息既有正常的程序访问日志,还可能有错误.警告等信息输出,python ...

  5. springboot aop + logback + 统一异常处理 打印日志

    1.src/resources路径下新建logback.xml 控制台彩色日志打印 info日志和异常日志分不同文件存储 每天自动生成日志 结合myibatis方便日志打印(debug模式) < ...

  6. 大数据项目中js中代码和java中代码(解决Tomcat打印日志中文乱码)

    Idea2018中集成Tomcat9导致OutPut乱码找到tomcat的安装目录,打开logging.properties文件,增加一行代码,覆盖默认设置,将日志编码格式修改为GBK.java.ut ...

  7. Java中打印日志,这4点很重要!

    目录 一.预先判断日志级别 二.避免无效日志打印 三.区别对待错误日志 四.保证记录完整内容 打印日志,要注意下面4点. 一.预先判断日志级别 对DEBUG.INFO级别的日志,必须使用条件输出或者使 ...

  8. 关于spring 事务 和 AOP 管理事务和打印日志问题

    关于spring 事务 和 AOP 管理事务和打印日志问题 1. 就是支持事务注解的(@Transactional) . ​ 可以在server层总使用@Transactional,进行方法内的事务管 ...

  9. Java如何打印日志

    以下为<正确的打日志姿势>学习笔记. 什么时候打日志 1.程序出现问题,只能通过 debug 功能来定位问题,很大程度是日志没打好.良好的系统,通过日志就能进行问题定位. 2.if-els ...

随机推荐

  1. Fail2ban 配置详解 过滤器配置

    Fail2ban自带了很多相关服务日志的过滤器. ### # 包含配置 ### [INCLUDES] before = common.conf # 还包含其他文件中的配置,在加载本配置文件中配置之前先 ...

  2. 《Mybatis 手撸专栏》第10章:使用策略模式,调用参数处理器

    作者:小傅哥 博客:https://bugstack.cn 沉淀.分享.成长,让自己和他人都能有所收获! 一.前言 你这代码写的,咋这么轴呢! 说到轴,让我想起初中上学时老师说的话:"你那脑 ...

  3. 如何优化PlantUML流程图(时序图)

    这篇文章用来介绍,如何画出好看的流程图. 1. 选择合适的组件 1.1 plantuml官方提供的组件 1.2 加载图片 1.2.1 加载本地图片 1.2.2 加载网络图片 1.2.3 图片资源 2. ...

  4. python PIL 图片素描化

    from PIL import Image import numpy as np a = np.asarray(Image.open("D://7.jpg").convert('L ...

  5. Java使用类-String

    String,StringBuffer,StringBuild 大佬的理解-><深入理解Java中的String> 1.String 1.1 String 实例化 String st ...

  6. 前端学习 linux —— shell 编程

    前端学习 linux - shell 编程 shell 原意是"外壳",与 kernel(内核)相对应,比喻内核外的一层,是用户和内核沟通的桥梁.shell 有很多种,国内通常使用 ...

  7. 记一次 .NET 某物管后台服务 卡死分析

    一:背景 1. 讲故事 这几个月经常被朋友问,为什么不更新这个系列了,哈哈,确实停了好久,主要还是打基础去了,分析 dump 的能力不在于会灵活使用 windbg,而是对底层知识有一个深厚的理解,比如 ...

  8. Mysql中的小技巧

    1.where 字段名 regexp '正则表达式' 正则符号: ^ $ . [ ] * | . 表示1个任意字符 * 表示前面重复0次,或者任意次 ^ 开始 $ 结尾 [] 范围 | 或 sql示例 ...

  9. 编写可维护的webpack配置

    为什么要构建配置抽离成npm包 通用性 业务开发者无需挂住配置 统一团队构建脚本 可维护性 构建配置合理的拆分 README文档, chan 构建配置管理的可选方案 通过多个配置管理不同环境的构建, ...

  10. Django WEB开发 - Django 3.0 Tutorial入门

    1. Django 官方网站 https://docs.djangoproject.com/zh-hans/3.0/intro/tutorial01/ 2. Model View Controller ...