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
     */
    @Override
    public int getOrder() {
        return -100;
    }

    @Override
    public 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 Body
        ServerHttpResponseDecorator 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 decorator
        headers.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) {

            @Override
            public 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()) {

            @Override
            public 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.org
                    httpHeaders.set(HttpHeaders.TRANSFER_ENCODING, "chunked");
                }
                return httpHeaders;
            }

            @Override
            public 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进行收集。

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

实际效果

服务正常响应

图片

服务异常响应

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

U-Net: Convolutional Networks for Biomedical Image Segmentation--论文笔记

U-Net: Convolutional Networks for Biomedical Image Segmentation 资料 1.代码地址 2.论文地址 https://arxiv.org/pdf/1505.04597 3.数据集地址 论文摘要的翻译 人们普遍认为&#xff0c;深度网络的成功训练需要数千个带注释的训练样本。在本文中&#xff0c;我们提出…

“GPT-4o深度解析:技术演进、能力评估与个人体验综述“

文章目录 每日一句正能量前言对比分析模型架构性能应用场景用户体验技术创新社区和生态系统总结 技术能力语言生成能力语言理解能力技术实现总结 个人感受关于GPT-4o的假设性观点&#xff1a;关于当前语言模型的一般性观点&#xff1a; 后记 每日一句正能量 又回到了原点&#…

【前端】display:none和visibility:hidden两者的区别

&#x1f60e; 作者介绍&#xff1a;我是程序员洲洲&#xff0c;一个热爱写作的非著名程序员。CSDN全栈优质领域创作者、华为云博客社区云享专家、阿里云博客社区专家博主。公粽号&#xff1a;洲与AI。 &#x1f913; 欢迎大家关注我的专栏&#xff0c;我将分享Web前后端开发、…

电机行业MES生产管理系统--助力电机企业数字化转型

电机行业 MES 系统是一个综合生产管理系统&#xff0c; 融合了工厂企业必要的销售、 物 流和制造管理等全公司基础业务以及生产计划和现场监测管理。 一、传统机电行业的管理难题&#xff1a; 1、 产品标准化程度较低&#xff0c; 制造工艺复杂&#xff0c; 生产周期较长&#…

day50 动态规划 198.打家劫舍 213.打家劫舍II 337.打家劫舍III

198.打家劫舍 当前房屋偷与不偷取决于 前一个房屋和前两个房屋是否被偷了。 动规五部曲 1.确定dp数组&#xff08;dp table&#xff09;以及下标的含义 dp[i]&#xff1a;考虑下标i&#xff08;包括i&#xff09;以内的房屋&#xff0c;最多可以偷窃的金额为dp[i]。 2.确…

结构体+结构体内存对齐+结构体实现位段

结构体内存对齐实现位段 一.结构体1.结构体的声明2.结构体变量成员访问操作符3.结构体传参4.匿名结构体5.结构的自引用 二.结构体内存对齐1.对齐规则2.为什么存在内存对齐&#xff1f;3.修改默认对齐数 三.结构体实现位段1.什么是位段2.位段的内存分配3.位段的跨平台问题4.位段…

SELinux深度解析:安全增强型Linux的探索与应用(上)

&#x1f407;明明跟你说过&#xff1a;个人主页 &#x1f3c5;个人专栏&#xff1a;《Linux &#xff1a;从菜鸟到飞鸟的逆袭》&#x1f3c5; &#x1f516;行路有良友&#xff0c;便是天堂&#x1f516; 目录 一、引言 1、SELinux概述 2、SELinux诞生背景 3、SELinux …

Django 视图探秘:FBV与CBV注册方式的异同,揭秘as_view()的执行魔法

文章目录 一、FBV、CBV注册方式及其区别FBVCBV 二、as_view()函数查看对应的view函数具体内容&#xff0c;最终返回的是dispatch方法查看dispatch方法 一、FBV、CBV注册方式及其区别 FBV FBV&#xff1a;path(index/,views.index) 通过调用函数方式&#xff0c;views.index是一…

打印机扫描工具V2.1发布

打印机扫描工具V2.1发布 从打印机扫描工具发布1.4版本以来&#xff0c;大家反馈了一些问题&#xff0c;目前就比较集中的问题&#xff0c;做了一些优化&#xff0c;做了一些大的调整&#xff0c;发布了2.1版本。 优化问题&#xff1a; 进一步优化安装包太大问题&#xff0c;…

