SpringCloud Gateway之请求应答日志打印方式

目录
  • Gateway请求应答日志打印
    • 第一步
    • 第二步
  • Gateway全局请求日志打印
    • 把请求体的数据存入exchange
    • 编写全局日志拦截器代码
    • 在代码中配置全局拦截器

Gateway请求应答日志打印

请求应答日志时在日常开发调试问题的重要手段之一,那么如何基于Spring Cloud Gateway做呢,请看我上代码。

第一步

创建RecorderServerHttpRequestDecorator,缓存请求参数,解决body只能读一次问题。

public class RecorderServerHttpRequestDecorator extends ServerHttpRequestDecorator { 
    private final List<DataBuffer> dataBuffers = new ArrayList<>(); 
    public RecorderServerHttpRequestDecorator(ServerHttpRequest delegate) {
        super(delegate);
        super.getBody().map(dataBuffer -> {
            dataBuffers.add(dataBuffer);
            return dataBuffer;
        }).subscribe();
    }
 
    @Override
    public Flux<DataBuffer> getBody() {
        return copy();
    }
 
    private Flux<DataBuffer> copy() {
        return Flux.fromIterable(dataBuffers)
                .map(buf -> buf.factory().wrap(buf.asByteBuffer()));
    }  
}

第二步

创建访问日志全局过滤器,然后在此过滤器进行日志构造。

@Slf4j
public class AccessLogGlobalFilter implements GlobalFilter , Ordered { 
    private static final String REQUEST_PREFIX = "Request Info [ "; 
    private static final String REQUEST_TAIL = " ]"; 
    private static final String RESPONSE_PREFIX = "Response Info [ "; 
    private static final String RESPONSE_TAIL = " ]"; 
    private StringBuilder normalMsg = new StringBuilder();
 
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        RecorderServerHttpRequestDecorator requestDecorator = new RecorderServerHttpRequestDecorator(request);
        InetSocketAddress address = requestDecorator.getRemoteAddress();
        HttpMethod method = requestDecorator.getMethod();
        URI url = requestDecorator.getURI();
        HttpHeaders headers = requestDecorator.getHeaders();
        Flux<DataBuffer> body = requestDecorator.getBody();
        //读取requestBody传参
        AtomicReference<String> requestBody = new AtomicReference<>("");
        body.subscribe(buffer -> {
            CharBuffer charBuffer = StandardCharsets.UTF_8.decode(buffer.asByteBuffer());
            requestBody.set(charBuffer.toString());
        });
        String requestParams = requestBody.get();
        normalMsg.append(REQUEST_PREFIX);
        normalMsg.append(";header=").append(headers);
        normalMsg.append(";params=").append(requestParams);
        normalMsg.append(";address=").append(address.getHostName() + address.getPort());
        normalMsg.append(";method=").append(method.name());
        normalMsg.append(";url=").append(url.getPath());
        normalMsg.append(REQUEST_TAIL);
 
        ServerHttpResponse response = exchange.getResponse();
 
        DataBufferFactory bufferFactory = response.bufferFactory();
        normalMsg.append(RESPONSE_PREFIX);
        ServerHttpResponseDecorator decoratedResponse = new ServerHttpResponseDecorator(response) {
            @Override
            public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
                if (body instanceof Flux) {
                    Flux<? extends DataBuffer> fluxBody = (Flux<? extends DataBuffer>) body;
                    return super.writeWith(fluxBody.map(dataBuffer -> {
                        // probably should reuse buffers
                        byte[] content = new byte[dataBuffer.readableByteCount()];
                        dataBuffer.read(content);
                        String responseResult = new String(content, Charset.forName("UTF-8"));
                        normalMsg.append("status=").append(this.getStatusCode());
                        normalMsg.append(";header=").append(this.getHeaders());
                        normalMsg.append(";responseResult=").append(responseResult);
                        normalMsg.append(RESPONSE_TAIL);
                        log.info(normalMsg.toString());
                        return bufferFactory.wrap(content);
                    }));
                }
                return super.writeWith(body); // if body is not a flux. never got there.
            }
        };
 
        return chain.filter(exchange.mutate().request(requestDecorator).response(decoratedResponse).build());
    }
 
    @Override
    public int getOrder() {
        return -2;
    } 
}

最后结果:

