SpringCloud 微服务中网关如何记录请求响应日志?

在基于SpringCloud开发的微服务中,我们一般会选择在网关层记录请求和响应日志,并将其收集到ELK中用作查询和分析。

今天我们就来看看如何实现此功能。

日志实体类

首先我们在网关中定义一个日志实体,用于组装日志对象

@Data
public class AccessLog {/**用户编号**/private Long userId;/**路由**/private String targetServer;/**协议**/private String schema;/**请求方法名**/private String requestMethod;/**访问地址**/private String requestUrl;/**请求IP**/private String clientIp;/**查询参数**/private MultiValueMap<String, String> queryParams;/**请求体**/private String requestBody;/**请求头**/private MultiValueMap<String, String> requestHeaders;/**响应体**/private String responseBody;/**响应头**/private MultiValueMap<String, String> responseHeaders;/**响应结果**/private HttpStatusCode httpStatusCode;/**开始请求时间**/private LocalDateTime startTime;/**结束请求时间**/private LocalDateTime endTime;/**执行时长,单位:毫秒**/private Integer duration;}
网关日志过滤器

接下来我们在网关中定义一个Filter,用于收集日志信息。

@Component
public class AccessLogFilter implements GlobalFilter, Ordered {private final List<HttpMessageReader<?>> messageReaders = HandlerStrategies.withDefaults().messageReaders();/*** 打印日志* @param accessLog 网关日志*/private void writeAccessLog(AccessLog accessLog) {log.info("----access---- : {}", JsonUtils.obj2StringPretty(accessLog));}/*** 顺序必须是<-1,否则标准的NettyWriteResponseFilter将在您的过滤器得到一个被调用的机会之前发送响应* 也就是说如果不小于 -1 ,将不会执行获取后端响应的逻辑* @return*/@Overridepublic int getOrder() {return -100;}@Overridepublic Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {// 将 Request 中可以直接获取到的参数,设置到网关日志ServerHttpRequest request = exchange.getRequest();AccessLog gatewayLog = new AccessLog();gatewayLog.setTargetServer(WebUtils.getGatewayRoute(exchange).getId());gatewayLog.setSchema(request.getURI().getScheme());gatewayLog.setRequestMethod(request.getMethod().name());gatewayLog.setRequestUrl(request.getURI().getRawPath());gatewayLog.setQueryParams(request.getQueryParams());gatewayLog.setRequestHeaders(request.getHeaders());gatewayLog.setStartTime(LocalDateTime.now());gatewayLog.setClientIp(WebUtils.getClientIP(exchange));// 继续 filter 过滤MediaType mediaType = request.getHeaders().getContentType();if (MediaType.APPLICATION_FORM_URLENCODED.isCompatibleWith(mediaType)|| MediaType.APPLICATION_JSON.isCompatibleWith(mediaType)) { // 适合 JSON 和 Form 提交的请求return filterWithRequestBody(exchange, chain, gatewayLog);}return filterWithoutRequestBody(exchange, chain, gatewayLog);}/*** 没有请求体的请求只需要记录日志*/private Mono<Void> filterWithoutRequestBody(ServerWebExchange exchange, GatewayFilterChain chain, AccessLog accessLog) {// 包装 Response,用于记录 Response BodyServerHttpResponseDecorator decoratedResponse = recordResponseLog(exchange, accessLog);return chain.filter(exchange.mutate().response(decoratedResponse).build()).then(Mono.fromRunnable(() -> writeAccessLog(accessLog)));}/*** 需要读取请求体* 参考 {@link ModifyRequestBodyGatewayFilterFactory} 实现*/private Mono<Void> filterWithRequestBody(ServerWebExchange exchange, GatewayFilterChain chain, AccessLog gatewayLog) {// 设置 Request Body 读取时,设置到网关日志ServerRequest serverRequest = ServerRequest.create(exchange, messageReaders);Mono<String> modifiedBody = serverRequest.bodyToMono(String.class).flatMap(body -> {gatewayLog.setRequestBody(body);return Mono.just(body);});// 通过 BodyInserter 插入 body(支持修改body), 避免 request body 只能获取一次BodyInserter<Mono<String>, ReactiveHttpOutputMessage> bodyInserter = BodyInserters.fromPublisher(modifiedBody, String.class);HttpHeaders headers = new HttpHeaders();headers.putAll(exchange.getRequest().getHeaders());// the new content type will be computed by bodyInserter// and then set in the request decoratorheaders.remove(HttpHeaders.CONTENT_LENGTH);CachedBodyOutputMessage outputMessage = new CachedBodyOutputMessage(exchange, headers);// 通过 BodyInserter 将 Request Body 写入到 CachedBodyOutputMessage 中return bodyInserter.insert(outputMessage, new BodyInserterContext()).then(Mono.defer(() -> {// 重新封装请求ServerHttpRequest decoratedRequest = requestDecorate(exchange, headers, outputMessage);// 记录响应日志ServerHttpResponseDecorator decoratedResponse = recordResponseLog(exchange, gatewayLog);// 记录普通的return chain.filter(exchange.mutate().request(decoratedRequest).response(decoratedResponse).build()).then(Mono.fromRunnable(() -> writeAccessLog(gatewayLog))); // 打印日志}));}/*** 记录响应日志* 通过 DataBufferFactory 解决响应体分段传输问题。*/private ServerHttpResponseDecorator recordResponseLog(ServerWebExchange exchange, AccessLog accessLog) {ServerHttpResponse response = exchange.getResponse();return new ServerHttpResponseDecorator(response) {@Overridepublic Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {if (body instanceof Flux) {DataBufferFactory bufferFactory = response.bufferFactory();// 计算执行时间accessLog.setEndTime(LocalDateTime.now());accessLog.setDuration((int) (LocalDateTimeUtil.between(accessLog.getStartTime(),accessLog.getEndTime()).toMillis()));accessLog.setResponseHeaders(response.getHeaders());accessLog.setHttpStatusCode(response.getStatusCode());// 获取响应类型,如果是 json 就打印String originalResponseContentType = exchange.getAttribute(ServerWebExchangeUtils.ORIGINAL_RESPONSE_CONTENT_TYPE_ATTR);if (StrUtil.isNotBlank(originalResponseContentType)&& originalResponseContentType.contains("application/json")) {Flux<? extends DataBuffer> fluxBody = Flux.from(body);return super.writeWith(fluxBody.buffer().map(dataBuffers -> {// 设置 response body 到网关日志byte[] content = readContent(dataBuffers);String responseResult = new String(content, StandardCharsets.UTF_8);accessLog.setResponseBody(responseResult);// 响应return bufferFactory.wrap(content);}));}}// if body is not a flux. never got there.return super.writeWith(body);}};}/*** 请求装饰器,支持重新计算 headers、body 缓存** @param exchange 请求* @param headers 请求头* @param outputMessage body 缓存* @return 请求装饰器*/private ServerHttpRequestDecorator requestDecorate(ServerWebExchange exchange, HttpHeaders headers, CachedBodyOutputMessage outputMessage) {return new ServerHttpRequestDecorator(exchange.getRequest()) {@Overridepublic HttpHeaders getHeaders() {long contentLength = headers.getContentLength();HttpHeaders httpHeaders = new HttpHeaders();httpHeaders.putAll(super.getHeaders());if (contentLength > 0) {httpHeaders.setContentLength(contentLength);} else {// TODO: this causes a 'HTTP/1.1 411 Length Required' // on// httpbin.orghttpHeaders.set(HttpHeaders.TRANSFER_ENCODING, "chunked");}return httpHeaders;}@Overridepublic Flux<DataBuffer> getBody() {return outputMessage.getBody();}};}/*** 从dataBuffers中读取数据* @author jam* @date 2024/5/26 22:31*/private byte[] readContent(List<? extends DataBuffer> dataBuffers) {// 合并多个流集合,解决返回体分段传输DataBufferFactory dataBufferFactory = new DefaultDataBufferFactory();DataBuffer join = dataBufferFactory.join(dataBuffers);byte[] content = new byte[join.readableByteCount()];join.read(content);// 释放掉内存DataBufferUtils.release(join);return content;}}

代码较长建议直接拷贝到编辑器,只要注意下面一个关键点:

getOrder()方法返回的值必须要<-1,否则标准的NettyWriteResponseFilter将在您的过滤器被调用的机会之前发送响应,即不会执行获取后端响应参数的方法

通过上面的两步我们已经可以获取到请求的输入输出参数了,在 writeAccessLog()中将其打印到日志文件,方便通过ELK进行收集。

在实际项目中,网关日志量一般会非常大,不建议使用数据库进行存储。

实际效果

服务正常响应

图片

服务异常响应

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

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

相关文章

第九十七节 Java面向对象设计 - Java Object.Finalize方法

Java面向对象设计 - Java Object.Finalize方法 Java提供了一种在对象即将被销毁时执行资源释放的方法。 在Java中&#xff0c;我们创建对象&#xff0c;但是我们不能销毁对象。 JVM运行一个称为垃圾收集器的低优先级特殊任务来销毁不再引用的所有对象。 垃圾回收器给我们一个…

函数计数和跟踪 --- console的count和trace方法

新学到一个小方法&#xff0c;分享一下哦。 使用 console 对象的 trace ⽅法在控制台上输出当前的调用栈&#xff0c;可以追踪⼀个函数的执⾏过程。 当我们想要了解一个函数是如何被其他函数调用的&#xff0c;或者想要查看调用栈中的其他信息时&#xff0c;这个方法非常有用…

Cadence Virtuoso IC617 系统内存清理

1、清空simelation和垃圾箱下的文件 2、在虚拟机磁盘路径下&#xff0c;例如/home下面输入 cat /dev/zero > zero.fill;sync;sleep 1;sync;rm -f zero.fill 3、在windows下winR ->cmd 找到VMware安装目录和系统存放目录 Microsoft Windows [版本 10.0.19045.4412] (c…

/etc/fstab、/etc/mtab 文件详解及永久挂载(文件系统、ISO镜像、文件网络共享)

/etc/mtab /etc/mtab 是当前的分区挂载情况&#xff0c;记录的是当前系统已挂载的分区。每次挂载/卸载分区时会更新 /etc/mtab 文件中的信息&#xff08;执行 mount 命令会改变 /etc/mtab 的信息&#xff09;。 文件样例 /etc/fstab 系统开机时会主动读取 /etc/fstab 这个文…

网络服务ftp实验

网络服务之ftp vsftpd的安装和配置 rpm -qc vsftpd #检查vsftpd安装包是否存在&#xff0c;存在即不需要安装 yum install -y vsftpd #yum 安装vsftpdcd /etc/vsftpd ls #切换到安装好vsftpd目录下查看文件cp vsftpd.conf vsftpd.conf.bak.20240604 #将vsftpd的…

数据库开发-Mysql03

目录 1. 多表查询 1.1 概述 1.1.1 数据准备 1.1.2 介绍 1.1.3 分类 1.2 内连接 1.3 外连接 1.4 子查询 1.4.1 介绍 1.4.2 标量子查询 1.4.3 列子查询 1.4.4 行子查询 1.4.5 表子查询 1.5 案例 2. 事务 2.1 介绍 2.2 操作 2.3 四大特性 3. 索引 3.1 介绍 3…

算法人生(18):从神经网络的“剪枝策略”看“怎么找回时间”

IT人的工作和生活难平衡这事&#xff0c;到底要怎么解决呢&#xff0c;让我们从神经网络的“剪枝策略”中找点灵感吧&#xff01; 剪枝策略是指训练和优化深度神经网络时采取的一种技术&#xff0c;从名字就知道&#xff0c;它就像修剪树木一样&#xff0c;去除不必要的枝叶&a…

《平渊》· 捌 —— 「庄子」山木篇中的 “空船效应”

《平渊》 捌 "若能虚己以游世&#xff0c;其孰能害之&#xff1f;" "方舟而济于河&#xff0c;有虚船来触舟&#xff0c;虽有惼心之人而不怒&#xff1b;有一人子其上&#xff0c;则呼张歙之&#xff1b;一呼而不闻&#xff0c;再呼而不闻&#xff0c;于是三呼邪…

Linux C语言学习:数据类型

一、 为什么要引入数据类型 • 计算机中每个字节都有一个地址&#xff08;类似门牌号&#xff09; • CPU通过 地址 来访问这个字节的空间 0x20001103 1 0 0 1 0 0 1 1 0x20001102 1 1 1 0 1 1 1 0 0x20001101 1 1 1 1 0 1 0 1 0x20001100 0 …

STM32 MDK Keil5软件调试功能使用(无需连接硬件)

MDK Keil5 在线仿真STM32&#xff08;无需连接硬件&#xff09; 首先点击工具栏的魔术棒配置一下&#xff1a;&#xff08;记得选择自己的STM32芯片类型&#xff09; 开启调试 使用逻辑分析仪查看IO输出 会打开这个界面&#xff0c;点击左边的setup按钮 会打开这个窗口&am…

Golang | Leetcode Golang题解之第119题杨辉三角II

题目&#xff1a; 题解&#xff1a; func getRow(rowIndex int) []int {row : make([]int, rowIndex1)row[0] 1for i : 1; i < rowIndex; i {row[i] row[i-1] * (rowIndex - i 1) / i}return row }

npm install pubsub-js报错的解决汇总

我在练习谷粒商城P83时&#xff0c;选择分类时触发向后端请求选择分类catId绑定的品牌数据&#xff0c;发现前端控制台报错&#xff1a; "PubSub is not definded",找不到pubsub。 因为缺少pubsub包&#xff0c;所以开始安装此包。 于是在网上一顿搜索猛如虎&…

理解NSCopying协议

NSCopying 协议用于让对象能够被复制。实现这个协议的类需要定义如何创建该对象的副本。这个副本是独立的&#xff0c;不会与原对象共享内存地址。 为什么需要 NSCopying 协议&#xff1f; 当你需要复制对象时&#xff0c;例如将对象存储到一个集合&#xff08;如数组、字典&…

如何规避亚马逊测评的风险?

国外的真实刷手和服务商的账号风险主要是表现在以下几点&#xff1a; 1. 账号资源的重复利用&#xff0c;下单的产品上一秒高端品牌&#xff0c;下一秒地摊货&#xff0c;对账号的标签定位不清晰 2. 留Review的时间周期长 3. 账号的质量会参差不齐&#xff0c;有的上不了评价…

INT202 例题

算法复杂度 O(n)&#xff1a;表示算法的渐进上界。如果一个算法的运行时间是O(n)&#xff0c;那么它的运行时间最多与输入规模n成正比。换句话说&#xff0c;当输入规模n增加时&#xff0c;算法的运行时间不会超过某个常数倍的n。比如&#xff0c;如果一个算法的时间复杂度是O(…

CHATGPT升级plus(已有账号前提下)

注册wildcard(虚拟卡) 注册号账号后先进行充值&#xff0c;充值后选择CHATGPT一键升级按照他的流程来即可 Wildcard网址&#xff1a;Wildcard跳转注册 填写邀请码充值时少两美金合计14&#xffe5; 邀请码&#xff1a;OL3QXTRH

Linux系统之mv命令的基本使用

Linux系统之mv命令的基本使用 一、mv命令介绍1. mv命令简介2. mv命令的使用结果 二、mv命令的使用帮助1. 在命令行的帮助信息2. mv常用选项 三、mv命令的基本使用1. 创建源目录和目标目录2. 新建测试文件3. 将源目录文件复制到目标目录4. 将文件进行改名5. 将目录的所有文件转移…

Kimichat使用案例007:用kimichat批量重命名txt文本文件

文章目录 一、介绍二、txt文件集三、Kimi操作内容四、Kimi输出内容一、介绍 一个文件夹中有很多个txt文本文件,需要全部进行重命名。 二、txt文件集 三、Kimi操作内容 可以在kimichat中输入提示词: 你是一个Python编程专家,要完成一个关于批量重命名txt文本文件的Python脚本…

提示词工程基础:定义与重要性

目录 一、引言二、提示词工程的定义1. 概念明晰2. 技术框架3. 功能作用 三、提示词工程的重要性1. 核心作用强调2. 提升效率与降低成本3. 推动技术发展与创新 四、提示词工程的组成部分1. 提示词设计2. 模型训练与调整3. 效果评估与优化 五、实际应用示例1. 虚拟助手2. 自动新闻…

Three.js中的Raycasting技术:实现3D场景交互事件的Raycaster详解

前言 在Web开发中&#xff0c;Three.js是一个极为强大的库&#xff0c;它让开发者能够轻松地在浏览器中创建和展示3D图形。随着3D技术在网页设计、游戏开发、数据可视化等领域的广泛应用&#xff0c;用户与3D场景的交互变得日益重要。而要实现这种交互&#xff0c;一个核心的技…