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.mzph.cn/pingmian/22128.shtml

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

相关文章

使用Java apache commons包五分钟搞定NCR解析(内附源码)

在网上看到很多关于解析NCR(Numeric Character Reference)字符串的java实现&#xff0c;核心都是通过自定义正则表达式来解析&#xff0c;其实org.apache.commons 已经为我们提供了jar包 解决该问题&#xff0c;非常的方便&#xff01;在这里我就来简单分享一下具体实现方法&am…

这就是英伟达 CEO 黄仁勋所说的人工智能“下一波浪潮”|TodayAI

在台湾一年一度的科技展 COMPUTEX 开幕前的周日&#xff0c;英伟达&#xff08;Nvidia&#xff09;首席执行官黄仁勋&#xff08;Jensen Huang&#xff09;表示&#xff0c;机器人和“理解物理定律的 AI”将成为下一波技术浪潮。他指出&#xff0c;英伟达目前正在推动生成式人工…

MyBatis核心对象

MyBatis核心类对象主要有俩个&#xff1a; 1&#xff1a;对相关配置文件信息进行封装的Configuration对象 2&#xff1a;用来执行数据库操作的Executor对象。 核心对象----存储类对象Configuration Configuration对象主要有三个作用&#xff1a; 1&#xff1a;封装MyBatis…

Pulsar 社区周报 | No.2024-05-30 | BIGO 百页小册《Apache Pulsar 调优指南》

“ 各位热爱 Pulsar 的小伙伴们&#xff0c;Pulsar 社区周报更新啦&#xff01;这里将记录 Pulsar 社区每周的重要更新&#xff0c;每周发布。 ” BIGO 百页小册《Apache Pulsar 调优指南》 Hi&#xff0c;Apache Pulsar 社区的小伙伴们&#xff0c;社区 2024 上半年度的有奖问…

AIGC和ChatGPT有什么区别?

AIGC和ChatGPT有什么区别? 首先先解释一下它们各自的概念 什么是AIGC AIGC&#xff0c;全称为Artificial Intelligence Generated Content&#xff0c;中文译为人工智能生成内容。这是一种利用人工智能技术自动生成内容的生产方式。例如&#xff0c;它可以创作出各种形式的内…

基于PHP+MySQL组合开发的同城便民小程序源码系统 房产出租+求职招聘+相亲交友 带完整的安装代码包以及搭建教程

系统概述 在当今信息化高速发展的时代&#xff0c;同城便民小程序已成为城市居民日常生活中不可或缺的一部分。为了满足广大用户的需求&#xff0c;小编给大家分享一款基于PHPMySQL组合开发的同城便民小程序源码系统。该系统集房产出租、求职招聘、相亲交友等多功能于一体&…

微信小程序使用echarts

思路 五个tab公用一个柱状图组件切换tab以及切换时间改变数据&#xff0c;传入子组件&#xff0c;子组件监听数据重新更新点击柱状图显示具体数值每个时间点有两个柱子&#xff08;高压和低压&#xff09;&#xff0c;柱状图显示高压的最大值到最小值的范围除了血压其余只有一…

Python采集数据处理:利用Pandas进行组排序和筛选

概述 在现代数据处理和分析中&#xff0c;网络爬虫技术变得越来越重要。通过网络爬虫&#xff0c;我们可以自动化地从网页上收集大量的数据。然而&#xff0c;如何高效地处理和筛选这些数据是一个关键问题。本文将介绍如何使用Python的Pandas库对采集到的数据进行组排序和筛选…

基于SpringBoot+Vue研究生志愿填报辅助系统设计和实现(源码+LW+调试文档+讲解等)

&#x1f497;博主介绍&#xff1a;✌全网粉丝1W,CSDN作者、博客专家、全栈领域优质创作者&#xff0c;博客之星、平台优质作者、专注于Java、小程序技术领域和毕业项目实战✌&#x1f497; &#x1f31f;文末获取源码数据库&#x1f31f; 感兴趣的可以先收藏起来&#xff0c;还…

