当前位置:   article > 正文

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

gateway记录请求响应日志

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

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

日志实体类

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

  1. @Data
  2. public class AccessLog {
  3.     /**用户编号**/
  4.     private Long userId;
  5.     /**路由**/
  6.     private String targetServer;
  7.     /**协议**/
  8.     private String schema;
  9.     
  10.     /**请求方法名**/
  11.     private String requestMethod;
  12.     
  13.     /**访问地址**/
  14.     private String requestUrl;
  15.     /**请求IP**/
  16.     private String clientIp;
  17.     /**查询参数**/
  18.     private MultiValueMap<StringString> queryParams;
  19.     
  20.     /**请求体**/
  21.     private String requestBody;
  22.     
  23.     /**请求头**/
  24.     private MultiValueMap<StringString> requestHeaders;
  25.      /**响应体**/
  26.     private String responseBody;
  27.     
  28.     /**响应头**/
  29.     private MultiValueMap<StringString> responseHeaders;
  30.     
  31.      /**响应结果**/
  32.     private HttpStatusCode httpStatusCode;
  33.     
  34.      /**开始请求时间**/
  35.     private LocalDateTime startTime;
  36.     
  37.     /**结束请求时间**/
  38.     private LocalDateTime endTime;
  39.     
  40.     /**执行时长,单位:毫秒**/
  41.     private Integer duration;
  42. }
