简体   繁体   English

如何记录 Spring 5 WebClient 调用

[英]how to log Spring 5 WebClient call

I'm trying to log a request using Spring 5 WebClient.我正在尝试使用 Spring 5 WebClient 记录请求。 Do you have any idea how could I achieve that?你知道我怎么能做到这一点吗?

(I'm using Spring 5 and Spring boot 2) (我正在使用 Spring 5 和 Spring 引导 2)

The code looks like this at the moment:代码现在看起来像这样:

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;
}

You can easily do it using ExchangeFilterFunction您可以使用ExchangeFilterFunction轻松完成

Just add the custom logRequest filter when you create your WebClient using WebClient.Builder .只需在使用WebClient.Builder创建WebClient时添加自定义logRequest过滤器。

Here is the example of such filter and how to add it to the WebClient .这是此类过滤器的示例以及如何将其添加到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);
        });
    }

}

Then just call myClient.send("get");然后只需调用myClient.send("get"); and log messages should be there.和日志消息应该在那里。

Output example:输出示例:

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

Edit编辑

Some people pointed out in comments that block() is bad practice etc. I want to clarify: block() call here is just for demo purposes .有些人在评论中指出block()是不好的做法等等。我想澄清一下:这里的block()调用仅用于演示目的 The request logging filter will work anyway.请求日志过滤器无论如何都会工作。 You will not need to add block() to your code to make ExchangeFilterFunction work.您无需在代码中添加block()即可使ExchangeFilterFunction工作。 You can use WebClient to perform a http-call in a usual way, chaining methods and returning Mono up the stack until someone will subscribe to it.您可以使用WebClient以通常的方式执行 http 调用,链接方法并将Mono返回堆栈,直到有人订阅它。 The only relevant part of the answer is logRequest() filter.答案的唯一相关部分是logRequest()过滤器。 You can ignore send() method altogether - it is not part of the solution - it just demonstrates that filter works.您可以完全忽略send()方法 - 它不是解决方案的一部分 - 它只是证明过滤器有效。

Some people also asked how to log the response.有些人还询问如何记录响应。 To log the response you can write another ExchangeFilterFunction and add it to WebClient .要记录响应,您可以编写另一个ExchangeFilterFunction并将其添加到WebClient You can use ExchangeFilterFunction.ofResponseProcessor helper for this purpose just the same way as ExchangeFilterFunction.ofRequestProcessor is used.您可以使用ExchangeFilterFunction.ofResponseProcessor帮手用于此目的,就像同样的方式ExchangeFilterFunction.ofRequestProcessor使用。 You can use methods of ClientResponse to get headers/cookies etc.您可以使用ClientResponse方法来获取标头/cookie 等。

    // 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);
        });
    }

Don't forget to add it to your WebClient :不要忘记将它添加到您的WebClient

.filter(logResponse())

But be careful and do not try to read the response body here in the filter.但要小心,不要试图在过滤器中读取响应正文。 Because of the stream nature of it, the body can be consumed only once without some kind of buffering wrapper.由于它的流性质,body 只能被消耗一次,而无需某种缓冲包装器。 So, if you will read it in the filter, you will not be able to read it in the subscriber.因此,如果您将在过滤器中阅读它,您将无法在订阅者中阅读它。

If you really need to log the body, you can make the underlying layer (Netty) to do this.如果真的需要log body,可以让底层(Netty)来做这个。 See Matthew Buckett's answer to get the idea.请参阅马修巴克特的答案以了解这个想法。

You can have netty do logging of the request/responses with by asking it todo wiretaping, if you create your Spring WebClient like this then it enables the wiretap option.您可以让 netty 记录请求/响应,方法是询问它是否进行窃听,如果您像这样创建 Spring WebClient,那么它会启用窃听选项。

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

and then have your logging setup:然后进行日志记录设置:

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

this will log everything for the request/response (including bodies), but the format is not specific to HTTP so not very readable.这将记录请求/响应的所有内容(包括正文),但格式并非特定于 HTTP,因此可读性不强。

You don't necessarily need to roll your own logger, reactor.ipc.netty.channel.ChannelOperationsHandler does it for you.您不一定需要滚动自己的记录器, reactor.ipc.netty.channel.ChannelOperationsHandler会为您完成。 Just configure your logging system for that class to log at DEBUG level:只需为该类配置日志系统以在调试级别进行日志记录:

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

One way to have fewer bugs is to not write code whenever possible.减少错误的一种方法是尽可能不编写代码。

Nov 2018 : 2018 年 11 月

With spring-webflux:5.1.2.RELEASE , the above no longer works.使用spring-webflux:5.1.2.RELEASE ,以上不再有效。 Use the following instead:请改用以下内容:

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

To log headers or form body, set the above to TRACE level;要记录标题或表单正文,请将上述设置为TRACE级别; however, that's not enough:然而,这还不够:

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

client = WebClient.builder()
    .exchangeStrategies(exchangeStrategies)

Mar 2019 : 2019 年 3 月

