天天看點

SpringCloud Gateway 請求響應日志

請求響應日志是日常開發調試定位問題的重要手段,在微服務中引入SpringCloud Gateway後我們希望在網關層統一進行日志的收集。

本節内容将實作以下兩個功能:

擷取請求的輸入輸出參數,封裝成自定義日志

将日志發送到MongoDB進行存儲

擷取輸入輸出參數

首先我們先定義一個日志體

@Data

public class GatewayLog {

/*通路執行個體/

private String targetServer;

/*請求路徑/

private String requestPath;

/*請求方法/

private String requestMethod;

/**協定 */

private String schema;

/*請求體/

private String requestBody;

/*響應體/

private String responseData;

/*請求ip/

private String ip;

/*請求時間/

private Date requestTime;

/響應時間/

private Date responseTime;

/執行時間/

private long executeTime;

}

【關鍵】在網關定義日志過濾器,擷取輸入輸出參數

/

  • 日志過濾器,用于記錄日志
  • @author
  • @date

    */

    @Slf4j

    @Component

    public class AccessLogFilter implements GlobalFilter, Ordered {

    @Autowired

    private AccessLogService accessLogService;

    private final List<HttpMessageReader<?>> messageReaders = HandlerStrategies.withDefaults().messageReaders();

    @Override

    public int getOrder() {

    return -100;

    @SuppressWarnings(“unchecked”)

    public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) {

    ServerHttpRequest request = exchange.getRequest();
    
     // 請求路徑
     String requestPath = request.getPath().pathWithinApplication().value();
    
     Route route = getGatewayRoute(exchange);
    
    
     String ipAddress = WebUtils.getServerHttpRequestIpAddress(request);
    
     GatewayLog gatewayLog = new GatewayLog();
     gatewayLog.setSchema(request.getURI().getScheme());
     gatewayLog.setRequestMethod(request.getMethodValue());
     gatewayLog.setRequestPath(requestPath);
     gatewayLog.setTargetServer(route.getId());
     gatewayLog.setRequestTime(new Date());
     gatewayLog.setIp(ipAddress);
    
     MediaType mediaType = request.getHeaders().getContentType();
    
     if(MediaType.APPLICATION_FORM_URLENCODED.isCompatibleWith(mediaType) || MediaType.APPLICATION_JSON.isCompatibleWith(mediaType)){
         return writeBodyLog(exchange, chain, gatewayLog);
     }else{
         return writeBasicLog(exchange, chain, gatewayLog);
     }
               

    }