上海亚商投顾:深成指、创业板指均涨超1%,电力股午后集体走强

上海亚商投顾前言&#xff1a;无惧大盘涨跌&#xff0c;解密龙虎榜资金&#xff0c;跟踪一线游资和机构资金动向&#xff0c;识别短期热点和强势个股。 一.市场情绪 沪指昨日低开后震荡反弹&#xff0c;深成指、创业板指均涨超1%&#xff0c;黄白二线依旧分化。电力、电网股午…

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

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

挑战你的数据结构技能:复习题来袭【6】

1. (单选题)设无向图的顶点个数为n,则该图最多有&#xff08;&#xff09;条边 A. n-1 B. n(n-1)/2 C. n(n1)/2 D. 0 答案&#xff1a;B 分析&#xff1a; 2. (单选题)含有n个顶点的连通无向图,其边的个数至少为()。 A. n-1 B. n C. n1 D. nlog2n 答案&#xff1a;A…

10 数据封装与层次对应关系

一、TCP/IP模型 二、封装与解封装 &#xff08;一&#xff09;数据的封装 &#xff08;二&#xff09;数据的解封装 三、协议、数据与设备 &#xff08;一&#xff09;对应层次协议 结构协议应用层HTTP / FTP / TFTP / SMTP / SNMP/ DNS传输层TCP / UDP网络层ICMP / IGMP / …

使用记事本或者写字板打开中文乱码问题

最近下载一个开源的公共的文件&#xff0c;下载下来是xml格式的文本文件&#xff0c;然后我尝试打开&#xff0c;使用记事本打开文件&#xff0c;内容显示正常&#xff0c;但是因为是xml文件&#xff0c;使用记事本打开的时候没有换行&#xff0c;不方便看&#xff0c;然后就使…

信息系统项目管理师0143:过程概述(9项目范围管理—9.2项目范围管理过程—9.2.1过程概述)

点击查看专栏目录 文章目录 9.2 项目范围管理过程9.2.1 过程概述 9.2 项目范围管理过程 9.2.1 过程概述 项目范围管理过程包括&#xff1a; 规划范围管理&#xff1a;为了记录如何定义、确认和控制项目范围及产品范围&#xff0c;创建范围管理计划。收集需求&#xff1a;为了…

文章自动排版

文字太多了不想看怎么办&#xff1f;想快速提取并罗列文章的重点要如何操作&#xff1f;今天给大家介绍一下如何把复杂的文章总结为一个个观点 使用说明 打开智游剪辑&#xff08;zyjj.cc&#xff09;&#xff0c;搜索文字排版 我们输入要排版的文章&#xff0c;点击立即生成就…

心链9----组队功能开发以及请求参数包装类和包装类实现

心链 — 伙伴匹配系统 组队功能开发 需求分析 理想的应用场景 我要跟别人一起参加竞赛或者做项目&#xff0c;可以发起队伍或者加入别人的队伍 用户可以 创建 一个队伍&#xff0c;设置队伍的人数、队伍名称&#xff08;标题&#xff09;、描述、超时时间 P0 队长、剩余的人数…

安防综合管理系统EasyCVR视频汇聚平台GA/T 1400协议中的关键消息交互示例

在当今的信息化时代&#xff0c;公共安全防范日益成为保障社会和谐稳定的关键。视频监控系统作为现代安全防范的重要手段&#xff0c;正不断在公安、交通、城市管理等领域发挥着越来越重要的作用。而GA/T 1400协议视图库&#xff0c;作为公安视频图像信息应用系统的标准&#x…

使用 TinyEngine 低代码引擎实现三方物料集成

本文由体验技术团队 TinyEngine 项目成员炽凌创作&#xff0c;欢迎大家实操体验&#xff0c;本体验内容基于 TinyEngine 低代码引擎提供的环境&#xff0c;介绍了如何通过 TinyEngine 低代码引擎实现三方物料集成&#xff0c;帮助开发者快速开发。 知识背景 1.1 TinyEngine 低…

江苏省汽车及零部件产业协作配套对接会在苏州举行

5月28日&#xff0c;江苏省汽车及零部件产业协作配套对接会暨“百场万企”大中小企业融通对接活动在苏州举办。本次活动以“深化整零协作&#xff0c;促进大中小企业融通发展”为主题&#xff0c;由江苏省工业和信息化厅、中国中检所属中国汽车工程研究院股份有限公司&#xff…