In response to a question in the comment that asked how to log request and response body, I don't know if Spring has such a logger but WebClient is built on Netty, so enabling debug logging for package reactor.ipc.netty should work, along with this answer.在回答评论中询问如何记录请求和响应正文的问题时,我不知道 Spring 是否有这样的记录器,但WebClient是基于 Netty 构建的,因此为包reactor.ipc.netty启用调试日志记录应该可以工作,连同这个答案。

If you don't want to log the body, then this is really easy.如果你不想记录身体,那么这真的很容易。

Spring Boot >= 2.1.0 Spring Boot >= 2.1.0

Add the following to application.properties:将以下内容添加到 application.properties:

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

The second line causes headers to be included in the log.第二行导致日志中包含标题。

Spring Boot < 2.1.0弹簧靴 < 2.1.0

Add the following to application.properties:将以下内容添加到 application.properties:

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

Instead of the second line above, you need to declare a class like this:您需要声明一个类,而不是上面的第二行:

@Configuration
static class LoggingCodecConfig {

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

}

Courtesy of this Brian Clozel answer感谢这个 Brian Clozel 的回答

@Matthew Buckett answer shows you how to get Netty wire logging. @Matthew Buckett 的回答向您展示了如何获取 Netty 线路日志记录。 However, the format is not very fancy (it includes hex dump).但是,格式不是很花哨(它包括十六进制转储)。 But it can be easily customized via extending io.netty.handler.logging.LoggingHandler但它可以通过扩展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);
    }
}

Then include it in your WebClient configuration:然后将其包含在您的WebClient配置中:

HttpClient httpClient = HttpClient.create()
    .tcpConfiguration(tcpClient ->
        tcpClient.bootstrap(bootstrap ->
            BootstrapHandlers.updateLogSupport(bootstrap, new HttpLoggingHandler())));

WebClient
    .builder()
    .clientConnector(new ReactorClientHttpConnector(httpClient))
    .build()

Example:例子:

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

If you want to suppress useless (for you) log entries like (note ACTIVE at the end):如果你想抑制无用的(对你来说)日志条目(注意最后的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

You can override channelActive and others like so:您可以像这样覆盖channelActive和其他人:

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

The answer is based on https://www.baeldung.com/spring-log-webclient-calls答案基于https://www.baeldung.com/spring-log-webclient-calls

An update of Feb 2020 for Spring Boot 2.2.4 and Spring 5.2.3: Spring Boot 2.2.4 和 Spring 5.2.3 的 2020 年 2 月更新:

I did not manage to get spring.http.log-request-details=true doing its job, and current Spring WebFlux reference suggests that some coding needs be done to have headers logged, though the code example uses deprecated exchangeStrategies() method.我没有设法让spring.http.log-request-details=true它的工作,并且当前的Spring WebFlux 参考表明需要完成一些编码来记录标头,尽管代码示例使用了不推荐使用的exchangeStrategies()方法。

There is still a replacement for the deprecated method, so a compact piece of code for getting headers logged at WebClient level may look like this:仍然有一个被弃用的方法的替代品,因此用于在 WebClient 级别获取标头的紧凑代码片段可能如下所示:

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

with further随着进一步

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

It should be noted though that not all of the headers are available (do exist) at WebFlux ExchangeFunctions level, so some more logging at Netty HttpClient level may be essential too, as per @Matthew's suggestion :应该注意的是,并非所有的头都在 WebFlux ExchangeFunctions级别可用(确实存在),因此根据 @Matthew的建议,在 Netty HttpClient级别进行更多日志记录也可能是必不可少

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

with further随着进一步

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

This will get bodies logged too.这也会记录身体。

In Spring Boot 2.4.0, the HttpClient's wiretap() method has additional parameters you can pass to show full request/response headers and body in normal human readable format.在 Spring Boot 2.4.0 中,HttpClient 的 wiretap() 方法具有额外的参数,您可以传递这些参数以以正常的人类可读格式显示完整的请求/响应标头和正文。 Use format (AdvancedByteBufFormat.TEXTUAL).使用格式 (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();

Result:结果:

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":[.....

This is what worked for me in 2021 :)这就是 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();

Spoiler: So far the custom logging with the ExchangeFilterFunction does not support to log the body.剧透:到目前为止,使用ExchangeFilterFunction的自定义日志记录不支持记录正文。

In my case the best logging is achieved with the solution from Bealdung (see this ).在我的情况下,最好的日志记录是通过 Bealdung 的解决方案实现的(请参阅)。

Therefore I setup a default builder so the different APIs share 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;
}

In the concrete API configuration I can then configure specific things:在具体的 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);
}

And then I have to set the following property:然后我必须设置以下属性:

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

Then the request log looks like:然后请求日志看起来像:

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>

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

I'm trying to log a request using Spring 5 WebClient.我正在尝试使用Spring 5 WebClient记录请求。 Do you have any idea how could I achieve that?你有什么主意我能做到吗?

(I'm using Spring 5 and Spring boot 2) (我正在使用Spring 5和Spring Boot 2)

The code looks like this at the moment:此刻的代码如下所示:

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;
}

