【问题标题】:how to log Spring 5 WebClient call如何记录 Spring 5 WebClient 调用
【发布时间】:2018-02-19 15:41:17
【问题描述】:

我正在尝试使用 Spring 5 WebClient 记录请求。你知道我怎么能做到这一点吗?

(我使用的是 Spring 5 和 Spring Boot 2)

现在的代码是这样的:

try {
    return webClient.get().uri(url, urlParams).exchange().flatMap(response -> response.bodyToMono(Test.class))
            .map(test -> xxx.set(test));
} catch (RestClientException e) {
    log.error("Cannot get counter from opus", e);
    throw e;
}

【问题讨论】:

标签: java spring-boot logging spring-webflux


【解决方案1】:

您可以使用过滤功能通过一些操作来跟踪 webclient 日志,包括请求和响应正文负载:

public class TracingExchangeFilterFunction implements ExchangeFilterFunction {


    return next.exchange(buildTraceableRequest(request))
            .flatMap(response ->
                    response.body(BodyExtractors.toDataBuffers())
                            .next()
                            .doOnNext(dataBuffer -> traceResponse(response, dataBuffer))
                            .thenReturn(response)) ;
}

private ClientRequest buildTraceableRequest( 
        final ClientRequest clientRequest) {
    return ClientRequest.from(clientRequest).body(
            new BodyInserter<>() {
                @Override
                public Mono<Void> insert(
                        final ClientHttpRequest outputMessage,
                        final Context context) {
                    return clientRequest.body().insert(
                            new ClientHttpRequestDecorator(outputMessage) {
                                @Override
                                public Mono<Void> writeWith(final Publisher<? extends DataBuffer> body) {
                                    return super.writeWith(
                                            from(body).doOnNext(buffer ->
                                                    traceRequest(clientRequest, buffer)));
                                }
                            }, context);
                }
            }).build();
}

private void traceRequest(ClientRequest clientRequest, DataBuffer buffer) {
    final ByteBuf byteBuf = NettyDataBufferFactory.toByteBuf(buffer);
    final byte[] bytes = ByteBufUtil.getBytes(byteBuf);
    // do some tracing
}


private void traceResponse(ClientResponse response, DataBuffer dataBuffer) {
    final byte[] bytes = new byte[dataBuffer.readableByteCount()];
    dataBuffer.read(bytes);
    // do some tracing
}

}

【讨论】:

    【解决方案2】:

    对于 Spring 的响应式 WebClient,正确获取请求/响应日志记录确实很困难。

    我有以下要求:

    • 一个日志语句中记录请求和响应包括正文(如果您在 AWS cloudwatch 中滚动浏览数百条日志,那么将所有内容放在一个语句中会更方便)
    • 从日志中过滤敏感数据,例如个人数据或财务数据,以符合 GDPR 和 PCI

    Wiretapping Netty 或使用custom Jackson en-/decoders 因此不是一种选择。

    这是我对这个问题的看法(同样基于 Stanislav 出色的 answer)。

    (以下代码使用Lombok注解处理,如果你还没有使用它,你可能也想使用它。否则应该很容易de-lombok)

    import lombok.RequiredArgsConstructor;
    import lombok.extern.slf4j.Slf4j;
    import org.reactivestreams.Publisher;
    import org.springframework.core.io.buffer.DataBuffer;
    import org.springframework.http.client.reactive.ClientHttpRequest;
    import org.springframework.http.client.reactive.ClientHttpRequestDecorator;
    import org.springframework.lang.NonNull;
    import org.springframework.util.StopWatch;
    import org.springframework.web.reactive.function.BodyInserter;
    import org.springframework.web.reactive.function.client.ClientRequest;
    import org.springframework.web.reactive.function.client.ClientResponse;
    import org.springframework.web.reactive.function.client.ExchangeFilterFunction;
    import org.springframework.web.reactive.function.client.ExchangeFunction;
    import reactor.core.publisher.Flux;
    import reactor.core.publisher.Mono;
    
    import java.util.concurrent.atomic.AtomicBoolean;
    
    import static java.lang.Math.min;
    import static java.util.UUID.randomUUID;
    import static net.logstash.logback.argument.StructuredArguments.v;
    
    @Slf4j
    @RequiredArgsConstructor
    public class RequestLoggingFilterFunction implements ExchangeFilterFunction {
    
      private static final int MAX_BYTES_LOGGED = 4_096;
    
      private final String externalSystem;
    
      @Override
      @NonNull
      public Mono<ClientResponse> filter(@NonNull ClientRequest request, @NonNull ExchangeFunction next) {
        if (!log.isDebugEnabled()) {
          return next.exchange(request);
        }
    
        var clientRequestId = randomUUID().toString();
    
        var requestLogged = new AtomicBoolean(false);
        var responseLogged = new AtomicBoolean(false);
    
        var capturedRequestBody = new StringBuilder();
        var capturedResponseBody = new StringBuilder();
    
        var stopWatch = new StopWatch();
        stopWatch.start();
    
        return next
          .exchange(ClientRequest.from(request).body(new BodyInserter<>() {
    
            @Override
            @NonNull
            public Mono<Void> insert(@NonNull ClientHttpRequest req, @NonNull Context context) {
              return request.body().insert(new ClientHttpRequestDecorator(req) {
    
                @Override
                @NonNull
                public Mono<Void> writeWith(@NonNull Publisher<? extends DataBuffer> body) {
                  return super.writeWith(Flux.from(body).doOnNext(data -> capturedRequestBody.append(extractBytes(data)))); // number of bytes appended is maxed in real code
                }
    
              }, context);
            }
          }).build())
          .doOnNext(response -> {
              if (!requestLogged.getAndSet(true)) {
                log.debug("| >>---> Outgoing {} request [{}]\n{} {}\n{}\n\n{}\n",
                  v("externalSystem", externalSystem),
                  v("clientRequestId", clientRequestId),
                  v("clientRequestMethod", request.method()),
                  v("clientRequestUrl", request.url()),
                  v("clientRequestHeaders", request.headers()), // filtered in real code
                  v("clientRequestBody", capturedRequestBody.toString()) // filtered in real code
                );
              }
            }
          )
          .doOnError(error -> {
            if (!requestLogged.getAndSet(true)) {
              log.debug("| >>---> Outgoing {} request [{}]\n{} {}\n{}\n\nError: {}\n",
                v("externalSystem", externalSystem),
                v("clientRequestId", clientRequestId),
                v("clientRequestMethod", request.method()),
                v("clientRequestUrl", request.url()),
                v("clientRequestHeaders", request.headers()), // filtered in real code
                error.getMessage()
              );
            }
          })
          .map(response -> response.mutate().body(transformer -> transformer
              .doOnNext(body -> capturedResponseBody.append(extractBytes(body))) // number of bytes appended is maxed in real code
              .doOnTerminate(() -> {
                if (stopWatch.isRunning()) {
                  stopWatch.stop();
                }
              })
              .doOnComplete(() -> {
                if (!responseLogged.getAndSet(true)) {
                  log.debug("| <---<< Response for outgoing {} request [{}] after {}ms\n{} {}\n{}\n\n{}\n",
                    v("externalSystem", externalSystem),
                    v("clientRequestId", clientRequestId),
                    v("clientRequestExecutionTimeInMillis", stopWatch.getTotalTimeMillis()),
                    v("clientResponseStatusCode", response.statusCode().value()),
                    v("clientResponseStatusPhrase", response.statusCode().getReasonPhrase()),
                    v("clientResponseHeaders", response.headers()), // filtered in real code
                    v("clientResponseBody", capturedResponseBody.toString()) // filtered in real code
                  );
                }
              })
              .doOnError(error -> {
                  if (!responseLogged.getAndSet(true)) {
                    log.debug("| <---<< Error parsing response for outgoing {} request [{}] after {}ms\n{}",
                      v("externalSystem", externalSystem),
                      v("clientRequestId", clientRequestId),
                      v("clientRequestExecutionTimeInMillis", stopWatch.getTotalTimeMillis()),
                      v("clientErrorMessage", error.getMessage())
                    );
                  }
                }
              )
            ).build()
          );
      }
    
      private static String extractBytes(DataBuffer data) {
        int currentReadPosition = data.readPosition();
        var numberOfBytesLogged = min(data.readableByteCount(), MAX_BYTES_LOGGED);
        var bytes = new byte[numberOfBytesLogged];
        data.read(bytes, 0, numberOfBytesLogged);
        data.readPosition(currentReadPosition);
        return new String(bytes);
      }
    
    }
    

    成功交换的日志条目如下所示:

    2021-12-07 17:14:04.029 DEBUG --- [ctor-http-nio-3] RequestLoggingFilterFunction        : | >>---> Outgoing SnakeOil request [6abd0170-d682-4ca6-806c-bbb3559998e8]
    POST https://localhost:8101/snake-oil/oauth/token
    Content-Type: application/x-www-form-urlencoded
    
    grant_type=client_credentials&client_id=*****&client_secret=*****
    
    2021-12-07 17:14:04.037 DEBUG --- [ctor-http-nio-3] RequestLoggingFilterFunction        : | <---<< Response for outgoing SnakeOil request [6abd0170-d682-4ca6-806c-bbb3559998e8] after 126ms
    200 OK
    Content-Type: application/json
    Vary: [Accept-Encoding, User-Agent]
    Transfer-Encoding: chunked
    
    {"access_token":"*****","expires_in":"3600","token_type":"BearerToken"}
    

    当然,错误情况也会得到妥善处理。

    【讨论】:

      【解决方案3】:

      这是我基于 Stanislav Burov 出色回答的 sn-p。我将一些 lambda 提取到独立类中,以使代码对我来说更具可读性,并且我还实现了一个有限的 UTF-8 感知解码器。我正在使用一些 Guava 和 Java 17 功能,但此代码可以轻松移植到早期版本。我不缓冲整个请求/响应正文,而是在单独调用时记录缓冲区,因此它不会使用过多的 RAM 或写入极长的行。

      package kz.doubleservices.healthbus.util;
      
      import org.reactivestreams.Publisher;
      import org.slf4j.Logger;
      import org.springframework.core.io.buffer.DataBuffer;
      import org.springframework.http.client.reactive.ClientHttpRequest;
      import org.springframework.http.client.reactive.ClientHttpRequestDecorator;
      import org.springframework.lang.NonNull;
      import org.springframework.web.reactive.function.BodyInserter;
      import org.springframework.web.reactive.function.client.ClientRequest;
      import org.springframework.web.reactive.function.client.ClientResponse;
      import org.springframework.web.reactive.function.client.ExchangeFilterFunction;
      import org.springframework.web.reactive.function.client.ExchangeFunction;
      import reactor.core.publisher.Flux;
      import reactor.core.publisher.Mono;
      
      import java.util.HexFormat;
      import java.util.List;
      import java.util.Locale;
      import java.util.concurrent.ThreadLocalRandom;
      import java.util.function.Function;
      
      import static com.google.common.io.BaseEncoding.base32;
      
      public class LoggingExchangeFilterFunction implements ExchangeFilterFunction {
          private final Logger logger;
      
          public LoggingExchangeFilterFunction(Logger logger) {
              this.logger = logger;
          }
      
          @Override
          @NonNull
          public Mono<ClientResponse> filter(@NonNull ClientRequest request, @NonNull ExchangeFunction next) {
              if (!logger.isDebugEnabled()) {
                  return next.exchange(request);
              }
      
              String requestId = generateRequestId();
      
              if (logger.isTraceEnabled()) {
                  var message = new StringBuilder();
                  message.append("HTTP request start; request-id=").append(requestId).append('\n')
                          .append(request.method()).append(' ').append(request.url());
                  request.headers().forEach((String name, List<String> values) -> {
                      for (String value : values) {
                          message.append('\n').append(name).append(": ").append(value);
                      }
                  });
                  logger.trace(message.toString());
              } else {
                  logger.debug("HTTP request; request-id=" + requestId + '\n' +
                               request.method() + ' ' + request.url());
              }
      
              if (logger.isTraceEnabled()) {
                  var bodyInserter = new LoggingBodyInserter(logger, requestId, request.body());
                  request = ClientRequest.from(request).body(bodyInserter).build();
              }
      
              return next.exchange(request).map(new LoggingClientResponseTransformer(logger, requestId));
          }
      
          private static String generateRequestId() {
              var bytes = new byte[5];
              ThreadLocalRandom.current().nextBytes(bytes);
              return base32().encode(bytes).toLowerCase(Locale.ROOT);
          }
      
          private static class LoggingBodyInserter implements BodyInserter<Object, ClientHttpRequest> {
              private final Logger logger;
              private final String requestId;
              private final BodyInserter<?, ? super ClientHttpRequest> originalBodyInserter;
      
              private LoggingBodyInserter(Logger logger, String requestId,
                                          BodyInserter<?, ? super ClientHttpRequest> originalBodyInserter) {
                  this.logger = logger;
                  this.requestId = requestId;
                  this.originalBodyInserter = originalBodyInserter;
              }
      
              @Override
              @NonNull
              public Mono<Void> insert(@NonNull ClientHttpRequest outputMessage, @NonNull Context context) {
                  var loggingOutputMessage = new LoggingClientHttpRequestDecorator(outputMessage, logger, requestId);
                  return originalBodyInserter.insert(loggingOutputMessage, context);
              }
          }
      
          private static class LoggingClientHttpRequestDecorator extends ClientHttpRequestDecorator {
              private final Logger logger;
              private final String requestId;
      
              public LoggingClientHttpRequestDecorator(ClientHttpRequest delegate, Logger logger, String requestId) {
                  super(delegate);
                  this.logger = logger;
                  this.requestId = requestId;
              }
      
              @Override
              @NonNull
              public Mono<Void> writeWith(@NonNull Publisher<? extends DataBuffer> body) {
                  Flux<? extends DataBuffer> loggingBody = Flux.from(body)
                          .doOnNext(this::logDataBuffer)
                          .doOnComplete(this::logComplete)
                          .doOnError(this::logError);
                  return super.writeWith(loggingBody);
              }
      
              @Override
              @NonNull
              public Mono<Void> setComplete() {
                  logger.trace("HTTP request end; request-id=" + requestId);
                  return super.setComplete();
              }
      
              private void logDataBuffer(DataBuffer dataBuffer) {
                  int readPosition = dataBuffer.readPosition();
                  byte[] data = new byte[dataBuffer.readableByteCount()];
                  dataBuffer.read(data);
                  dataBuffer.readPosition(readPosition);
                  logger.trace("HTTP request data; request-id=" + requestId + '\n' + bytesToString(data));
              }
      
              private void logComplete() {
                  logger.trace("HTTP request end; request-id=" + requestId);
              }
      
              private void logError(Throwable exception) {
                  logger.trace("HTTP request error; request-id=" + requestId, exception);
              }
          }
      
          private static class LoggingClientResponseTransformer implements Function<ClientResponse, ClientResponse> {
              private final Logger logger;
              private final String requestId;
      
              private LoggingClientResponseTransformer(Logger logger, String requestId) {
                  this.logger = logger;
                  this.requestId = requestId;
              }
      
              @Override
              public ClientResponse apply(ClientResponse clientResponse) {
                  if (logger.isTraceEnabled()) {
                      var message = new StringBuilder();
                      message.append("HTTP response start; request-id=").append(requestId).append('\n')
                              .append("HTTP ").append(clientResponse.statusCode());
                      clientResponse.headers().asHttpHeaders().forEach((String name, List<String> values) -> {
                          for (String value : values) {
                              message.append('\n').append(name).append(": ").append(value);
                          }
                      });
                      logger.trace(message.toString());
                  } else {
                      logger.debug("HTTP response; request-id=" + requestId + '\n' +
                                   "HTTP " + clientResponse.statusCode());
                  }
      
                  return clientResponse.mutate()
                          .body(new ClientResponseBodyTransformer(logger, requestId))
                          .build();
              }
          }
      
          private static class ClientResponseBodyTransformer implements Function<Flux<DataBuffer>, Flux<DataBuffer>> {
              private final Logger logger;
              private final String requestId;
              private boolean completed = false;
      
              private ClientResponseBodyTransformer(Logger logger, String requestId) {
                  this.logger = logger;
                  this.requestId = requestId;
              }
      
              @Override
              public Flux<DataBuffer> apply(Flux<DataBuffer> body) {
                  return body
                          .doOnNext(this::logDataBuffer)
                          .doOnComplete(this::logComplete)
                          .doOnError(this::logError);
              }
      
              private void logDataBuffer(DataBuffer dataBuffer) {
                  int readPosition = dataBuffer.readPosition();
                  byte[] data = new byte[dataBuffer.readableByteCount()];
                  dataBuffer.read(data);
                  dataBuffer.readPosition(readPosition);
                  logger.trace("HTTP response data; request-id=" + requestId + '\n' + bytesToString(data));
              }
      
              private void logComplete() {
                  if (!completed) {
                      logger.trace("HTTP response end; request-id=" + requestId);
                      completed = true;
                  }
              }
      
              private void logError(Throwable exception) {
                  logger.trace("HTTP response error; request-id=" + requestId, exception);
              }
          }
      
          private static String bytesToString(byte[] bytes) {
              var string = new StringBuilder(bytes.length);
              for (int i = 0; i < bytes.length; i++) {
                  byte b1 = bytes[i];
                  if (b1 >= 0) {
                      if (32 <= b1 && b1 < 127) { // ordinary ASCII characters
                          string.append((char) b1);
                      } else {  // control characters
                          switch (b1) {
                              case '\t' -> string.append("\\t");
                              case '\n' -> string.append("\\n");
                              case '\r' -> string.append("\\r");
                              default -> {
                                  string.append("\\x");
                                  HexFormat.of().toHexDigits(string, b1);
                              }
                          }
                      }
                      continue;
                  }
                  if ((b1 & 0xe0) == 0xc0) { // UTF-8 first byte of 2-bytes sequence
                      i++;
                      if (i < bytes.length) {
                          byte b2 = bytes[i];
                          if ((b2 & 0xc0) == 0x80) { // UTF-8 second byte of 2-bytes sequence
                              char c = (char) ((b1 & 0x1f) << 6 | b2 & 0x3f);
                              if (Character.isLetter(c)) {
                                  string.append(c);
                                  continue;
                              }
                          }
                          string.append("\\x");
                          HexFormat.of().toHexDigits(string, b1);
                          string.append("\\x");
                          HexFormat.of().toHexDigits(string, b2);
                          continue;
                      }
                  }
                  string.append("\\x");
                  HexFormat.of().toHexDigits(string, b1);
              }
              return string.toString();
          }
      }
      

      【讨论】:

        【解决方案4】:

        根据Stanislav Burov 的回答,我制作了这个记录器,它记录所有请求/响应标头、方法、url 和正文。

        public class WebClientLogger implements ExchangeFilterFunction {
        
        @Override
        public Mono<ClientResponse> filter(ClientRequest request, ExchangeFunction next) {
            // Log url using 'request.url()'
            // Log method using 'request.method()'
            // Log request headers using 'request.headers().entrySet().stream().map(Object::toString).collect(joining(","))'
        
            BodyInserter<?, ? super ClientHttpRequest> originalBodyInserter = request.body();
        
            ClientRequest loggingClientRequest = ClientRequest.from(request)
                    .body((outputMessage, context) -> {
                        ClientHttpRequestDecorator loggingOutputMessage = new ClientHttpRequestDecorator(outputMessage) {
                            private final AtomicBoolean alreadyLogged = new AtomicBoolean(false);
        
                            @Override
                            public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
                                boolean needToLog = alreadyLogged.compareAndSet(false, true);
                                if (needToLog) {
        
                                    body = DataBufferUtils.join(body)
                                            .doOnNext(content -> {
                                                // Log request body using 'content.toString(StandardCharsets.UTF_8)'
                                            });
                                }
                                return super.writeWith(body);
                            }
        
                            @Override
                            public Mono<Void> setComplete() { // This is for requests with no body (e.g. GET).
                                boolean needToLog = alreadyLogged.compareAndSet(false, true);
                                if (needToLog) {
                                    
                                }
                                return super.setComplete();
                            }
                        };
        
                        return originalBodyInserter.insert(loggingOutputMessage, context);
                    })
                    .build();
        
            return next.exchange(loggingClientRequest)
                    .map(clientResponse -> {
                                // Log response status using 'clientResponse.statusCode().value())'
                                // Log response headers using 'clientResponse.headers().asHttpHeaders().entrySet().stream().map(Object::toString).collect(joining(","))'
        
                                return clientResponse.mutate()
                                        .body(f -> f.map(dataBuffer -> {
                                            // Log response body using 'dataBuffer.toString(StandardCharsets.UTF_8)'
                                            return dataBuffer;
                                        }))
                                        .build();
                            }
                    );
        
        }
        

        }

        【讨论】:

          【解决方案5】:

          您可以使用ExchangeFilterFunction轻松做到这一点

          当您使用WebClient.Builder 创建WebClient 时,只需添加自定义logRequest 过滤器即可。

          这是此类过滤器的示例以及如何将其添加到WebClient

          @Slf4j
          @Component
          public class MyClient {
          
              private final WebClient webClient;
          
              // Create WebClient instance using builder.
              // If you use spring-boot 2.0, the builder will be autoconfigured for you
              // with the "prototype" scope, meaning each injection point will receive
              // a newly cloned instance of the builder.
              public MyClient(WebClient.Builder webClientBuilder) {
                  webClient = webClientBuilder // you can also just use WebClient.builder()
                          .baseUrl("https://httpbin.org")
                          .filter(logRequest()) // here is the magic
                          .build();
              }
          
              // Just example of sending request. This method is NOT part of the answer
              public void send(String path) {
                  ClientResponse clientResponse = webClient
                          .get().uri(uriBuilder -> uriBuilder.path(path)
                                  .queryParam("param", "value")
                                  .build())
                          .exchange()
                          .block();
                  log.info("Response: {}", clientResponse.toEntity(String.class).block());
              }
          
              // This method returns filter function which will log request data
              private static ExchangeFilterFunction logRequest() {
                  return ExchangeFilterFunction.ofRequestProcessor(clientRequest -> {
                      log.info("Request: {} {}", clientRequest.method(), clientRequest.url());
                      clientRequest.headers().forEach((name, values) -> values.forEach(value -> log.info("{}={}", name, value)));
                      return Mono.just(clientRequest);
                  });
              }
          
          }
          

          然后只需调用myClient.send("get"); 并且日志消息应该在那里。

          输出示例:

          Request: GET https://httpbin.org/get?param=value
          header1=value1
          header2=value2
          

          编辑

          有人在 cmets 中指出 block() 是不好的做法等。我想澄清一下:block() 在这里调用只是用于演示目的。请求日志过滤器无论如何都会起作用。您无需将block() 添加到您的代码中即可使ExchangeFilterFunction 工作。您可以使用WebClient 以通常的方式执行http 调用,链接方法并在堆栈上返回Mono,直到有人订阅它。答案中唯一相关的部分是logRequest() 过滤器。您可以完全忽略 send() 方法 - 它不是解决方案的一部分 - 它只是证明过滤器有效。

          有些人还问如何记录响应。要记录响应,您可以编写另一个 ExchangeFilterFunction 并将其添加到 WebClient。为此,您可以使用 ExchangeFilterFunction.ofResponseProcessor 助手,就像使用 ExchangeFilterFunction.ofRequestProcessor 一样。你可以使用ClientResponse的方法来获取headers/cookies等。

              // This method returns filter function which will log response data
              private static ExchangeFilterFunction logResponse() {
                  return ExchangeFilterFunction.ofResponseProcessor(clientResponse -> {
                      log.info("Response status: {}", clientResponse.statusCode());
                      clientResponse.headers().asHttpHeaders().forEach((name, values) -> values.forEach(value -> log.info("{}={}", name, value)));
                      return Mono.just(clientResponse);
                  });
              }
          

          别忘了将它添加到您的WebClient

          .filter(logResponse())
          

          但要小心,不要试图在过滤器中读取响应正文。由于它的流性质,主体只能被消耗一次,而无需某种缓冲包装器。因此,如果您将在过滤器中阅读它,您将无法在订阅者中阅读它。

          如果你真的需要记录body,你可以让底层(Netty)来做这个。请参阅Matthew Buckett's answer 了解相关信息。

          【讨论】:

          • IMO,调用 block() 违背了使用 webclient 的目的。我们可以使用过滤器记录请求,但不确定如何在不阻塞的情况下记录来自Mono&lt;ClientResponse&gt; 的响应。
          • @PavanKumar 此处的block() 调用仅用于演示目的。请求日志过滤器无论如何都会起作用。要记录响应,您可以编写另一个 ExchangeFilterFunction 并记录响应。但是在记录响应正文时要小心——由于它的流性质,它只能被使用一次而无需包装。
          • 如何从clientRequest 获取请求正文?
          • 简短回答:你不能。一旦您阅读正文(记录它),消费者就无法再访问它了。我认为,它可以实现将主体流包装到一些缓冲流中,但老实说我从来没有这样做过。而且它会吃掉内存,让响应式编程变得毫无意义。如果你真的需要记录body,你可以让底层(Netty)来做这件事。请参阅Matthew Buckett's answer 了解这个想法。
          • 由于阻塞,我认为此解决方案不实用。
          【解决方案6】:

          有一种方法可以仅使用 ExchangeFilterFunction 来记录请求和响应正文。独立于底层ClientHttpConnector,支持定制输出。实际输出不包含在实现中。相反,可以访问请求和响应正文的行包含解释性 cmets。将以下类实例添加到WebClient 过滤器列表中:

          import org.reactivestreams.Publisher;
          import org.springframework.core.io.buffer.DataBuffer;
          import org.springframework.http.client.reactive.ClientHttpRequest;
          import org.springframework.http.client.reactive.ClientHttpRequestDecorator;
          import org.springframework.web.reactive.function.BodyInserter;
          import org.springframework.web.reactive.function.client.ClientRequest;
          import org.springframework.web.reactive.function.client.ClientResponse;
          import org.springframework.web.reactive.function.client.ExchangeFilterFunction;
          import org.springframework.web.reactive.function.client.ExchangeFunction;
          import reactor.core.publisher.BaseSubscriber;
          import reactor.core.publisher.Mono;
          
          import java.util.concurrent.atomic.AtomicBoolean;
          
          public class LoggingExchangeFilterFunction implements ExchangeFilterFunction {
          
              @Override
              public Mono<ClientResponse> filter(ClientRequest request, ExchangeFunction next) {
                  BodyInserter<?, ? super ClientHttpRequest> originalBodyInserter = request.body();
                  ClientRequest loggingClientRequest = ClientRequest.from(request)
                          .body((outputMessage, context) -> {
                              ClientHttpRequestDecorator loggingOutputMessage = new ClientHttpRequestDecorator(outputMessage) {
          
                                  private final AtomicBoolean alreadyLogged = new AtomicBoolean(false); // Not sure if thread-safe is needed...
          
                                  @Override
                                  public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
                                      boolean needToLog = alreadyLogged.compareAndSet(false, true);
                                      if (needToLog) {
                                          // use `body.toString(Charset.defaultCharset())` to obtain request body
                                      }
                                      return super.writeWith(body);
                                  }
          
                                  @Override
                                  public Mono<Void> writeAndFlushWith(Publisher<? extends Publisher<? extends DataBuffer>> body) {
                                      boolean needToLog = alreadyLogged.compareAndSet(false, true);
                                      if (needToLog) {
                                          BaseSubscriber<Publisher<? extends DataBuffer>> bodySubscriber = new BaseSubscriber<Publisher<? extends DataBuffer>>() {
                                              @Override
                                              protected void hookOnNext(Publisher<? extends DataBuffer> next) {
                                                  // use `next.toString(Charset.defaultCharset())` to obtain request body element
                                              }
                                          };
                                          body.subscribe(bodySubscriber);
                                          bodySubscriber.request(Long.MAX_VALUE);
                                      }
                                      return super.writeAndFlushWith(body);
                                  }
          
                                  @Override
                                  public Mono<Void> setComplete() { // This is for requests with no body (e.g. GET).
                                      boolean needToLog = alreadyLogged.compareAndSet(false, true);
                                      if (needToLog) {
                                          // A request with no body, could log `request.method()` and `request.url()`.
                                      }
                                      return super.setComplete();
                                  }
                              };
                              return originalBodyInserter.insert(loggingOutputMessage, context);
                          })
                          .build();
                  return next.exchange(loggingClientRequest)
                          .map(
                                  clientResponse -> clientResponse.mutate()
                                          .body(f -> f.map(dataBuffer -> {
                                              // Use `dataBuffer.toString(Charset.defaultCharset())` to obtain response body.
                                              return dataBuffer;
                                          }))
                                          .build()
                          );
              }
          
          }
          

          【讨论】:

          • 请求未写入。回应是。也需要请求订阅()
          【解决方案7】:

          如果要实现CustomLoggerHandler,别忘了实现equals()hashCode(),否则会出现内存泄漏https://github.com/reactor/reactor-netty/issues/988#issuecomment-582489035

          【讨论】:

            【解决方案8】:

            这就是 2021 年对我有用的方法 :)

            HttpClient httpClient = HttpClient
                    .create()
                    .wiretap(this.getClass().getCanonicalName(),
                            LogLevel.INFO, AdvancedByteBufFormat.TEXTUAL);
            
            WebClient client = WebClient.builder()
                    .baseUrl("https://example.com")
                    .clientConnector(new ReactorClientHttpConnector(httpClient))
                    .build();
            

            【讨论】:

            • WebTestClient 怎么样?不是 WebClient。它不适用于 WebTestClient。
            • 使用LogLevel.DEBUG并将其包含在您的application.yml中:logging.level.reactor.netty.http.client.HttpClient: DEBUG
            【解决方案9】:

            在 Spring Boot 2.4.0 中,HttpClient 的 wiretap() 方法具有额外的参数,您可以传递这些参数以正常人类可读格式显示完整的请求/响应标头和正文.使用格式(AdvancedByteBufFormat.TEXTUAL)。

            HttpClient httpClient = HttpClient.create()
                  .wiretap(this.getClass().getCanonicalName(), LogLevel.DEBUG, AdvancedByteBufFormat.TEXTUAL);
            ClientHttpConnector conn = new ReactorClientHttpConnector(httpClient);   
            
            WebClient client =  WebClient.builder()
                        .clientConnector(conn)
                        .build();
            

            结果:

            POST /score HTTP/1.1
            Host: localhost:8080
            User-Agent: insomnia/2020.5.2
            Content-Type: application/json
            access_: 
            Authorization: Bearer eyJ0e....
            Accept: application/json
            content-length: 4506
            
            WRITE: 4506B {"body":{"invocations":[{"id":....
            
            
            READ: 2048B HTTP/1.0 200 OK
            Content-Type: application/json
            Content-Length: 2271
            Server: Werkzeug/1.0.1 Python/3.7.7
            Date: Fri, 29 Jan 2021 18:49:53 GMT
            
            {"body":{"results":[.....
            

            【讨论】:

            • 我们有了新的赢家!这很简单,可以按预期工作,谢谢!
            • 请问这里“com.somepackage”的值是多少? HttpClient 所在的当前包? Webclient 实例所在的包? http 调用所在的包? (我都有不同的类/包)
            • @PatPatPat 此值将传递给 SLF4J,并在生成带有跟踪的日志行时用作包名称。所以从技术上讲,任何字符串都可以去那里,但最好把 this.getClass().getCanonicalName()
            • 这适用于 WebTestClient 日志记录吗?我没有成功实现它。
            • 谢谢...节省了我的时间,并为请求和响应提供了可读的输出。
            【解决方案10】:

            剧透:到目前为止,使用ExchangeFilterFunction 的自定义日志记录不支持记录正文。

            在我的例子中,Bealdung 的解决方案实现了最好的日志记录(参见 this)。

            因此,我设置了一个默认构建器,以便不同的 API 共享它。

            @Bean
            public WebClient.Builder defaultWebClient() {
                final var builder = WebClient.builder();
                if (LOG.isDebugEnabled()) {
                    builder.clientConnector(new ReactorClientHttpConnector(
                            HttpClient.create().wiretap("reactor.netty.http.client.HttpClient",
                                    LogLevel.DEBUG, AdvancedByteBufFormat.TEXTUAL)
                    ));
                }
                return builder;
            }
            

            在具体的 API 配置中,我可以配置具体的东西:

            @Bean
            public SpecificApi bspApi(@Value("${specific.api.url}") final String baseUrl,
                                 final WebClient.Builder builder) {
                final var webClient = builder.baseUrl(baseUrl).build();
                return new SpecificApi(webClient);
            }
            

            然后我必须设置以下属性:

            logging.level.reactor.netty.http.client: DEBUG
            

            那么请求日志是这样的:

            021-03-03 12:56:34.589 DEBUG 20464 --- [ctor-http-nio-2] reactor.netty.http.client.HttpClient     : [id: 0xe75a7fb8] REGISTERED
            2021-03-03 12:56:34.590 DEBUG 20464 --- [ctor-http-nio-2] reactor.netty.http.client.HttpClient     : [id: 0xe75a7fb8] CONNECT: /192.168.01:80
            2021-03-03 12:56:34.591 DEBUG 20464 --- [ctor-http-nio-2] reactor.netty.http.client.HttpClient     : [id: 0xe75a7fb8, L:/192.168.04:56774 - R:/192.168.01:80] ACTIVE
            2021-03-03 12:56:34.591 DEBUG 20464 --- [ctor-http-nio-2] r.netty.http.client.HttpClientConnect    : [id: 0xe75a7fb8, L:/192.168.04:56774 - R:/192.168.01:80] Handler is being applied: {uri=http://192.168.01/user, method=GET}
            2021-03-03 12:56:34.592 DEBUG 20464 --- [ctor-http-nio-2] reactor.netty.http.client.HttpClient     : [id: 0xe75a7fb8, L:/192.168.04:56774 - R:/192.168.01:80] WRITE: 102B GET /user HTTP/1.1
            user-agent: ReactorNetty/1.0.3
            host: 192.168.01
            accept: */*
            
            <REQUEST_BODY>
            
            2021-03-03 12:56:34.592 DEBUG 20464 --- [ctor-http-nio-2] reactor.netty.http.client.HttpClient     : [id: 0xe75a7fb8, L:/192.168.04:56774 - R:/192.168.01:80] FLUSH
            2021-03-03 12:56:34.592 DEBUG 20464 --- [ctor-http-nio-2] reactor.netty.http.client.HttpClient     : [id: 0xe75a7fb8, L:/192.168.04:56774 - R:/192.168.01:80] WRITE: 0B 
            2021-03-03 12:56:34.592 DEBUG 20464 --- [ctor-http-nio-2] reactor.netty.http.client.HttpClient     : [id: 0xe75a7fb8, L:/192.168.04:56774 - R:/192.168.01:80] FLUSH
            2021-03-03 12:56:34.594 DEBUG 20464 --- [ctor-http-nio-2] reactor.netty.http.client.HttpClient     : [id: 0xe75a7fb8, L:/192.168.04:56774 - R:/192.168.01:80] READ: 2048B HTTP/1.1 200 
            Server: nginx/1.16.1
            Date: Wed, 03 Mar 2021 11:56:31 GMT
            Content-Type: application/json
            Content-Length: 4883
            Connection: keep-alive
            Access-Control-Allow-Origin: *
            Content-Range: items 0-4/4
            
            <RESPONSE_BODY>
            

            【讨论】:

            • 这对我来说非常有用,日志详细信息非常有用
            【解决方案11】:

            @StasKolodyuk 的回答详细说明了来自baeldung 的解决方案,用于记录反应式 WebClient 的响应正文。请注意

            tc.bootstrap(...)
            

            中已弃用
                HttpClient httpClient = HttpClient
                  .create()
                  .tcpConfiguration(
                    tc -> tc.bootstrap(
                      b -> BootstrapHandlers.updateLogSupport(b, new CustomLogger(HttpClient.class))))
                  .build()
            

            添加自定义 LoggingHandler 的另一种不被弃用的方法是 (Kotlin)

            val httpClient: HttpClient = HttpClient.create().mapConnect { conn, b ->
                BootstrapHandlers.updateLogSupport(b, CustomLogger(HttpClient::class.java))
                conn
            }
            

            【讨论】:

              【解决方案12】:

              如果您希望在请求或响应中记录 JSON 的序列化版本,您可以创建自己的 Json 编码器/解码器类来包装默认值并记录 JSON。具体来说,您将继承 Jackson2JsonEncoderJackson2JsonDecoder 类并覆盖公开串行数据的方法。

              这里有解释:https://andrew-flower.com/blog/webclient-body-logging

              上面显示的方法主要关注非流数据。为流式数据执行此操作可能更具挑战性。

              由于需要额外的内存/处理,显然不建议在 Prod 环境中执行此操作,但为开发环境配置它很有用。

              【讨论】:

                【解决方案13】:

                Spring Boot 2.2.4 和 Spring 5.2.3 的 2020 年 2 月更新:

                我没有设法让spring.http.log-request-details=true 完成它的工作,而当前Spring WebFlux reference suggests 需要进行一些编码才能记录标题,尽管代码示例使用了已弃用的exchangeStrategies() 方法。

                仍有一个已弃用方法的替代品,因此用于获取在 WebClient 级别记录的标头的紧凑代码可能如下所示:

                WebClient webClient = WebClient.builder()
                    .codecs(configurer -> configurer.defaultCodecs().enableLoggingRequestDetails(true))
                    .build();
                

                进一步

                logging.level.org.springframework.web.reactive.function.client.ExchangeFunctions=TRACE
                

                应该注意的是,并非所有标头都在 WebFlux ExchangeFunctions 级别可用(确实存在),因此根据@Matthew's suggestion,在 Netty HttpClient 级别进行更多日志记录也可能是必不可少的:

                WebClient webClient = WebClient.builder()
                    .clientConnector(new ReactorClientHttpConnector(
                        HttpClient.create()
                            .wiretap(true)))
                    .build()
                

                进一步

                logging.level.reactor.netty.http.client.HttpClient: DEBUG
                

                这也会记录尸体。

                【讨论】:

                  【解决方案14】:

                  @Matthew Buckett 的回答向您展示了如何获取 Netty 线路日志记录。但是,格式不是很花哨(它包括十六进制转储)。但是可以通过扩展io.netty.handler.logging.LoggingHandler来轻松定制

                  public class HttpLoggingHandler extends LoggingHandler {
                  
                      @Override
                      protected String format(ChannelHandlerContext ctx, String event, Object arg) {
                          if (arg instanceof ByteBuf) {
                              ByteBuf msg = (ByteBuf) arg;
                              return msg.toString(StandardCharsets.UTF_8);
                          }
                          return super.format(ctx, event, arg);
                      }
                  }
                  
                  

                  然后将其包含在您的 WebClient 配置中:

                  HttpClient httpClient = HttpClient.create()
                      .tcpConfiguration(tcpClient ->
                          tcpClient.bootstrap(bootstrap ->
                              BootstrapHandlers.updateLogSupport(bootstrap, new HttpLoggingHandler())));
                  
                  WebClient
                      .builder()
                      .clientConnector(new ReactorClientHttpConnector(httpClient))
                      .build()
                  

                  例子:

                  webClient.post()
                      .uri("https://postman-echo.com/post")
                      .syncBody("{\"foo\" : \"bar\"}")
                      .accept(MediaType.APPLICATION_JSON)
                      .exchange()
                      .block();
                  
                  2019-09-22 18:09:21.477 DEBUG   --- [ctor-http-nio-4] c.e.w.c.e.logging.HttpLoggingHandler     : [id: 0x505be2bb] REGISTERED
                  2019-09-22 18:09:21.489 DEBUG   --- [ctor-http-nio-4] c.e.w.c.e.logging.HttpLoggingHandler     : [id: 0x505be2bb] CONNECT: postman-echo.com/35.170.134.160:443
                  2019-09-22 18:09:21.701 DEBUG   --- [ctor-http-nio-4] c.e.w.c.e.logging.HttpLoggingHandler     : [id: 0x505be2bb, L:/192.168.100.35:55356 - R:postman-echo.com/35.170.134.160:443] ACTIVE
                  2019-09-22 18:09:21.836 DEBUG   --- [ctor-http-nio-4] c.e.w.c.e.logging.HttpLoggingHandler     : [id: 0x505be2bb, L:/192.168.100.35:55356 - R:postman-echo.com/35.170.134.160:443] READ COMPLETE
                  2019-09-22 18:09:21.905 DEBUG   --- [ctor-http-nio-4] c.e.w.c.e.logging.HttpLoggingHandler     : [id: 0x505be2bb, L:/192.168.100.35:55356 - R:postman-echo.com/35.170.134.160:443] READ COMPLETE
                  2019-09-22 18:09:22.036 DEBUG   --- [ctor-http-nio-4] c.e.w.c.e.logging.HttpLoggingHandler     : [id: 0x505be2bb, L:/192.168.100.35:55356 - R:postman-echo.com/35.170.134.160:443] USER_EVENT: SslHandshakeCompletionEvent(SUCCESS)
                  2019-09-22 18:09:22.082 DEBUG   --- [ctor-http-nio-4] c.e.w.c.e.logging.HttpLoggingHandler     : POST /post HTTP/1.1
                  user-agent: ReactorNetty/0.8.11.RELEASE
                  host: postman-echo.com
                  Accept: application/json
                  Content-Type: text/plain;charset=UTF-8
                  content-length: 15
                  
                  {"foo" : "bar"}
                  2019-09-22 18:09:22.083 DEBUG   --- [ctor-http-nio-4] c.e.w.c.e.logging.HttpLoggingHandler     : [id: 0x505be2bb, L:/192.168.100.35:55356 - R:postman-echo.com/35.170.134.160:443] FLUSH
                  2019-09-22 18:09:22.086 DEBUG   --- [ctor-http-nio-4] c.e.w.c.e.logging.HttpLoggingHandler     : [id: 0x505be2bb, L:/192.168.100.35:55356 - R:postman-echo.com/35.170.134.160:443] READ COMPLETE
                  2019-09-22 18:09:22.217 DEBUG   --- [ctor-http-nio-4] c.e.w.c.e.logging.HttpLoggingHandler     : HTTP/1.1 200 OK
                  Content-Type: application/json; charset=utf-8
                  Date: Sun, 22 Sep 2019 15:09:22 GMT
                  ETag: W/"151-Llbe8OYGC3GeZCxttuAH3BOYBKA"
                  Server: nginx
                  set-cookie: sails.sid=s%3APe39li6V8TL8FOJOzSINZRkQlZ7HFAYi.UkLZjfajJqkq9fUfF2Y8N4JOInHNW5t1XACu3fhQYSc; Path=/; HttpOnly
                  Vary: Accept-Encoding
                  Content-Length: 337
                  Connection: keep-alive
                  
                  {"args":{},"data":"{\"foo\" : \"bar\"}","files":{},"form":{},"headers":{"x-forwarded-proto":"https","host":"postman-echo.com","content-length":"15","accept":"application/json","content-type":"text/plain;charset=UTF-8","user-agent":"ReactorNetty/0.8.11.RELEASE","x-forwarded-port":"443"},"json":null,"url":"https://postman-echo.com/post"}
                  2019-09-22 18:09:22.243 DEBUG   --- [ctor-http-nio-4] c.e.w.c.e.logging.HttpLoggingHandler     : [id: 0x505be2bb, L:/192.168.100.35:55356 - R:postman-echo.com/35.170.134.160:443] READ COMPLETE
                  

                  如果您想禁止(对您而言)无用的日志条目,例如(最后注意 ACTIVE):

                  2019-09-22 18:09:21.701 DEBUG   --- [ctor-http-nio-4] c.e.w.c.e.logging.HttpLoggingHandler     : [id: 0x505be2bb, L:/192.168.100.35:55356 - R:postman-echo.com/35.170.134.160:443] ACTIVE
                  

                  您可以像这样覆盖channelActive 和其他人:

                  @Override
                  public void channelActive(ChannelHandlerContext ctx) {
                      ctx.fireChannelActive();
                  }
                  

                  答案基于https://www.baeldung.com/spring-log-webclient-calls

                  【讨论】:

                  • 我在 Netty 4.1.38 上,此代码无法正确打印响应内容 (����$N\;��z�e��>$[�)。
                  • 添加链接baeldung.com/spring-log-webclient-calls中提到的构造函数后生效
                  • tcpClient.bootstrap 已被弃用,使用哪种方法并不明显
                  • 是的,这是记录正文的好方法。缺点是响应分散在多个日志条目中。
                  【解决方案15】:

                  您可以让 netty 记录请求/响应,方法是让它执行窃听,如果您像这样创建 Spring WebClient,那么它会启用窃听选项。

                          WebClient webClient = WebClient.builder()
                              .clientConnector(new ReactorClientHttpConnector(
                                  HttpClient.create().wiretap(true)
                              ))
                              .build()
                  

                  然后设置日志记录:

                  logging.level.reactor.netty.http.client.HttpClient: DEBUG
                  

                  这将记录请求/响应的所有内容(包括正文),但格式并非特定于 HTTP,因此可读性不强。

                  【讨论】:

                  • 非常感谢!虽然格式不是很可读,但这是我迄今为止发现的唯一方法,可以看到实际的请求和响应正文通过网络传输。
                  • 但是你仍然可以使用wiretap(HTTP_CLIENT, LogLevel.DEBUG, AdvancedByteBufFormat.TEXTUAL) 代替wiretap(true)
                  【解决方案16】:

                  您不一定需要滚动自己的记录器,reactor.ipc.netty.channel.ChannelOperationsHandler 会为您完成。只需为该类配置日志系统以在 DEBUG 级别进行日志记录:

                  2017-11-23 12:52:04.562 DEBUG 41449 --- [ctor-http-nio-5] r.i.n.channel.ChannelOperationsHandler   : [id: 0x9183d6da, L:/127.0.0.1:57681 - R:localhost/127.0.0.1:8000] Writing object DefaultFullHttpRequest(decodeResult: success, version: HTTP/1.1, content: UnpooledByteBufAllocator$InstrumentedUnpooledUnsafeHeapByteBuf(ridx: 0, widx: 0, cap: 0))
                  GET /api/v1/watch/namespaces/default/events HTTP/1.1
                  user-agent: ReactorNetty/0.7.1.RELEASE
                  host: localhost:8000
                  accept-encoding: gzip
                  Accept: application/json
                  content-length: 0
                  

                  减少错误的一种方法是尽可能不编写代码。

                  2018 年 11 月

                  使用spring-webflux:5.1.2.RELEASE,上述方法不再有效。请改用以下内容:

                  logging.level.org.springframework.web.reactive.function.client.ExchangeFunctions=DEBUG
                  ...
                  2018-11-06 20:58:58.181 DEBUG 20300 --- [           main] o.s.w.r.f.client.ExchangeFunctions       : [2026fbff] HTTP GET http://localhost:8080/stocks/search?symbol=AAPL
                  2018-11-06 20:58:58.451 DEBUG 20300 --- [ctor-http-nio-4] o.s.w.r.f.client.ExchangeFunctions       : [2026fbff] Response 400 BAD_REQUEST
                  

                  要记录标题或表单正文,请将上述设置为TRACE 级别;然而,这还不够:

                  ExchangeStrategies exchangeStrategies = ExchangeStrategies.withDefaults();
                  exchangeStrategies
                      .messageWriters().stream()
                      .filter(LoggingCodecSupport.class::isInstance)
                      .forEach(writer -> ((LoggingCodecSupport)writer).setEnableLoggingRequestDetails(true));
                  
                  client = WebClient.builder()
                      .exchangeStrategies(exchangeStrategies)
                  

                  2019 年 3 月

                  针对评论中询问如何记录请求和响应正文的问题,我不知道 Spring 是否有这样的记录器,但 WebClient 是基于 Netty 构建的,因此启用包 reactor.ipc.netty 的调试日志记录应该可以工作,以及this 答案。

                  【讨论】:

                  • 我更喜欢在有限和受控的环境中使用此解决方案作为调试的更简单选项。并不总是建议这样做,因为它可能会在标题等中暴露一些敏感信息。如果插入了自定义过滤器,则可以选择解析和隐藏此类敏感信息。
                  • @PavanKumar 任何类型的日志线路传输都可能暴露敏感的标头,例如Authorization。 OP 没有要求隐藏它们,但是 if 这是一个要求,then 代码可能会被编写。通常,在 DEBUG 级别记录此类标头是可以接受的。
                  • 是否也可以记录响应?
                  • @AbhijitSarkar LoggingHandler 应该如何添加到管道中?作为 Netty/Reactor 新手,引用的答案对我来说还不够。
                  • @AndrasHatvani 请开一个新线程,不要劫持这个。
                  【解决方案17】:

                  如果您不想记录正文,那么这真的很容易。

                  Spring Boot >= 2.1.0

                  将以下内容添加到 application.properties:

                  logging.level.org.springframework.web.reactive.function.client.ExchangeFunctions=TRACE
                  spring.http.log-request-details=true
                  

                  第二行导致标题包含在日志中。

                  Spring Boot

                  将以下内容添加到 application.properties:

                  logging.level.org.springframework.web.reactive.function.client.ExchangeFunctions=TRACE
                  

                  你需要像这样声明一个类,而不是上面的第二行:

                  @Configuration
                  static class LoggingCodecConfig {
                  
                      @Bean
                      @Order(0)
                      public CodecCustomizer loggingCodecCustomizer() {
                          return (configurer) -> configurer.defaultCodecs()
                                  .enableLoggingRequestDetails(true);
                      }
                  
                  }
                  

                  感谢this Brian Clozel answer

                  【讨论】:

                  • 我喜欢 Spring Boot >= 2.1.0 的版本,但在我的情况下它只记录 headers={masked}。知道为什么吗?
                  • 好的,有时我应该多花一点时间寻找解决方案:docs.spring.io/spring/docs/current/spring-framework-reference/…。这解释了为什么它会被屏蔽以及如何禁用它。
                  • @Tobske 来自 >=2.1.0 部分的第二个链接负责处理被屏蔽的标题,您不需要任何进一步的更改。至少这是我的经验。
                  • spring.http.log-request-details=true已弃用,取而代之的是spring.mvc.log-request-details=true
                  • 我看不到我的请求正文正在被记录。我错过了什么吗?
                  猜你喜欢
                  • 2018-10-17
                  • 2021-12-02
                  • 1970-01-01
                  • 2021-01-19
                  • 2018-01-07
                  • 2021-11-03
                  • 2018-04-09
                  • 2021-01-31
                  • 2020-04-05
                  相关资源
                  最近更新 更多