Spring 5 WebClient 통화 기록 방법
Spring 5 Web Client를 사용하여 요청을 기록하려고 합니다.내가 그걸 어떻게 할 수 있는지 알기나 해?
(스프링 5와 스프링 부츠 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;
}
Exchange Filter Function을 사용하면 쉽게 할 수 있습니다.
관습만 돼요.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. 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
편집
요.block()
나쁜 관행 등입니다.하고 말이 있습니다.block()
전화는 데모용입니다.요청 로깅 필터는 계속 작동합니다.추가할 필요가 없습니다.block()
'''를 만듭니다'''ExchangeFilterFunction
일, 일, 일, 일, 일, 일, 일, 일, 일, 일, 일, 일, 일.WebClient
http-call을 반환한다.Mono
다른 사람이 구독할 때까지 스택 위로 이동합니다.은 '아예'입니다.logRequest()
필터링을 실시합니다.해도 send()
이 방법은 솔루션의 일부가 아니라 필터가 작동한다는 것을 보여줄 뿐입니다.
을 사용하다 또 다른 글을 .ExchangeFilterFunction
해서 it에추 and에추 and and and and 。WebClient
이 목적으로 도우미를 사용할 수 있습니다.ExchangeFilterFunction.ofRequestProcessor
사용됩니다.메서드를 사용하여 헤더/쿠키 등을 가져올 수 있습니다.
// 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);
});
}
꼭 의 지 your에 해 주세요WebClient
:
.filter(logResponse())
단, 필터 내의 응답 본문을 읽지 않도록 주의해 주십시오.본체는 스트림 특성상 완충포장 없이 한 번만 섭취할 수 있습니다.따라서 필터로 읽을 경우 서브스크라이버에서는 읽을 수 없습니다.
본문을 로그에 기록해야 하는 경우 기본 계층(Netty)에서 이 작업을 수행할 수 있습니다.아이디어를 얻으려면 매튜 버켓의 답변을 참조하십시오.
요청/응답의 netty do logging은 도청 처리를 의뢰함으로써 할 수 있습니다.Spring Web Client를 이렇게 작성하면 도청 옵션이 활성화됩니다.
WebClient webClient = WebClient.builder()
.clientConnector(new ReactorClientHttpConnector(
HttpClient.create().wiretap(true)
))
.build()
로그 설정을 실시합니다.
logging.level.reactor.netty.http.client.HttpClient: DEBUG
요청/응답(본문 포함)을 모두 기록하지만 형식이 HTTP에 고유하지 않기 때문에 읽을 수 없습니다.
Spring Boot 2.4.0에서는 HttpClient의 wiretap() 메서드에는 완전한 요청/응답 헤더 및 본문을 일반 사람이 읽을 수 있는 형식으로 표시하기 위해 전달할 수 있는 추가 파라미터가 있습니다.형식(AdvancedByteBufFormat)을 사용합니다.텍스트)
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":[.....
로거를 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에 패키지 Netty의 하게 합니다.reactor.ipc.netty
이 답변과 함께 작동해야 합니다.
시체를 기록하고 싶지 않다면, 이건 정말 쉬워요.
스프링 부트 > = 2.1.0
application.properties에 다음 항목을 추가합니다.
logging.level.org.springframework.web.reactive.function.client.ExchangeFunctions=TRACE
spring.http.log-request-details=true
두 번째 행은 헤더가 로그에 포함되도록 합니다.
스프링 부트 < 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);
}
}
@Matew Buckett answer Netty 。그러나 형식은 그다지 화려하지 않습니다(16진수 덤프 포함). 그것은 할 수 .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에 근거하고 있습니다.
Spring Boot 2.2.4 및 Spring 5.2.3에 대한 2020년 2월 업데이트:
에 넣을 수 있었다spring.http.log-request-details=true
현재 Spring WebFlux 참조에서는 코드 예제에서는 사용되지 않는 메서드를 사용하지만 헤더를 기록하기 위해 몇 가지 코딩이 필요하다고 합니다.
권장되지 않는 메서드를 대체할 수 있는 방법이 아직 있으므로 WebClient 수준에서 헤더를 기록하기 위한 콤팩트한 코드 조각은 다음과 같습니다.
WebClient webClient = WebClient.builder()
.codecs(configurer -> configurer.defaultCodecs().enableLoggingRequestDetails(true))
.build();
더 나아가서
logging.level.org.springframework.web.reactive.function.client.ExchangeFunctions=TRACE
, WebFlux , 、 WebFlux 、 web((((((( ( ()(((((((((((((((((((존존존존존존존존존존존존존존존 it it존존존존존존 it it it it it it it it )에서 모든 헤더를 사용할 수 .ExchangeFunctions
몇 개 더합니다.HttpClient
@Matthew의 제안대로 레벨도 필수일 수 있습니다.
WebClient webClient = WebClient.builder()
.clientConnector(new ReactorClientHttpConnector(
HttpClient.create()
.wiretap(true)))
.build()
더 나아가서
logging.level.reactor.netty.http.client.HttpClient: DEBUG
이렇게 하면 시체도 기록될 거야
이것이 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();
Exchange Filter Function만을 사용하여 요청 및 응답 본문을 기록하는 방법이 있습니다.그것은 기초와는 무관하다.ClientHttpConnector
및 맞춤형 출력을 지원합니다.이치노대신 요청 및 응답 본문에 액세스할 수 있는 행에는 설명 설명이 포함됩니다. add add다 、 음 add to to to to add to to to 。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()
);
}
}
스포일러: 지금까지의 커스텀 로깅은ExchangeFilterFunction
는 본문 로그를 지원하지 않습니다.
나의 경우, Bealdung의 솔루션을 사용하여 최적의 로깅을 할 수 있습니다(이것 참조).
따라서 다른 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>
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();
}
);
}
}
여기 스타니슬라브 부로프의 훌륭한 답변을 바탕으로 한 제 토막이 있습니다.코드를 읽기 쉽게 하기 위해 스탠드아론 클래스에 람다를 추출하고 제한된 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();
}
}
Spring의 사후 대응형 WebClient에서는 요청/응답 로깅을 올바르게 수행하는 것이 매우 어렵습니다.
다음과 같은 요건이 있었습니다.
- 하나의 로그 스테이트먼트에 본문을 포함한 로그 요구 및 응답(AWS Cloudwatch에서 수백 개의 로그를 스크롤하면 모든 것을 하나의 스테이트먼트에 저장하는 것이 훨씬 편리함)
- 로그에서 개인 데이터나 재무 데이터 등의 기밀 데이터를 필터링하여 GPR 및 PCI에 준거
따라서 Netty를 도청하거나 맞춤형 Jackson 디코더를 사용하는 것은 선택사항이 아니었다.
스타니슬라프의 훌륭한 답변에 기초한 문제에 대한 제 견해입니다.
(아래 코드는 롬복 주석 처리를 사용하고 있으며, 아직 사용하지 않으신다면 이 주석 처리도 사용하고 싶을 것입니다.그렇지 않으면 쉽게 디롬복(delombok)을 해제할 수 있습니다.)
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"}
물론 오류 조건도 적절하게 처리됩니다.
WebTestClient
답은 매우 유사합니다.
final HttpClient httpClient = HttpClient
.create()
.wiretap(
this.getClass().getCanonicalName(),
LogLevel.INFO,
AdvancedByteBufFormat.TEXTUAL);
final WebTestClient webTestClient = WebTestClient
.bindToServer(new ReactorClientHttpConnector(httpClient))
.build();
요청 또는 응답에 JSON의 시리얼 버전을 기록하는 경우 기본값을 랩하는 자체 Json Encoder/Decoder 클래스를 만들고 JSON을 기록할 수 있습니다.으로는 '아주 좋다'라는 하위 를 합니다.Jackson2JsonEncoder
★★★★★★★★★★★★★★★★★」Jackson2JsonDecoder
클래스 및 시리얼 데이터를 노출하는 메서드를 덮어씁니다.
자세한 것은, https://andrew-flower.com/blog/webclient-body-logging 를 참조해 주세요.
위의 접근방식은 주로 비스트리밍 데이터에 초점을 맞추고 있습니다.데이터 스트리밍을 위해 이 작업을 수행하는 것이 더 어려울 수 있습니다.
추가 메모리/프로세싱이 필요하기 때문에 Prod 환경에서는 권장하지 않지만 개발 환경에 맞게 구성하는 것이 유용합니다.
@StasKolodyuk의 답변은 반응하는 WebClient의 응답 본문을 기록하기 위한 beldung의 솔루션에 대해 자세히 설명하고 있습니다.주의:
tc.bootstrap(...)
에서 권장되지 않습니다.
HttpClient httpClient = HttpClient
.create()
.tcpConfiguration(
tc -> tc.bootstrap(
b -> BootstrapHandlers.updateLogSupport(b, new CustomLogger(HttpClient.class))))
.build()
커스텀 Logging Handler를 추가하는 또 하나의 권장되지 않는 방법은 (Kotlin)입니다.
val httpClient: HttpClient = HttpClient.create().mapConnect { conn, b ->
BootstrapHandlers.updateLogSupport(b, CustomLogger(HttpClient::class.java))
conn
}
Handler 를 실장하는 Custom Logger Handler 를 실장하는 말아 .equals()
★★★★★★★★★★★★★★★★★」hashCode()
그렇지 않으면 메모리 누수가 발생합니다.https://github.com/reactor/reactor-netty/issues/988#issuecomment-582489035
필터 기능을 사용하여 요청 및 응답 본문 페이로드 등의 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
}
}
언급URL : https://stackoverflow.com/questions/46154994/how-to-log-spring-5-webclient-call
'programing' 카테고리의 다른 글
Scala / Lift에서 JSON 문자열을 작성 및 해석하려면 어떻게 해야 합니까? (0) | 2023.04.01 |
---|---|
팬더 DataFrame에서 여러 목록 열을 효율적으로 제거(해독)하는 방법 (0) | 2023.04.01 |
onChange 수신기를 사용해도 반응에서 입력 값을 변경할 수 없는 이유 (0) | 2023.04.01 |
mac의 mongodb 데이터베이스 위치 (0) | 2023.04.01 |
저장 프로시저에서 여러 행을 반환하는 방법(Oracle PL/SQL) (0) | 2023.04.01 |