There is a way to log request and response body using only ExchangeFilterFunction .有一种方法可以仅使用ExchangeFilterFunction来记录请求和响应正文。 It is independent of the underlying ClientHttpConnector and supports tailor-made output.它独立于底层的ClientHttpConnector并支持量身定制的输出。 The actual output isn't included in the implementation.实际输出不包括在实现中。 Instead the lines where request and response body can be accessed contain explanatory comments.相反,可以访问请求和响应正文的行包含解释性注释。 Add the following class instance to the WebClient filter list:将以下类实例添加到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()
                );
    }

}

Based on Stanislav Burov 's answer I made this logger, that logs all request/response headers, method, url and body.根据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();
                    }
            );

}

} }

For those asking about the same with WebTestClient the answer is very similar:对于那些询问与WebTestClient相同的人,答案非常相似:

final HttpClient httpClient = HttpClient
  .create()
  .wiretap(
    this.getClass().getCanonicalName(), 
    LogLevel.INFO, 
    AdvancedByteBufFormat.TEXTUAL);

final WebTestClient webTestClient = WebTestClient
  .bindToServer(new ReactorClientHttpConnector(httpClient))
  .build();

If you are looking to log the serialized version of the JSON in the request or response, you can create your own Json Encoder/Decoder classes that wrap the defaults and log the JSON.如果您希望在请求或响应中记录 JSON 的序列化版本,您可以创建自己的 Json 编码器/解码器类来包装默认值并记录 JSON。 Specifically you would subclass the Jackson2JsonEncoder and Jackson2JsonDecoder classes and override the methods that expose the serial data.具体来说,您将Jackson2JsonEncoderJackson2JsonDecoder类并覆盖公开串行数据的方法。

This is explained here: https://andrew-flower.com/blog/webclient-body-logging这在这里解释: https : //andrew-flower.com/blog/webclient-body-logging

The approach shown above is focused mainly on non-streaming data.上面显示的方法主要关注非流数据。 Doing it for streaming data might be more challenging.为流数据做这件事可能更具挑战性。

It's obviously not recommended to do this in a Prod environment due to extra memory / processing required, but configuring it for development environments is useful.由于需要额外的内存/处理,显然不建议在 Prod 环境中执行此操作,但为开发环境配置它很有用。

@StasKolodyuk 's answer elaborates on the solution from baeldung for logging the response body of a reactive WebClient. @StasKolodyuk的回答详细说明了baeldung的解决方案,用于记录反应式 WebClient 的响应主体。 Note that注意

tc.bootstrap(...)

is deprecated in已弃用

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

Another non-deprecated way to add your custom LoggingHandler is (Kotlin)添加自定义 LoggingHandler 的另一种非弃用方法是 (Kotlin)

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

Here's my snippet based on excellent Stanislav Burov's answer.这是我根据斯坦尼斯拉夫·布罗夫 (Stanislav Burov) 的出色回答编写的片段。 I extracted some lambdas to standalone classes to make code more readable for me and I also implemented a limited UTF-8 aware decoder.我将一些 lambda 提取到独立类中,以使代码对我更具可读性,并且我还实现了一个有限的 UTF-8 感知解码器。 I'm using some Guava and Java 17 features, but this code could be easily ported to early versions.我正在使用一些 Guava 和 Java 17 功能,但是可以轻松地将此代码移植到早期版本。 I don't buffer the whole request/response body, but rather log buffers as they come in a separate calls, so it does not use excessive RAM or writes extremely long lines.我不缓冲整个请求/响应主体,而是记录缓冲区,因为它们在单独的调用中出现,因此它不会使用过多的 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();
    }
}

Getting request/response logging right is somehow really difficult when it comes to Spring's reactive WebClient.当涉及到 Spring 的响应式 WebClient 时,正确获取请求/响应日志在某种程度上真的很困难。

I had the following requirements:我有以下要求:

  • Log request and response including bodies in one log statement (it's so much more convenient to have it all in one statement if you scroll through hundreds of logs in AWS cloudwatch)将请求和响应包括一个日志语句中(如果您在 AWS cloudwatch 中滚动浏览数百个日志,将所有内容都放在一个语句中会方便得多)
  • Filter sensitiv data such as personal data or financial data from the logs to be compliant with GDPR and PCI从日志中过滤敏感数据,例如个人数据或财务数据,以符合 GDPR 和 PCI

Wiretapping Netty or using custom Jackson en-/decoders was therefore not an option.因此,窃听 Netty或使用自定义 Jackson 编码器/解码器不是一种选择。

Here's my take on the problem (again based on Stanislav's excellent answer ).这是我对这个问题的看法(再次基于斯坦尼斯拉夫的出色回答)。

(The following code uses Lombok annotation processing, which you probably want to use as well, if you don't use it yet. Otherwise it should be easy to de-lombok) (以下代码使用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);
  }

}

Log entries look like this for successful exchanges:成功交换的日志条目如下所示:

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"}

Error conditions are handled gracefully as well, of course.当然,错误条件也会得到妥善处理。

You can do trace webclient logs including request and response body payload with some manipulations using filter function:您可以使用过滤器功能通过一些操作来跟踪 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
}

} }

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM