当前位置: 首页 > 科技观察

SpringCloudAlibaba实战中的SpringCloudGateway请求响应日志

时间:2023-03-18 00:42:16 科技观察

< titlesplit >转载请联系JAVA日知录公众号。请求响应日志是日常开发、调试、定位问题的重要手段。在微服务中引入SpringCloudGateway后,我们希望在网关层统一收集日志。本节内容将实现以下两个功能:获取请求的输入输出参数,封装成自定义日志,将日志发送到MongoDB存储,获取输入输出参数。首先我们定义一个日志体@DatapublicclassGatewayLog{/**访问实例*/privateStringtargetServer;/**请求路径*/privateStringrequestPath;/**请求方法*/privateStringrequestMethod;/**协议*/privateStringschema;/**请求体*/privateStringrequestBody;/**响应体*/privateStringresponseData;/**请求ip*/privateStringip;/**请求时间*/privateDaterequestTime;/**响应时间*/privateDateresponseTime;/**执行时间*/privatelongexecuteTime;}【重点】在网关上定义日志过滤器获取输入输出参数/***日志过滤器用于记录日志*@authorjianzh5*@date2020/3/2417:17*/@Slf4j@ComponentpublicclassAccessLogFilterimplementsGlobalFilter,Ordered{@AutowiredprivateAccessLogServiceaccessLogService;privatefinalList>messageReaders=HandlerStrategies.withDefaults(Readers());@OverridepublicintgetOrder(){return-100;}@Override@SuppressWarnings("unchecked")publicMonofilter(ServerWebExchangeexchange,GatewayFilterChainchain){ServerHttpRequestrequest=exchange.getRequest();//请求路径stringrequestPath=request.getPath().pathWithinApplication().value();Routeroute=getGatewayRoute(交换);StringipAddress=WebUtils.getServerHttpRequestIpAddress(请求);GatewayLoggatewayLog=newGatewayLog();gatewayLog.setSchema(request.getURI().getScheme());gatewayLog.setRequestMethod(request.getMethodValue());gatewayLog.setRequestPath(requestPath);gatewayLog.setTargetServer(route.getId());gatewayLog.setRequestTime(newDate());gatewayLog.setIp(ipAddress);MediaTypemediaType=request.getHeaders().getContentType();if(MediaType.APPLICATION_FORM_URLENCODED.isCompatibleWith(mediaType)||MediaType.APPLICATION_JSON.isCompatibleWith(mediaType)){returnwriteBodyLog(exchange,chain,gatewayLog);}else{returnwriteBasicLog(exchange,chain,gatewayLog);}}privateMonowriteBasicLog(ServerWebExchangeexchange,GatewayFilterChainchain,GatewayLogaccessLog){StringBuilderbuilder=newStringBuilder();MultiValueMapqueryParams=exchange.getRequest().getQueryParams();for(Map.Entry>entry:queryParams.entrySet()){builder.append(entry.getKey()).append("=").append(StringUtils.join(entry.getValue(),","));}accessLog.setRequestBody(builder.toString());//获取响应体ServerHttpResponseDecoratordecoratedResponse=recordResponseLog(exchange,accessLog);returnchain.filter(exchange.mutate().response(decoratedResponse).build()).then(Mono.fromRunnable(()->{//打印日志writeAccessLog(accessLog);}));}/***解决requestbody只能读取一次问题,*参考:org.springframework.cloud.gateway.filter.factory.rewrite.ModifyRequestBodyGatewayFilterFactory*@paramexchange*@paramchain*@paramgatewayLog*@return*/@SuppressWarnings("unchecked")privateMonowriteBodyLog(ServerWebExchangeexchange,GatewayFilterChainchain,GatewayLoggatewayLog){ServerRequestserverRequest=ServerRequest.create(交换,messageReaders);MonomodifiedBody=serverRequest.bodyToMono(String.class).flatMap(body->{gatewayLog.setRequestBody(body);returnMono.just(body);});//通过BodyInserter插入body(支持修改body),避免requestbody只能获取一次BodyInserterbodyInserter=BodyInserters.fromPublisher(modifiedBody,String.class);HttpHeadersheaders=newHttpHeaders();headers.putAll(exchange.getRequest().getHeaders());//新内容类型将由bodyInserter计算//然后设置在requestdecoratorheaders.remove(HttpHeaders.CONTENT_LENGTH);CachedBodyOutputMessageoutputMessage=newCachedBodyOutputMessage(exchange,headers);returnbodyInserter.insert(outputMessage,newBodyInser()).then(Mono.defer(()->{//重新封装请求ServerHttpRequestdecoratedRequest=requestDecorate(exchange,headers,outputMessage);//记录响应日志ServerHttpResponseDecoratordecoratedResponse=recordResponseLog(exchange,gatewayLog);//记录正常的returnchain.filter(exchange.mutate().request(decoratedRequest).response(decoratedResponse).build()).then(Mono.fromRunnable(()->{//printlogwriteAccessLog(gatewayLog);}));}));}/***打印日志*@authorjavadaily*@date2021/3/2414:53*@paramgatewayLog网关日志*/privatevoidwriteAccessLog(GatewayLoggatewayLog){log.info(gatewayLog.toString());}privateRoutegetGatewayRoute(ServerWebExchangeexchange){returnexchange.getAttribute(ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR);}/***请申请装饰器,重新计算headers*@paramexchange*@paramheaders*@paramoutputMessage*@return*/privateServerHttpRequestDecoratorrequestDecorate(ServerWebExchangeexchange,HttpHeadersheaders,CachedBodyOutputMessageoutputMessage){returnnewServerHttpRequestDecorator(exchange.getRequest()){@OverridepublicHttpHeadersgetHeaders(){longcontentLength=headers.getContentLength();HttpHeadershttpHeaders=newHttpHeaders();httpHeaders.putAll(super.getHeaders());if(contentLength>0){httpHeaders.setContentLength(contentLength);}else{//TODO:thiscausesa'HTTP/1.1411LengthRequired'//on//httpbin.orghttpHeaders.set(HttpHeaders.TRANSFER_ENCODING,"chunked");}returnhttpHeaders;}@OverridepublicFluxgetBody(){returnoutputMessage.getBody();}};}/***Recordresponselog*解决通过DataBufferFactory分段传输responsebody的问题*/privateServerHttpResponseDecoratorrecordResponseLog(ServerWebExchangeexchange,GatewayLoggatewayLog){ServerHttpResponseresponse=exchange.getResponse();DataBufferFactorybufferFactory=response.bufferFactory();returnnewServerHttpResponseDecorator(响应){@OverridepublicMonowriteWithbody){instendofbody(DataBufferfluxBody=Flux.from(body);returnsuper.writeWith(fluxBody.buffer().map(dataBuffers->{//合并多个流集合,解决返回body分段传输DataBufferFactorydataBufferFactory=newDefaultDataBufferFactory();DataBufferjoin=dataBufferFactory.join(dataBuffers);byte[]content=newbyte[join.readableByteCount()];join.read(content);//释放内存DataBufferUtils.release(join);StringresponseResult=newString(content,StandardCharsets.UTF_8);gatewayLog.setResponseData(responseResult);returnbufferFactory.wrap(content);})));}}//ifbodyisnotaflux.nevergotthere.returnsuper.writeWith(body);}};}}代码较长,建议直接复制到编辑器中,只需要注意以下关键点:值getOrder()方法返回的必须<-1,“否则标准的NettyWriteResponseFilter会在你的过滤器有机会被调用之前发送响应,即获取后端响应参数的方法不会被执行。”通过以上两步,我们已经可以获取请求的输入输出参数,在writeAccessLog()中输出到日志文件中,可以发送请求在Postman中观察日志存储日志。如果需要持久化日志供以后检索,可以考虑将日志存储在MongoDB中。实现过程非常简单。(安装MongoDB可以参考这篇文章:实战|MongoDB安装与配置)引入MongoDBorg.springframework.bootspring-boot-starter-data-mongodb-reactive由于网关是基于webflux的,所以我们需要选择reactive版本。在GatewayLog上添加相应的注解@Data@DocumentpublicclassGatewayLog{@IdprivateStringid;...}建立AccessLogRepository@RepositorypublicinterfaceAccessLogRepositoryextendsReactiveMongoRepository{}建立ServicepublicinterfaceAccessLogService{/***保存AccessLog*@paramgatewayLogRequest响应日志*@return响应日志*/MonosaveAccessLog(GatewayLoggatewayLog);}创建一个实现类@ServicepublicclassAccessLogServiceImplimplementsAccessLogService{@AutowiredprivateAccessLogRepositoryaccessLogRepository;@OverridepublicMonosaveAccessLog(GatewayLoggatewayLog){returnaccessLogRepository.insert(gatewayLogs)对应Monco的配置中心data:mongodb:host:xxx。xx.x.xxport:27017database:accesslogusername:accesslogpassword:xxxxxx执行请求,打开MongoDB客户端,查看以上日志结果,希望对你有帮助!