网关日志过滤器

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

  1. @Component
  2. public class AccessLogFilter implements GlobalFilter, Ordered {
  3.     private final List<HttpMessageReader<?>> messageReaders = HandlerStrategies.withDefaults().messageReaders();
  4.     /**
  5.      * 打印日志
  6.      * @param accessLog 网关日志
  7.      */
  8.     private void writeAccessLog(AccessLog accessLog) {
  9.         log.info("----access---- : {}", JsonUtils.obj2StringPretty(accessLog));
  10.     }
  11.     /**
  12.      * 顺序必须是<-1,否则标准的NettyWriteResponseFilter将在您的过滤器得到一个被调用的机会之前发送响应
  13.      * 也就是说如果不小于 -1 ,将不会执行获取后端响应的逻辑
  14.      * @return
  15.      */
  16.     @Override
  17.     public int getOrder() {
  18.         return -100;
  19.     }
  20.     @Override
  21.     public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
  22.         // 将 Request 中可以直接获取到的参数,设置到网关日志
  23.         ServerHttpRequest request = exchange.getRequest();
  24.         AccessLog gatewayLog = new AccessLog();
  25.         gatewayLog.setTargetServer(WebUtils.getGatewayRoute(exchange).getId());
  26.         gatewayLog.setSchema(request.getURI().getScheme());
  27.         gatewayLog.setRequestMethod(request.getMethod().name());
  28.         gatewayLog.setRequestUrl(request.getURI().getRawPath());
  29.         gatewayLog.setQueryParams(request.getQueryParams());
  30.         gatewayLog.setRequestHeaders(request.getHeaders());
  31.         gatewayLog.setStartTime(LocalDateTime.now());
  32.         gatewayLog.setClientIp(WebUtils.getClientIP(exchange));
  33.         // 继续 filter 过滤
  34.         MediaType mediaType = request.getHeaders().getContentType();
  35.         if (MediaType.APPLICATION_FORM_URLENCODED.isCompatibleWith(mediaType)
  36.                 || MediaType.APPLICATION_JSON.isCompatibleWith(mediaType)) { // 适合 JSON 和 Form 提交的请求
  37.             return filterWithRequestBody(exchange, chain, gatewayLog);
  38.         }
  39.         return filterWithoutRequestBody(exchange, chain, gatewayLog);
  40.     }
  41.     /**
  42.      * 没有请求体的请求只需要记录日志
  43.      */
  44.     private Mono<Void> filterWithoutRequestBody(ServerWebExchange exchange, GatewayFilterChain chain, AccessLog accessLog) {
  45.         // 包装 Response,用于记录 Response Body
  46.         ServerHttpResponseDecorator decoratedResponse = recordResponseLog(exchange, accessLog);
  47.         return chain.filter(exchange.mutate().response(decoratedResponse).build())
  48.                 .then(Mono.fromRunnable(() -> writeAccessLog(accessLog)));
  49.     }
  50.     /**
  51.      * 需要读取请求体
  52.      * 参考 {@link ModifyRequestBodyGatewayFilterFactory} 实现
  53.      */
  54.     private Mono<Void> filterWithRequestBody(ServerWebExchange exchange, GatewayFilterChain chain, AccessLog gatewayLog) {
  55.         // 设置 Request Body 读取时,设置到网关日志
  56.         ServerRequest serverRequest = ServerRequest.create(exchange, messageReaders);
  57.         Mono<String> modifiedBody = serverRequest.bodyToMono(String.class).flatMap(body -> {
  58.             gatewayLog.setRequestBody(body);
  59.             return Mono.just(body);
  60.         });
  61.         // 通过 BodyInserter 插入 body(支持修改body), 避免 request body 只能获取一次
  62.         BodyInserter<Mono<String>, ReactiveHttpOutputMessage> bodyInserter = BodyInserters.fromPublisher(modifiedBody, String.class);
  63.         HttpHeaders headers = new HttpHeaders();
  64.         headers.putAll(exchange.getRequest().getHeaders());
  65.         // the new content type will be computed by bodyInserter
  66.         // and then set in the request decorator
  67.         headers.remove(HttpHeaders.CONTENT_LENGTH);
  68.         CachedBodyOutputMessage outputMessage = new CachedBodyOutputMessage(exchange, headers);
  69.         // 通过 BodyInserter 将 Request Body 写入到 CachedBodyOutputMessage 中
  70.         return bodyInserter.insert(outputMessage, new BodyInserterContext()).then(Mono.defer(() -> {
  71.             // 重新封装请求
  72.             ServerHttpRequest decoratedRequest = requestDecorate(exchange, headers, outputMessage);
  73.             // 记录响应日志
  74.             ServerHttpResponseDecorator decoratedResponse = recordResponseLog(exchange, gatewayLog);
  75.             // 记录普通的
  76.             return chain.filter(exchange.mutate().request(decoratedRequest).response(decoratedResponse).build())
  77.                     .then(Mono.fromRunnable(() -> writeAccessLog(gatewayLog))); // 打印日志
  78.         }));
  79.     }
  80.     /**
  81.      * 记录响应日志
  82.      * 通过 DataBufferFactory 解决响应体分段传输问题。
  83.      */
  84.     private ServerHttpResponseDecorator recordResponseLog(ServerWebExchange exchange, AccessLog accessLog) {
  85.         ServerHttpResponse response = exchange.getResponse();
  86.         return new ServerHttpResponseDecorator(response) {
  87.             @Override
  88.             public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
  89.                 if (body instanceof Flux) {
  90.                     DataBufferFactory bufferFactory = response.bufferFactory();
  91.                     // 计算执行时间
  92.                     accessLog.setEndTime(LocalDateTime.now());
  93.                     accessLog.setDuration((int) (LocalDateTimeUtil.between(accessLog.getStartTime(),
  94.                             accessLog.getEndTime()).toMillis()));
  95.                     accessLog.setResponseHeaders(response.getHeaders());
  96.                     accessLog.setHttpStatusCode(response.getStatusCode());
  97.                     // 获取响应类型,如果是 json 就打印
  98.                     String originalResponseContentType = exchange.getAttribute(ServerWebExchangeUtils.ORIGINAL_RESPONSE_CONTENT_TYPE_ATTR);
  99.                     if (StrUtil.isNotBlank(originalResponseContentType)
  100.                             && originalResponseContentType.contains("application/json")) {
  101.                         Flux<? extends DataBuffer> fluxBody = Flux.from(body);
  102.                         return super.writeWith(fluxBody.buffer().map(dataBuffers -> {
  103.                             // 设置 response body 到网关日志
  104.                             byte[] content = readContent(dataBuffers);
  105.                             String responseResult = new String(content, StandardCharsets.UTF_8);
  106.                             accessLog.setResponseBody(responseResult);
  107.                             // 响应
  108.                             return bufferFactory.wrap(content);
  109.                         }));
  110.                     }
  111.                 }
  112.                 // if body is not a flux. never got there.
  113.                 return super.writeWith(body);
  114.             }
  115.         };
  116.     }
  117.     /**
  118.      * 请求装饰器,支持重新计算 headers、body 缓存
  119.      *
  120.      * @param exchange 请求
  121.      * @param headers 请求头
  122.      * @param outputMessage body 缓存
  123.      * @return 请求装饰器
  124.      */
  125.     private ServerHttpRequestDecorator requestDecorate(ServerWebExchange exchange, HttpHeaders headers, CachedBodyOutputMessage outputMessage) {
  126.         return new ServerHttpRequestDecorator(exchange.getRequest()) {
  127.             @Override
  128.             public HttpHeaders getHeaders() {
  129.                 long contentLength = headers.getContentLength();
  130.                 HttpHeaders httpHeaders = new HttpHeaders();
  131.                 httpHeaders.putAll(super.getHeaders());
  132.                 if (contentLength > 0) {
  133.                     httpHeaders.setContentLength(contentLength);
  134.                 } else {
  135.                     // TODO: this causes a 'HTTP/1.1 411 Length Required' // on
  136.                     // httpbin.org
  137.                     httpHeaders.set(HttpHeaders.TRANSFER_ENCODING, "chunked");
  138.                 }
  139.                 return httpHeaders;
  140.             }
  141.             @Override
  142.             public Flux<DataBuffer> getBody() {
  143.                 return outputMessage.getBody();
  144.             }
  145.         };
  146.     }
  147.     /**
  148.      * 从dataBuffers中读取数据
  149.      * @author jam
  150.      * @date 2024/5/26 22:31
  151.      */
  152.     private byte[] readContent(List<? extends DataBuffer> dataBuffers) {
  153.         // 合并多个流集合,解决返回体分段传输
  154.         DataBufferFactory dataBufferFactory = new DefaultDataBufferFactory();
  155.         DataBuffer join = dataBufferFactory.join(dataBuffers);
  156.         byte[] content = new byte[join.readableByteCount()];
  157.         join.read(content);
  158.         // 释放掉内存
  159.         DataBufferUtils.release(join);
  160.         return content;
  161.     }
  162. }

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

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

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

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

实际效果

服务正常响应

图片

服务异常响应

本文内容由网友自发贡献,转载请注明出处:https://www.wpsshop.cn/w/weixin_40725706/article/detail/878087
推荐阅读
相关标签
  

闽ICP备14008679号