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进行收集。
在实际项目中,网关日志量一般会非常大,不建议使用数据库进行存储。
实际效果
服务正常响应
服务异常响应
相关文章:

SpringCloud 微服务中网关如何记录请求响应日志?
在基于SpringCloud开发的微服务中,我们一般会选择在网关层记录请求和响应日志,并将其收集到ELK中用作查询和分析。 今天我们就来看看如何实现此功能。 日志实体类 首先我们在网关中定义一个日志实体,用于组装日志对象 Data public class …...

【运维项目经历|028】Cobbler自动化部署平台构建项目
🍁博主简介: 🏅云计算领域优质创作者 🏅2022年CSDN新星计划python赛道第一名 🏅2022年CSDN原力计划优质作者 🏅阿里云ACE认证高级工程师 🏅阿里云开发者社区专…...
“物联网安全:万物互联背景下的隐私保护与数据安全策略“
在物联网(IoT)时代,随着智能设备的普及和万物互联的加速,隐私保护与数据安全成为了亟待解决的关键问题。以下是一些重要的隐私保护与数据安全策略,以确保在万物互联背景下信息的安全: 1. 加强设备安全&…...

LeetCode216组合总和3
题目描述 找出所有相加之和为 n 的 k 个数的组合,且满足下列条件:只使用数字1到9。每个数字 最多使用一次。返回 所有可能的有效组合的列表 。该列表不能包含相同的组合两次,组合可以以任何顺序返回。 解析 递归加剪枝,搜索长度达…...

微软找腾讯接盘,Windows直接安装手机APP体验起飞了
熟悉微软的同学都知道微软有个传统艺能——什么好用砍什么。 比如 Win10 砍掉了还算方便的小娜,推出 Win11 砍掉了 Win10 上面好用的磁贴功能等。 上一秒用户还在夸奖点赞。 下一秒就给你砍掉,顺带还塞一堆 BUG 给你。 但没办法,PC 近乎垄断…...

【Springcloud微服务】MybatisPlus下篇
🔥 本文由 程序喵正在路上 原创,CSDN首发! 💖 系列专栏:Springcloud微服务 🌠 首发时间:2024年6月4日 🦋 欢迎关注🖱点赞👍收藏🌟留言ὃ…...

i18n-demo
一、demo 1、资源文件准备 如我需要对menu、logMsg内容做国际化。 resources下放各个语言文件,直接放resources下都行。我是新建了一个myi18n文件夹, (1)然后在myi18n上点击New--Resource Bundle (2)在…...
[Leetcode] 0-1背包和完全背包
46. 携带研究材料 纯01背包(非应用):只能选择一次物品 dp[j]:容量为j的背包所能装的最大容量 容量需要倒序 416. 分割等和子集 能否装满 dp[j]:容量为j的背包所能装的最大容量 1049. 最后一块石头的重量 II 尽可…...

自定义类型:联合体和枚举
1. 联合体类型的声明 2. 联合体的特点 3. 联合体大小的计算 4. 枚举类型的声明 5. 枚举类型的优点 6. 枚举类型的使用 欢迎关注 熬夜学编程 创作不易,请多多支持 感谢大家的阅读、点赞、收藏和关注 如有问题,欢迎指正 1. 联合体 1.1 联合体类型的声…...

【Cityengine】Cityengine生产带纹理的建筑模型导入UE4/UE5(下)
【Cityengine】Cityengine生产带纹理的建筑模型导入UE4/UE5(下) 一、导出数据(2022中文版案例)二、安装datasmith插件三、导入数据四、检查导入材质是否正常五、编辑替换材质六、安装模型编辑插件七、编辑替换建筑规则 一、导出数…...