    private Mono writeBasicLog(ServerWebExchange exchange, GatewayFilterChain chain, GatewayLog accessLog) {

    StringBuilder builder = new StringBuilder();

    MultiValueMap<String, String> queryParams = exchange.getRequest().getQueryParams();

    for (Map.Entry<String, List> entry : queryParams.entrySet()) {

    builder.append(entry.getKey()).append("=").append(StringUtils.join(entry.getValue(), “,”));

    accessLog.setRequestBody(builder.toString());

    //擷取響應體
     ServerHttpResponseDecorator decoratedResponse = recordResponseLog(exchange, accessLog);
    
     return chain.filter(exchange.mutate().response(decoratedResponse).build())
             .then(Mono.fromRunnable(() -> {
                 // 列印日志
                 writeAccessLog(accessLog);
             }));
               
    /**
    • 解決 request body 隻能讀取一次問題,
    • 參考: org.springframework.cloud.gateway.filter.factory.rewrite.ModifyRequestBodyGatewayFilterFactory
    • @param exchange
    • @param chain
    • @param gatewayLog
    • @return

      private Mono writeBodyLog(ServerWebExchange exchange, GatewayFilterChain chain, GatewayLog gatewayLog) {

      ServerRequest serverRequest = ServerRequest.create(exchange,messageReaders);

      Mono modifiedBody = serverRequest.bodyToMono(String.class)

      .flatMap(body ->{

      gatewayLog.setRequestBody(body);

      return Mono.just(body);

      });

      // 通過 BodyInserter 插入 body(支援修改body), 避免 request body 隻能擷取一次

      BodyInserter 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);

      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);
                       }));
           }));
                 
    • 列印日志
    • @param gatewayLog 網關日志

      private void writeAccessLog(GatewayLog gatewayLog) {

      log.info(gatewayLog.toString());

    private Route getGatewayRoute(ServerWebExchange exchange) {

    return exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR);

    • 請求裝飾器,重新計算 headers
    • @param headers
    • @param outputMessage
    • 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();
       }
                 
      };
    • 記錄響應日志
    • 通過 DataBufferFactory 解決響應體分段傳輸問題。

      private ServerHttpResponseDecorator recordResponseLog(ServerWebExchange exchange, GatewayLog gatewayLog) {

      ServerHttpResponse response = exchange.getResponse();

      DataBufferFactory bufferFactory = response.bufferFactory();

      return new ServerHttpResponseDecorator(response) {

      public Mono writeWith(Publisher<? extends DataBuffer> body) {

      if (body instanceof Flux) {

      Date responseTime = new Date();

      gatewayLog.setResponseTime(responseTime);

      // 計算執行時間

      long executeTime = (responseTime.getTime() - gatewayLog.getRequestTime().getTime());

      gatewayLog.setExecuteTime(executeTime);
      
               // 擷取響應類型,如果是 json 就列印
               String originalResponseContentType = exchange.getAttribute(ServerWebExchangeUtils.ORIGINAL_RESPONSE_CONTENT_TYPE_ATTR);
      
      
               if (ObjectUtil.equal(this.getStatusCode(), HttpStatus.OK)
                       && StringUtil.isNotBlank(originalResponseContentType)
                       && originalResponseContentType.contains("application/json")) {
      
                   Flux<? extends DataBuffer> fluxBody = Flux.from(body);
                   return super.writeWith(fluxBody.buffer().map(dataBuffers -> {
      
                       // 合并多個流集合,解決傳回體分段傳輸
                       DataBufferFactory dataBufferFactory = new DefaultDataBufferFactory();
                       DataBuffer join = dataBufferFactory.join(dataBuffers);
                       byte[] content = new byte[join.readableByteCount()];
                       join.read(content);
      
                       // 釋放掉記憶體
                       DataBufferUtils.release(join);
                       String responseResult = new String(content, StandardCharsets.UTF_8);
      
      
      
                       gatewayLog.setResponseData(responseResult);
      
                       return bufferFactory.wrap(content);
                   }));
               }
           }
           // if body is not a flux. never got there.
           return super.writeWith(body);
       }
                 
      代碼較長建議直接拷貝到編輯器,隻要注意下面一個關鍵點:

getOrder()方法傳回的值必須要<-1,「否則标準的NettyWriteResponseFilter将在您的過濾器被調用的機會之前發送響應,即不會執行擷取後端響應參數的方法」

通過上面的兩步我們已經可以擷取到請求的輸入輸出參數了,在 writeAccessLog()中将其輸出到了日志檔案,大家可以在Postman發送請求觀察日志。

存儲日志

如果需要将日志持久化友善後期檢索的話可以考慮将日志存儲在MongoDB中,實作過程很簡單。(安裝MongoDB可以參考這篇文章:實戰|MongoDB的安裝配置)

引入MongoDB

org.springframework.boot

spring-boot-starter-data-mongodb-reactive

由于gateway是基于webflux,是以我們需要選擇reactive版本。

在GatewayLog上添加對應的注解

@Document

@Id

private String id;

建立AccessLogRepository

@Repository

public interface AccessLogRepository extends ReactiveMongoRepository<GatewayLog,String> {

/**
 * 儲存AccessLog
 * @param gatewayLog 請求響應日志
 * @return 響應日志
 */
Mono<GatewayLog> saveAccessLog(GatewayLog gatewayLog);
           
@Override
public Mono<GatewayLog> saveAccessLog(GatewayLog gatewayLog) {
    return accessLogRepository.insert(gatewayLog);
}