mp公共字段自动注入

目录 一 什么是公共字段自动注入 二 使用mp实现公共字段自动注入 1.实现步骤 ①导入mp相关依赖 ② 在实体类上给相关字段加上 TableField()注解 ③自定义元数据对象处理器 2.实现原理 一 什么是公共字段自动注入 我们平时在执行更新或者是插入数据功能的时候&#xff0c;…

智绘“水蓝图”,宏电亮相第4届中国(山东)水利科技与生态建设博览会

5月23-25日&#xff0c;第4届中国&#xff08;山东&#xff09;水利科技与生态建设博览会在济南黄河国际会展中心成功召开。展会以“人水和谐&#xff0c;生态山东”为主题&#xff0c;围绕智慧水利建设、水环境治理、水生态保护等领域&#xff0c;展示当下水利行业的新技术、新…

红酒:如何正确地储存红酒

云仓酒庄雷盛红酒&#xff0c;以其优良的品质和丰富的口感&#xff0c;深受广大消费者的喜爱。然而&#xff0c;要想让这些美酒能够长时间保持良好的状态&#xff0c;正确的储存方式是必不可少的。下面&#xff0c;云仓酒庄雷盛红酒将为您详细介绍如何正确地储存红酒。 一、合适…

C语言笔记23 •文件操作•

1.为什么要使用文件&#xff1f; 文件&#xff0c;顾名思义就是存储我们所写在电脑上的文本内容。如果没有⽂件&#xff0c;我们写的程序的数据是存储在电脑的内存中&#xff0c;如果程序退出&#xff0c;内存回收&#xff0c;数据就丢失 了&#xff0c;等再次运⾏程序&#x…

AI工具:如何通过智能助手简化工作流程?

工欲善其事&#xff0c;必先利其器。 随着AI技术与各个行业或细分场景的深度融合&#xff0c;日常工作可使用的AI工具呈现出井喷式发展的趋势&#xff0c;AI工具的类别也从最初的AI文本生成、AI绘画工具&#xff0c;逐渐扩展到AI思维导图工具、AI流程图工具、AI生成PPT工具、AI…

【二叉树】Leetcode 103. 二叉树的锯齿形层序遍历【中等】

二叉树的锯齿形层序遍历 给你二叉树的根节点 root &#xff0c;返回其节点值的 锯齿形层序遍历 。&#xff08;即先从左往右&#xff0c;再从右往左进行下一层遍历&#xff0c;以此类推&#xff0c;层与层之间交替进行&#xff09;。 示例 1&#xff1a; 输入&#xff1a;roo…

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

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

产品经理的需求善变,利用规则引擎减少80%的需求变更成本

经常有技术团队的小伙伴抱怨最烦的就是产品经理发起需求变更&#xff0c;才做了三周&#xff0c;改了八回需求...... 其实这个故事&#xff0c;每天都在开发团队中上演&#xff0c;作为 IT的 leader 来透视这个问题&#xff0c;要辩证的看待&#xff0c;其实80% 的可能性都是如…

《对马岛之魂:导演剪辑版》新鲜出炉,AOC电竞显示器与你并肩作战!

超越PS版本的画面表现&#xff0c;AOC U27G3XM助你轻松拉满游戏体验&#xff01; 近日&#xff0c;《对马岛之魂&#xff1a;导演剪辑版》正式登陆PC平台。这款备受期待的作品不仅在战斗机制和故事内容上进行了创新&#xff0c;还引入了更高级的图形选项和更丰富的自定义设置。…

Android 绑定服务的5个问题。

1.android studio 目录结构改变了。为什么会报R 资源文件找不到。 在写项目的时候经常需要改到。 gradle文件里的域名名字要改变下。 2.Caused by: android.app.BackgroundServiceStartNotAllowedException: Not allowed to start service Intent { cmpcom.zjtzsw.sbkDevice/…

oracle 数字或者小数格式化

select trim(. from to_char(1,fm9999990.9999)),trim(. from to_char(0.1,fm9999990.9999)) from dual