如何记录Spring 5 WebClient调用

时间:2017-09-11 11:46:08

标签: java spring-boot logging spring-webflux

我正在尝试使用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;
}

14 个答案:

答案 0 :(得分:18)

您可以使用ExchangeFilterFunction

轻松完成

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

以下是此类过滤器的示例以及如何将其添加到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
    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

答案 1 :(得分:18)

您不一定需要使用自己的记录器,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构建的,因此启用了包{的调试日志记录{1}}应该与this回答一起使用。

答案 2 :(得分:11)

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

答案 3 :(得分:8)

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

我没有设法使spring.http.log-request-details=true正常工作,并且当前Spring WebFlux reference suggests需要完成一些编码才能记录标头,尽管该代码示例使用了弃用的exchangeStrategies()方法。 / p>

仍然不推荐使用此方法,因此,用于在WebClient级别获取标头的紧凑代码如下:

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

进一步

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

请注意,尽管并非所有标头在WebFlux ExchangeFunctions级别都可用(确实存在),所以根据{{ 3}}:

HttpClient

进一步

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

这也会记录尸体。

答案 4 :(得分:6)

如果您不想记录身体,那真的很容易。

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 <2.1.0

将以下内容添加到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

提供

答案 5 :(得分:3)

@Matthew Buckett的答案向您展示了如何获取Netty电汇记录。但是,格式不是很特别(它包括十六进制转储)。但是可以通过扩展'\n'

轻松地对其进行自定义
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

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

答案 6 :(得分:2)

您可以要求请求进行窃听,从而让Netty记录请求/响应,如果您这样创建Spring WebClient,则它将启用窃听选项。

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

,然后进行日志记录设置:

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

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

答案 7 :(得分:2)

剧透:到目前为止,使用 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>

答案 8 :(得分:2)

这就是 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();

答案 9 :(得分:1)

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

答案 10 :(得分:0)

如果要在请求或响应中记录JSON的序列化版本,则可以创建自己的Json Encoder / Decoder类,该类包装默认值并记录JSON。具体来说,您可以将Jackson2JsonEncoderJackson2JsonDecoder类子类化,并覆盖公开串行数据的方法。

在此说明:https://andrew-flower.com/blog/webclient-body-logging

上面显示的方法主要集中在非流数据上。将其用于流数据可能更具挑战性。

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

答案 11 :(得分:0)

@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 :(得分:0)

有一种方法可以仅使用 ExchangeFilterFunction 来记录请求和响应正文。它独立于底层 ClientHttpConnector 并支持量身定制的输出。实际输出不包括在实现中。相反,可以访问请求和响应正文的行包含解释性注释。将以下类实例添加到 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()
                );
    }

}

答案 13 :(得分:0)

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

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

}

}