详解51种企业应用架构模式
导读:企业应用包括哪些?它们又分别有哪些架构模式?世界著名软件开发大师Martin Fowler给你答案 01、什么是企业应用 我的职业生涯专注于企业应用,因此,这里所谈及的模式也都是关于企业应用的。(企业应用还…...

【十年java搬砖路】Jumpserver docker版安装及配置Ldap登陆认证
Jumpserver docker 安装启动教程 拉取镜像 docker pull JumpServer启动进行前确保有Redis 和Mysql 创建jumperServer数据库 在MYSQL上执行 创建数据库 登陆MYSQL mysql -u root -p 创建Jumperserveri库 create database jumpserver default charset utf8mb4;可以为jumperSe…...

C\C++内存管理(未完结)
文章目录 一.C\C内存分布二.C语言中动态内存管理方式:malloc/calloc/realloc/free三.C内存管理方式3.1.new/delete操作内置类型3.2.new和delete操作自定义类型 四.operator new与operator delete函数(重要点进行讲解)4.1. operator new与oper…...
一个小时搞定JAVA面向对象(5)——抽象与接口
文章目录 抽象抽象的注意事项static\final\private是否可以修饰抽象方法继承和抽象知识点回顾 接口接口实现总结抽象方法默认方法静态方法成员变量接口的特点接口和抽象类的区别 抽象 关键字: abstract 抽象方法: 修饰符 abstract 返回值类型 方法名(参数); 抽象类: public a…...
图像关键特征描述方法-小目标
图像关键特征描述方法主要包括以下几种: SIFT(尺度不变特征变换): SIFT是一种广泛使用的特征描述方法,它通过尺度空间和梯度方向直方图来描述图像中的关键点。SIFT特征描述具有尺度不变性和旋转不变性,对于光照和视角变化也具有一定的鲁棒性。 SURF(加速稳健特征): SURF…...

【qt15】windeployqt 安装依赖
debug模式vs可以使用qt插件新建qt文件 D:\Qt15\5.15.2\msvc2019\bin\windeployqt.exe Warning: Cannot find Visual Studio installation directory, VCINSTALLDIR is not set.D:\Qt15\5.15.2\msvc2019\bin\windeployqt.exe .\filecopier.exeWindows PowerShell Copyright (C) …...

DETR论文重点
DETR就是 DEtection TRansformer 的缩写。 论文原名:End-to-End Object Detection with Transoformers。 重点有两个:端到端、Transformer结构 论文概述 注意:斜体的文字为论文原文,其他部分内容则是为增进理解而做的解释。 …...

slf4j等多个jar包冲突绑定的排查方法使用IDEA的maven help解决
1.安装 2.使用maven help解决,找到对应包存在的冲突 使用exclude直接解决即可...
MySQL主从的延迟怎么解决呢?
以下是一些减少或解决MySQL主从延迟的策略: 优化查询和索引: 确保所有的查询都经过优化,以减少主服务器上的负载。使用合适的索引来加速查询速度,减少锁的时间。 分散复制负载: 使用多个从服务器分散读取负载。使用并…...

【一百】【算法分析与设计】N皇后问题常规解法+位运算解法
N皇后问题 链接:登录—专业IT笔试面试备考平台_牛客网 来源:牛客网 题目描述 给出一个nnn\times nnn的国际象棋棋盘,你需要在棋盘中摆放nnn个皇后,使得任意两个皇后之间不能互相攻击。具体来说,不能存在两个皇后位于同…...

智慧医疗能源事业线深度画像分析(上)
引言 医疗行业作为现代社会的关键基础设施,其能源消耗与环境影响正日益受到关注。随着全球"双碳"目标的推进和可持续发展理念的深入,智慧医疗能源事业线应运而生,致力于通过创新技术与管理方案,重构医疗领域的能源使用模式。这一事业线融合了能源管理、可持续发…...
前端倒计时误差!
提示:记录工作中遇到的需求及解决办法 文章目录 前言一、误差从何而来?二、五大解决方案1. 动态校准法(基础版)2. Web Worker 计时3. 服务器时间同步4. Performance API 高精度计时5. 页面可见性API优化三、生产环境最佳实践四、终极解决方案架构前言 前几天听说公司某个项…...
mongodb源码分析session执行handleRequest命令find过程
mongo/transport/service_state_machine.cpp已经分析startSession创建ASIOSession过程,并且验证connection是否超过限制ASIOSession和connection是循环接受客户端命令,把数据流转换成Message,状态转变流程是:State::Created 》 St…...
服务器硬防的应用场景都有哪些?
服务器硬防是指一种通过硬件设备层面的安全措施来防御服务器系统受到网络攻击的方式,避免服务器受到各种恶意攻击和网络威胁,那么,服务器硬防通常都会应用在哪些场景当中呢? 硬防服务器中一般会配备入侵检测系统和预防系统&#x…...

学习STC51单片机31(芯片为STC89C52RCRC)OLED显示屏1
每日一言 生活的美好,总是藏在那些你咬牙坚持的日子里。 硬件:OLED 以后要用到OLED的时候找到这个文件 OLED的设备地址 SSD1306"SSD" 是品牌缩写,"1306" 是产品编号。 驱动 OLED 屏幕的 IIC 总线数据传输格式 示意图 …...

令牌桶 滑动窗口->限流 分布式信号量->限并发的原理 lua脚本分析介绍
文章目录 前言限流限制并发的实际理解限流令牌桶代码实现结果分析令牌桶lua的模拟实现原理总结: 滑动窗口代码实现结果分析lua脚本原理解析 限并发分布式信号量代码实现结果分析lua脚本实现原理 双注解去实现限流 并发结果分析: 实际业务去理解体会统一注…...
Device Mapper 机制
Device Mapper 机制详解 Device Mapper(简称 DM)是 Linux 内核中的一套通用块设备映射框架,为 LVM、加密磁盘、RAID 等提供底层支持。本文将详细介绍 Device Mapper 的原理、实现、内核配置、常用工具、操作测试流程,并配以详细的…...
Mysql8 忘记密码重置,以及问题解决
1.使用免密登录 找到配置MySQL文件,我的文件路径是/etc/mysql/my.cnf,有的人的是/etc/mysql/mysql.cnf 在里最后加入 skip-grant-tables重启MySQL服务 service mysql restartShutting down MySQL… SUCCESS! Starting MySQL… SUCCESS! 重启成功 2.登…...

脑机新手指南(七):OpenBCI_GUI:从环境搭建到数据可视化(上)
一、OpenBCI_GUI 项目概述 (一)项目背景与目标 OpenBCI 是一个开源的脑电信号采集硬件平台,其配套的 OpenBCI_GUI 则是专为该硬件设计的图形化界面工具。对于研究人员、开发者和学生而言,首次接触 OpenBCI 设备时,往…...
Python网页自动化Selenium中文文档
1. 安装 1.1. 安装 Selenium Python bindings 提供了一个简单的API,让你使用Selenium WebDriver来编写功能/校验测试。 通过Selenium Python的API,你可以非常直观的使用Selenium WebDriver的所有功能。 Selenium Python bindings 使用非常简洁方便的A…...