|
|
|
|
公众号矩阵

SpringCloud Alibaba实战之SpringCloud Gateway 请求响应日志

请求响应日志是日常开发调试定位问题的重要手段,在微服务中引入SpringCloud Gateway后我们希望在网关层统一进行日志的收集。

作者: 单一色调 来源:JAVA日知录|2021-03-26 06:01

本文转载自微信公众号「JAVA日知录」,作者单一色调。转载本文请联系JAVA日知录公众号。

请求响应日志是日常开发调试定位问题的重要手段,在微服务中引入SpringCloud Gateway后我们希望在网关层统一进行日志的收集。

本节内容将实现以下两个功能:

获取请求的输入输出参数,封装成自定义日志

将日志发送到MongoDB进行存储

获取输入输出参数

首先我们先定义一个日志体

  1. @Data 
  2. public class GatewayLog { 
  3.     /**访问实例*/ 
  4.     private String targetServer; 
  5.     /**请求路径*/ 
  6.     private String requestPath; 
  7.     /**请求方法*/ 
  8.     private String requestMethod; 
  9.     /**协议 */ 
  10.     private String schema
  11.     /**请求体*/ 
  12.     private String requestBody; 
  13.     /**响应体*/ 
  14.     private String responseData; 
  15.     /**请求ip*/ 
  16.     private String ip; 
  17.  /**请求时间*/ 
  18.     private Date requestTime; 
  19.  /**响应时间*/ 
  20.     private Date responseTime; 
  21.     /**执行时间*/ 
  22.     private long executeTime; 

【关键】在网关定义日志过滤器,获取输入输出参数

  1. /** 
  2.  * 日志过滤器,用于记录日志 
  3.  * @author jianzh5 
  4.  * @date 2020/3/24 17:17 
  5.  */ 
  6. @Slf4j 
  7. @Component 
  8. public class AccessLogFilter implements GlobalFilter, Ordered { 
  9.     @Autowired 
  10.     private AccessLogService accessLogService; 
  11.  
  12.     private final List<HttpMessageReader<?>> messageReaders = HandlerStrategies.withDefaults().messageReaders(); 
  13.  
  14.     @Override 
  15.     public int getOrder() { 
  16.         return -100; 
  17.     } 
  18.  
  19.     @Override 
  20.     @SuppressWarnings("unchecked"
  21.     public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { 
  22.  
  23.         ServerHttpRequest request = exchange.getRequest(); 
  24.  
  25.         // 请求路径 
  26.         String requestPath = request.getPath().pathWithinApplication().value(); 
  27.  
  28.         Route route = getGatewayRoute(exchange); 
  29.  
  30.  
  31.         String ipAddress = WebUtils.getServerHttpRequestIpAddress(request); 
  32.  
  33.         GatewayLog gatewayLog = new GatewayLog(); 
  34.         gatewayLog.setSchema(request.getURI().getScheme()); 
  35.         gatewayLog.setRequestMethod(request.getMethodValue()); 
  36.         gatewayLog.setRequestPath(requestPath); 
  37.         gatewayLog.setTargetServer(route.getId()); 
  38.         gatewayLog.setRequestTime(new Date()); 
  39.         gatewayLog.setIp(ipAddress); 
  40.  
  41.         MediaType mediaType = request.getHeaders().getContentType(); 
  42.  
  43.         if(MediaType.APPLICATION_FORM_URLENCODED.isCompatibleWith(mediaType) || MediaType.APPLICATION_JSON.isCompatibleWith(mediaType)){ 
  44.             return writeBodyLog(exchange, chain, gatewayLog); 
  45.         }else
  46.             return writeBasicLog(exchange, chain, gatewayLog); 
  47.         } 
  48.     } 
  49.  
  50.     private Mono<Void> writeBasicLog(ServerWebExchange exchange, GatewayFilterChain chain, GatewayLog accessLog) { 
  51.         StringBuilder builder = new StringBuilder(); 
  52.         MultiValueMap<String, String> queryParams = exchange.getRequest().getQueryParams(); 
  53.         for (Map.Entry<String, List<String>> entry : queryParams.entrySet()) { 
  54.             builder.append(entry.getKey()).append("=").append(StringUtils.join(entry.getValue(), ",")); 
  55.         } 
  56.         accessLog.setRequestBody(builder.toString()); 
  57.  
  58.         //获取响应体 
  59.         ServerHttpResponseDecorator decoratedResponse = recordResponseLog(exchange, accessLog); 
  60.  
  61.         return chain.filter(exchange.mutate().response(decoratedResponse).build()) 
  62.                 .then(Mono.fromRunnable(() -> { 
  63.                     // 打印日志 
  64.                     writeAccessLog(accessLog); 
  65.                 })); 
  66.     } 
  67.  
  68.  
  69.     /** 
  70.      * 解决 request body 只能读取一次问题, 
  71.      * 参考: org.springframework.cloud.gateway.filter.factory.rewrite.ModifyRequestBodyGatewayFilterFactory 
  72.      * @param exchange 
  73.      * @param chain 
  74.      * @param gatewayLog 
  75.      * @return 
  76.      */ 
  77.     @SuppressWarnings("unchecked"
  78.     private Mono writeBodyLog(ServerWebExchange exchange, GatewayFilterChain chain, GatewayLog gatewayLog) { 
  79.         ServerRequest serverRequest = ServerRequest.create(exchange,messageReaders); 
  80.  
  81.         Mono<String> modifiedBody = serverRequest.bodyToMono(String.class) 
  82.                 .flatMap(body ->{ 
  83.                     gatewayLog.setRequestBody(body); 
  84.                     return Mono.just(body); 
  85.                 }); 
  86.  
  87.         // 通过 BodyInserter 插入 body(支持修改body), 避免 request body 只能获取一次 
  88.         BodyInserter bodyInserter = BodyInserters.fromPublisher(modifiedBody, String.class); 
  89.         HttpHeaders headers = new HttpHeaders(); 
  90.         headers.putAll(exchange.getRequest().getHeaders()); 
  91.         // the new content type will be computed by bodyInserter 
  92.         // and then set in the request decorator 
  93.         headers.remove(HttpHeaders.CONTENT_LENGTH); 
  94.  
  95.         CachedBodyOutputMessage outputMessage = new CachedBodyOutputMessage(exchange, headers); 
  96.  
  97.         return bodyInserter.insert(outputMessage,new BodyInserterContext()) 
  98.                 .then(Mono.defer(() -> { 
  99.                     // 重新封装请求 
  100.                     ServerHttpRequest decoratedRequest = requestDecorate(exchange, headers, outputMessage); 
  101.  
  102.                     // 记录响应日志 
  103.                     ServerHttpResponseDecorator decoratedResponse = recordResponseLog(exchange, gatewayLog); 
  104.  
  105.                     // 记录普通的 
  106.                     return chain.filter(exchange.mutate().request(decoratedRequest).response(decoratedResponse).build()) 
  107.                             .then(Mono.fromRunnable(() -> { 
  108.                                 // 打印日志 
  109.                                 writeAccessLog(gatewayLog); 
  110.                             })); 
  111.                 })); 
  112.     } 
  113.  
  114.     /** 
  115.      * 打印日志 
  116.      * @author javadaily 
  117.      * @date 2021/3/24 14:53 
  118.      * @param gatewayLog 网关日志 
  119.      */ 
  120.     private void writeAccessLog(GatewayLog gatewayLog) { 
  121.         log.info(gatewayLog.toString());   
  122.     } 
  123.  
  124.  
  125.  
  126.     private Route getGatewayRoute(ServerWebExchange exchange) { 
  127.         return exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR); 
  128.     } 
  129.  
  130.  
  131.     /** 
  132.      * 请求装饰器,重新计算 headers 
  133.      * @param exchange 
  134.      * @param headers 
  135.      * @param outputMessage 
  136.      * @return 
  137.      */ 
  138.     private ServerHttpRequestDecorator requestDecorate(ServerWebExchange exchange, HttpHeaders headers, 
  139.                                                        CachedBodyOutputMessage outputMessage) { 
  140.         return new ServerHttpRequestDecorator(exchange.getRequest()) { 
  141.             @Override 
  142.             public HttpHeaders getHeaders() { 
  143.                 long contentLength = headers.getContentLength(); 
  144.                 HttpHeaders httpHeaders = new HttpHeaders(); 
  145.                 httpHeaders.putAll(super.getHeaders()); 
  146.                 if (contentLength > 0) { 
  147.                     httpHeaders.setContentLength(contentLength); 
  148.                 } else { 
  149.                     // TODO: this causes a 'HTTP/1.1 411 Length Required' // on 
  150.                     // httpbin.org 
  151.                     httpHeaders.set(HttpHeaders.TRANSFER_ENCODING, "chunked"); 
  152.                 } 
  153.                 return httpHeaders; 
  154.             } 
  155.  
  156.             @Override 
  157.             public Flux<DataBuffer> getBody() { 
  158.                 return outputMessage.getBody(); 
  159.             } 
  160.         }; 
  161.     } 
  162.  
  163.  
  164.     /** 
  165.      * 记录响应日志 
  166.      * 通过 DataBufferFactory 解决响应体分段传输问题。 
  167.      */ 
  168.     private ServerHttpResponseDecorator recordResponseLog(ServerWebExchange exchange, GatewayLog gatewayLog) { 
  169.         ServerHttpResponse response = exchange.getResponse(); 
  170.         DataBufferFactory bufferFactory = response.bufferFactory(); 
  171.  
  172.         return new ServerHttpResponseDecorator(response) { 
  173.             @Override 
  174.             public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) { 
  175.                 if (body instanceof Flux) { 
  176.                     Date responseTime = new Date(); 
  177.                     gatewayLog.setResponseTime(responseTime); 
  178.                     // 计算执行时间 
  179.                     long executeTime = (responseTime.getTime() - gatewayLog.getRequestTime().getTime()); 
  180.  
  181.                     gatewayLog.setExecuteTime(executeTime); 
  182.  
  183.                     // 获取响应类型,如果是 json 就打印 
  184.                     String originalResponseContentType = exchange.getAttribute(ServerWebExchangeUtils.ORIGINAL_RESPONSE_CONTENT_TYPE_ATTR); 
  185.  
  186.  
  187.                     if (ObjectUtil.equal(this.getStatusCode(), HttpStatus.OK) 
  188.                             && StringUtil.isNotBlank(originalResponseContentType) 
  189.                             && originalResponseContentType.contains("application/json")) { 
  190.  
  191.                         Flux<? extends DataBuffer> fluxBody = Flux.from(body); 
  192.                         return super.writeWith(fluxBody.buffer().map(dataBuffers -> { 
  193.  
  194.                             // 合并多个流集合,解决返回体分段传输 
  195.                             DataBufferFactory dataBufferFactory = new DefaultDataBufferFactory(); 
  196.                             DataBuffer join = dataBufferFactory.join(dataBuffers); 
  197.                             byte[] content = new byte[join.readableByteCount()]; 
  198.                             join.read(content); 
  199.  
  200.                             // 释放掉内存 
  201.                             DataBufferUtils.release(join); 
  202.                             String responseResult = new String(content, StandardCharsets.UTF_8); 
  203.  
  204.  
  205.  
  206.                             gatewayLog.setResponseData(responseResult); 
  207.  
  208.                             return bufferFactory.wrap(content); 
  209.                         })); 
  210.                     } 
  211.                 } 
  212.                 // if body is not a flux. never got there. 
  213.                 return super.writeWith(body); 
  214.             } 
  215.         }; 
  216.     } 

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

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

通过上面的两步我们已经可以获取到请求的输入输出参数了,在 writeAccessLog()中将其输出到了日志文件,大家可以在Postman发送请求观察日志。

存储日志

如果需要将日志持久化方便后期检索的话可以考虑将日志存储在MongoDB中,实现过程很简单。(安装MongoDB可以参考这篇文章:实战|MongoDB的安装配置)

  • 引入MongoDB
  1. <dependency> 
  2.     <groupId>org.springframework.boot</groupId> 
  3.     <artifactId>spring-boot-starter-data-mongodb-reactive</artifactId> 
  4. </dependency> 

由于gateway是基于webflux,所以我们需要选择reactive版本。

  • 在GatewayLog上添加对应的注解
  1. @Data 
  2. @Document 
  3. public class GatewayLog { 
  4.     @Id 
  5.     private String id; 
  6.  ... 
  • 建立AccessLogRepository
  1. @Repository 
  2. public interface AccessLogRepository extends ReactiveMongoRepository<GatewayLog,String> { 
  3.    
  • 建立Service
  1. public interface AccessLogService { 
  2.  
  3.     /** 
  4.      * 保存AccessLog 
  5.      * @param gatewayLog 请求响应日志 
  6.      * @return 响应日志 
  7.      */ 
  8.     Mono<GatewayLog> saveAccessLog(GatewayLog gatewayLog); 
  9.  
  • 建立实现类
  1. @Service 
  2. public class AccessLogServiceImpl implements AccessLogService { 
  3.     @Autowired 
  4.     private AccessLogRepository accessLogRepository; 
  5.  
  6.     @Override 
  7.     public Mono<GatewayLog> saveAccessLog(GatewayLog gatewayLog) { 
  8.         return accessLogRepository.insert(gatewayLog); 
  9.     } 
  • 在Nacos配置中心添加MongoDB对应配置
  1. spring: 
  2.   data: 
  3.     mongodb: 
  4.       host: xxx.xx.x.xx 
  5.       port: 27017 
  6.       database: accesslog 
  7.       username: accesslog 
  8.       password: xxxxxx 

执行请求,打开MongoDB客户端,查看日志结果

以上,希望对你有所帮助!

【编辑推荐】

  1. 6期大数据之日志系统Flume技术教程(讲理论还讲实操)
  2. 企业项目2-Linux用户行为日志审计实战视频课程(老男孩全新运维进阶系列L021)
  3. Elastic Stack(ELK日志收集)7.3 基础与实践
  4. 2021年优秀日志可视化工具
  5. Redis日志篇:无畏宕机快速恢复的杀手锏
【责任编辑:武晓燕 TEL:(010)68476606】

点赞 0
分享:
大家都在看
猜你喜欢

订阅专栏+更多

数据湖与数据仓库的分析实践攻略

数据湖与数据仓库的分析实践攻略

助力现代化数据管理:数据湖与数据仓库的分析实践攻略
共3章 | 创世达人

7人订阅学习

云原生架构实践

云原生架构实践

新技术引领移动互联网进入急速赛道
共3章 | KaliArch

36人订阅学习

数据中心和VPDN网络建设案例

数据中心和VPDN网络建设案例

漫画+案例
共20章 | 捷哥CCIE

230人订阅学习

订阅51CTO邮刊

点击这里查看样刊

订阅51CTO邮刊

51CTO服务号

51CTO官微