Request Info [ ;header={cache-control=[no-cache], Postman-Token=[790488a5-a284-4a0e-968f-1b588cb26688], Content-Type=[application/json], User-Agent=[PostmanRuntime/3.0.9], Accept=[*/*], Host=[localhost:8084], cookie=[JSESSIONID=E161AC22204E626FBE6E96EE7B62EE70], accept-encoding=[gzip, deflate], content-length=[13], Connection=[keep-alive]};params={"name":"ss"};address=0:0:0:0:0:0:0:159621;method=POST;url=/account/testBody ]Response Info [ ;status=200;header={Content-Type=[text/plain;charset=UTF-8], Content-Length=[41], Date=[Mon, 18 Mar 2019 08:21:57 GMT]};responseResult=account hellowordAccountEntity{name='ss'} ]

以上代码即可完成请求应答日志打印功能。

Gateway全局请求日志打印

实现GlobalFilter则所有该自定义Filter会对所有的路由生效。

把请求体的数据存入exchange

便于打印日志时获取

package com.qykj.gateway.filter;
import com.qykj.gateway.ConstantFilter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
/**
 * @calssName AppCacheRequestBodyFilter
 * @Description 将 request body 中的内容 copy 一份,记录到 exchange 的一个自定义属性中
 * @Author jiangshaoneng
 * @DATE 2020/9/27 14:42
 */
public class GlobalCacheRequestBodyFilter implements GlobalFilter, Ordered {
    private static final Logger logger = LoggerFactory.getLogger(GlobalCacheRequestBodyFilter.class);
    private int order;
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        //logger.info("GlobalCacheRequestBodyFilter ...");
        // 将 request body 中的内容 copy 一份,记录到 exchange 的一个自定义属性中
        Object cachedRequestBodyObject = exchange.getAttributeOrDefault(ConstantFilter.CACHED_REQUEST_BODY_OBJECT_KEY, null);
        // 如果已经缓存过,略过
        if (cachedRequestBodyObject != null) {
            return chain.filter(exchange);
        }
        // 如果没有缓存过,获取字节数组存入 exchange 的自定义属性中
        return DataBufferUtils.join(exchange.getRequest().getBody())
                .map(dataBuffer -> {
                    byte[] bytes = new byte[dataBuffer.readableByteCount()];
                    dataBuffer.read(bytes);
                    DataBufferUtils.release(dataBuffer);
                    return bytes;
                }).defaultIfEmpty(new byte[0])
                .doOnNext(bytes -> exchange.getAttributes().put(ConstantFilter.CACHED_REQUEST_BODY_OBJECT_KEY, bytes))
                .then(chain.filter(exchange));
    }
    @Override
    public int getOrder() {
        return this.order;
    }
    public GlobalCacheRequestBodyFilter(int order){
        this.order = order;
    }
}

编写全局日志拦截器代码

/**
 * @calssName LogFilter
 * @Description 全局日志打印,请求日志以及返回日志,并在返回结果日志中添加请求时间
 * @Author jiangshaoneng
 * @DATE 2020/9/25 14:54
 */
public class GlobalLogFilter implements GlobalFilter, Ordered {
    private static final Logger logger = LoggerFactory.getLogger(GlobalLogFilter.class);
    private int order;
    private static final String REQUEST_PREFIX = "\n--------------------------------- Request  Info -----------------------------";
    private static final String REQUEST_TAIL   = "\n-----------------------------------------------------------------------------";
    private static final String RESPONSE_PREFIX = "\n--------------------------------- Response Info -----------------------------";
    private static final String RESPONSE_TAIL   = "\n-------------------------------------------------------------------------->>>";
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        long start = DateUtil.getCurrentTime();
        StringBuilder reqMsg = new StringBuilder();
        StringBuilder resMsg = new StringBuilder();
        // 获取请求信息
        ServerHttpRequest request = exchange.getRequest();
        InetSocketAddress address = request.getRemoteAddress();
        String method = request.getMethodValue();
        URI uri = request.getURI();
        HttpHeaders headers = request.getHeaders();
        // 获取请求body
        Object cachedRequestBodyObject = exchange.getAttributeOrDefault(ConstantFilter.CACHED_REQUEST_BODY_OBJECT_KEY, null);
        byte[] body = (byte[]) cachedRequestBodyObject;
        String params = new String(body);
        // 获取请求query
        Map queryMap = request.getQueryParams();
        String query = JSON.toJSONString(queryMap);
        // 拼接请求日志
        reqMsg.append(REQUEST_PREFIX);
        reqMsg.append("\n header=").append(headers);
        reqMsg.append("\n query=").append(query);
        reqMsg.append("\n params=").append(params);
        reqMsg.append("\n address=").append(address.getHostName()).append(address.getPort());
        reqMsg.append("\n method=").append(method);
        reqMsg.append("\n url=").append(uri.getPath());
        reqMsg.append(REQUEST_TAIL);
        logger.info(reqMsg.toString()); // 打印入参日志
        ServerHttpResponse response = exchange.getResponse();
        DataBufferFactory bufferFactory = response.bufferFactory();
        resMsg.append(RESPONSE_PREFIX);
        ServerHttpResponseDecorator decoratedResponse = new ServerHttpResponseDecorator(response) {
            @Override
            public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
                if (body instanceof Flux) {
                    Flux<? extends DataBuffer> fluxBody = (Flux<? extends DataBuffer>) body;
                    return super.writeWith(fluxBody.map(dataBuffer -> {
                        byte[] content = new byte[dataBuffer.readableByteCount()];
                        dataBuffer.read(content);
                        String responseResult = new String(content, Charset.forName("UTF-8"));
                        resMsg.append("\n status=").append(this.getStatusCode());
                        resMsg.append("\n header=").append(this.getHeaders());
                        resMsg.append("\n responseResult=").append(responseResult);
                        resMsg.append(RESPONSE_TAIL);
                        // 计算请求时间
                        long end = DateUtil.getCurrentTime();
                        long time = end - start;
                        resMsg.append("耗时ms:").append(time);
                        logger.info(resMsg.toString()); // 打印结果日志
                        return bufferFactory.wrap(content);
                    }));
                }
                return super.writeWith(body);
            }
        };
        return chain.filter(exchange.mutate().response(decoratedResponse).build());
    }
    @Override
    public int getOrder() {
        return this.order;
    }
    public GlobalLogFilter(int order){
        this.order = order;
    }
}

在代码中配置全局拦截器

/**
 * @calssName GatewayConfig
 * @Description 网关配置
 * @Author jiangshaoneng
 * @DATE 2020/9/25 14:26
 */
@Configuration
public class GatewayConfig {
    /**
     * 全局过滤器:请求日志打印
     */
    @Bean
    public GlobalLogFilter globalLogFilter(){
        // 该值越小权重却大,所以应根据具体项目配置。需要尽早的获取到参数,一般会是一个比较小的值
        return new GlobalLogFilter(-20); 
    }
    
    // 其他的路由配置 ... 
}

以上为个人经验,希望能给大家一个参考,也希望大家多多支持我们。

(0)

相关推荐

  • Spring Cloud Gateway全局通用异常处理的实现

    为什么需要全局异常处理 在传统 Spring Boot 应用中, 我们 @ControllerAdvice 来处理全局的异常,进行统一包装返回 // 摘至 spring cloud alibaba console 模块处理 @ControllerAdvice public class ConsoleExceptionHandler { @ExceptionHandler(AccessException.class) private ResponseEntity<String> handleAc

  • spring cloud gateway中如何读取请求参数

    spring cloud gateway读取请求参数 1. 我的版本: spring-cloud:Hoxton.RELEASE spring-boot:2.2.2.RELEASE spring-cloud-starter-gateway 2. 请求日志 import lombok.extern.slf4j.Slf4j; import org.springframework.cloud.gateway.filter.GatewayFilterChain; import org.springfram

  • Spring Cloud Gateway 记录请求应答数据日志操作

    我就废话不多说了,大家还是直接看代码吧~ public class GatewayContext { public static final String CACHE_GATEWAY_CONTEXT = "cacheGatewayContext"; /** * cache json body */ private String cacheBody; /** * cache formdata */ private MultiValueMap<String, String> f

  • Spring Cloud gateway 网关如何拦截Post请求日志

    gateway版本是 2.0.1 1.pom结构 (部分内部项目依赖已经隐藏) <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> <dependency> <groupId>org.s

  • SpringCloud Gateway之请求应答日志打印方式

    目录 Gateway请求应答日志打印 第一步 第二步 Gateway全局请求日志打印 把请求体的数据存入exchange 编写全局日志拦截器代码 在代码中配置全局拦截器 Gateway请求应答日志打印 请求应答日志时在日常开发调试问题的重要手段之一,那么如何基于Spring Cloud Gateway做呢,请看我上代码. 第一步 创建RecorderServerHttpRequestDecorator,缓存请求参数,解决body只能读一次问题. public class RecorderServ

  • Log4j关闭Spring和Hibernate日志打印方式

    目录 Log4j关闭Spring和Hibernate日志打印 Log4j关闭Hibernate日志输出 Log4j关闭Spring和Hibernate日志打印 这里所说的关闭不是全部不打印,只需要修改一下log级别就好了. 普通的info信息其实我们是不看的,所以直接设置成warn或error级别就好了. #close java-jdbc log out log4j.logger.java.sql.Connection=ERROR log4j.logger.java.sql.Statement=

  • SpringMVC框架中使用Filter实现请求日志打印方式

    目录 查找资料后确定两种技术方案 1. 使用AOP对所有Controller的方法进行环绕通知处理: 2. 使用Filter拦截所有的Request和Response,并获取body. 最后选择了第二种方式,具体实现记录如下. 具体实现 日志记录过滤器 public class RequestFilter implements Filter{private static final String LOG_FORMATTER_IN = "请求路径:{%s},请求方法:{%s},参数:{%s},来源

  • SpringCloud Gateway读取Request Body方式

    目录 Gateway读取RequestBody 分析ReadBodyPredicateFactory 配置ReadBodyPredicateFactory 编写自定义GatewayFilterFactory 完整的yml配置 Gateway自定义filter获取body的数据为空 首先创建一个全局过滤器把body中的数据缓存起来 在自定义的过滤器中尝试获取body中的数据 解析body的工具类 Gateway读取Request Body 我们使用SpringCloud Gateway做微服务网关

  • SpringCloud gateway request的body验证或修改方式

    SpringCloud gateway request的body验证或修改 后续版本新增了以下过滤器 org.springframework.cloud.gateway.filter.headers.RemoveHopByHopHeadersFilter 默认会把以下头部移除(暂不了解这做法的目的) - connection - keep-alive - te - transfer-encoding - trailer - proxy-authorization - proxy-authenti

  • Nginx设置日志打印post请求参数的方法

    [前言] 我们项目的短信功能是接第三方,原来对接第三方给我们回执确认请求是get请求我们在排查问题的时候可以通过nginx的日志拿到对方给我们请求的参数:最近我们换了另外一家第三方,新的第三方给我们的确认请求是post,遇到问题排查,发现nginx没有打印具体参数,于是查阅一些资料和运维一起做了实验和线上调整,调整后我们可以拿到请求参数,更方便我们排查问题: [Nginx设置打印post请求参数]            一.Nginx配置文件(nginx.conf)设置打印post请求参数:在h

  • SpringCloud Gateway HttpWebHandlerAdapter链路调用请求流程介绍

    目录 前言 web容器 前言 上一节我们说到从HttpWebHandlerAdapter的handle方法说起到DispatcherHandler的调用流程 那么HttpWebHandlerAdapter的handle方法是从哪里来调用的呢? 我们可以找下看哪些类使用了HttpHandler 通过这些类的名字,我们可以发现,HttpHandler 往下就是具体的 Web 容器了,也就是说有具体的 Web 容器来接受请求,然后通过调用 HttpWebHandlerAdapter#handler 来

  • 详解SpringCloud Gateway 2020.0.2最新版

    简述 官网:https://spring.io/projects/spring-cloud-gateway GitHub地址:https://github.com/spring-cloud/spring-cloud-gateway 本文编写自2021年4月7日,当前SpringCloud最新版本为2020.0.2版本 本文使用版本为 SpringCloud 版本2020.0.2 spring-cloud-starter-gateway版本3.0.2 spring-boot-starter版本2.

  • 浅谈springcloud gateway 连接保活问题

    项目中使用了springcloud gateway作为网关,上游与负载均衡服务器连接. 近期通过监控系统观察,发现网关与上游负载均衡服务器保持的TCP连接有300+,初步怀疑是调用方未释放连接 用如下方法进行分析: 1)周期性采集当前建立的连接及端口数据 首先是每隔10分钟连续采集2两个小时,发现在两个小时之内新出现的端口不到12个,再逐步缩短采样周期,到最后每秒采集一次,分析发现每秒种建立一个连接,同时关闭一个连接,当仍存在300+连接,这些连接对应的端口称为不活跃端口,记录下这300+不活跃

随机推荐