Merge pull request #7670 from maryarm/BAEL-3009
BAEL-3009: Logging Spring WebClient calls
This commit is contained in:
commit
0ca78b440d
|
@ -56,7 +56,6 @@
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- runtime and test scoped -->
|
<!-- runtime and test scoped -->
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-devtools</artifactId>
|
<artifactId>spring-boot-devtools</artifactId>
|
||||||
|
@ -89,6 +88,14 @@
|
||||||
<groupId>org.projectlombok</groupId>
|
<groupId>org.projectlombok</groupId>
|
||||||
<artifactId>lombok</artifactId>
|
<artifactId>lombok</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<!-- WebClient logging with Jetty -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.eclipse.jetty</groupId>
|
||||||
|
<artifactId>jetty-reactive-httpclient</artifactId>
|
||||||
|
<version>${jetty-reactive-httpclient.version}</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
<build>
|
<build>
|
||||||
|
@ -110,6 +117,7 @@
|
||||||
<jsonb-api.version>1.0</jsonb-api.version>
|
<jsonb-api.version>1.0</jsonb-api.version>
|
||||||
<geronimo-json_1.1_spec.version>1.0</geronimo-json_1.1_spec.version>
|
<geronimo-json_1.1_spec.version>1.0</geronimo-json_1.1_spec.version>
|
||||||
<commons-collections4.version>4.1</commons-collections4.version>
|
<commons-collections4.version>4.1</commons-collections4.version>
|
||||||
|
<jetty-reactive-httpclient.version>1.0.3</jetty-reactive-httpclient.version>
|
||||||
</properties>
|
</properties>
|
||||||
|
|
||||||
</project>
|
</project>
|
||||||
|
|
|
@ -0,0 +1,154 @@
|
||||||
|
package com.baeldung.reactive.logging;
|
||||||
|
|
||||||
|
import ch.qos.logback.classic.spi.LoggingEvent;
|
||||||
|
import ch.qos.logback.core.Appender;
|
||||||
|
import com.baeldung.reactive.logging.filters.LogFilters;
|
||||||
|
import com.baeldung.reactive.logging.netty.CustomLogger;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import java.net.URI;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import org.eclipse.jetty.client.api.Request;
|
||||||
|
import org.eclipse.jetty.util.ssl.SslContextFactory;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.http.client.reactive.JettyClientHttpConnector;
|
||||||
|
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
|
||||||
|
import org.springframework.web.reactive.function.BodyInserters;
|
||||||
|
import org.springframework.web.reactive.function.client.WebClient;
|
||||||
|
import reactor.netty.channel.BootstrapHandlers;
|
||||||
|
import reactor.netty.http.client.HttpClient;
|
||||||
|
|
||||||
|
import static com.baeldung.reactive.logging.jetty.RequestLogEnhancer.enhance;
|
||||||
|
import static org.mockito.ArgumentMatchers.argThat;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
|
||||||
|
public class WebClientLoggingIntegrationTest {
|
||||||
|
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Data
|
||||||
|
class Post {
|
||||||
|
private String title;
|
||||||
|
private String body;
|
||||||
|
private int userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Appender jettyAppender;
|
||||||
|
private Appender nettyAppender;
|
||||||
|
private Appender mockAppender;
|
||||||
|
private String sampleUrl = "https://jsonplaceholder.typicode.com/posts";
|
||||||
|
|
||||||
|
private Post post;
|
||||||
|
private String sampleResponseBody;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
private void setup() throws Exception {
|
||||||
|
|
||||||
|
post = new Post("Learn WebClient logging with Baeldung!", "", 1);
|
||||||
|
sampleResponseBody = new ObjectMapper().writeValueAsString(post);
|
||||||
|
|
||||||
|
ch.qos.logback.classic.Logger jetty = (ch.qos.logback.classic.Logger) LoggerFactory.getLogger("com.baeldung.reactive.logging.jetty");
|
||||||
|
jettyAppender = mock(Appender.class);
|
||||||
|
when(jettyAppender.getName()).thenReturn("com.baeldung.reactive.logging.jetty");
|
||||||
|
jetty.addAppender(jettyAppender);
|
||||||
|
|
||||||
|
ch.qos.logback.classic.Logger netty = (ch.qos.logback.classic.Logger) LoggerFactory.getLogger("reactor.netty.http.client");
|
||||||
|
nettyAppender = mock(Appender.class);
|
||||||
|
when(nettyAppender.getName()).thenReturn("reactor.netty.http.client");
|
||||||
|
netty.addAppender(nettyAppender);
|
||||||
|
|
||||||
|
ch.qos.logback.classic.Logger test = (ch.qos.logback.classic.Logger) LoggerFactory.getLogger("com.baeldung.reactive");
|
||||||
|
mockAppender = mock(Appender.class);
|
||||||
|
when(mockAppender.getName()).thenReturn("com.baeldung.reactive");
|
||||||
|
test.addAppender(mockAppender);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void givenJettyHttpClient_whenEndpointIsConsumed_thenRequestAndResponseBodyLogged() {
|
||||||
|
SslContextFactory.Client sslContextFactory = new SslContextFactory.Client();
|
||||||
|
org.eclipse.jetty.client.HttpClient httpClient = new org.eclipse.jetty.client.HttpClient(sslContextFactory) {
|
||||||
|
@Override
|
||||||
|
public Request newRequest(URI uri) {
|
||||||
|
Request request = super.newRequest(uri);
|
||||||
|
return enhance(request);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
WebClient
|
||||||
|
.builder()
|
||||||
|
.clientConnector(new JettyClientHttpConnector(httpClient))
|
||||||
|
.build()
|
||||||
|
.post()
|
||||||
|
.uri(sampleUrl)
|
||||||
|
.body(BodyInserters.fromObject(post))
|
||||||
|
.retrieve()
|
||||||
|
.bodyToMono(String.class)
|
||||||
|
.block();
|
||||||
|
|
||||||
|
verify(jettyAppender).doAppend(argThat(argument -> (((LoggingEvent) argument).getFormattedMessage()).contains(sampleResponseBody)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void givenNettyHttpClientWithWiretap_whenEndpointIsConsumed_thenRequestAndResponseBodyLogged() {
|
||||||
|
|
||||||
|
reactor.netty.http.client.HttpClient httpClient = HttpClient
|
||||||
|
.create()
|
||||||
|
.wiretap(true);
|
||||||
|
WebClient
|
||||||
|
.builder()
|
||||||
|
.clientConnector(new ReactorClientHttpConnector(httpClient))
|
||||||
|
.build()
|
||||||
|
.post()
|
||||||
|
.uri(sampleUrl)
|
||||||
|
.body(BodyInserters.fromObject(post))
|
||||||
|
.exchange()
|
||||||
|
.block();
|
||||||
|
|
||||||
|
verify(nettyAppender).doAppend(argThat(argument -> (((LoggingEvent) argument).getFormattedMessage()).contains("00000300")));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void givenNettyHttpClientWithCustomLogger_whenEndpointIsConsumed_thenRequestAndResponseBodyLogged() {
|
||||||
|
|
||||||
|
reactor.netty.http.client.HttpClient httpClient = HttpClient
|
||||||
|
.create()
|
||||||
|
.tcpConfiguration(
|
||||||
|
tc -> tc.bootstrap(
|
||||||
|
b -> BootstrapHandlers.updateLogSupport(b, new CustomLogger(HttpClient.class))));
|
||||||
|
WebClient
|
||||||
|
.builder()
|
||||||
|
.clientConnector(new ReactorClientHttpConnector(httpClient))
|
||||||
|
.build()
|
||||||
|
.post()
|
||||||
|
.uri(sampleUrl)
|
||||||
|
.body(BodyInserters.fromObject(post))
|
||||||
|
.exchange()
|
||||||
|
.block();
|
||||||
|
|
||||||
|
verify(nettyAppender).doAppend(argThat(argument -> (((LoggingEvent) argument).getFormattedMessage()).contains(sampleResponseBody)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void givenDefaultHttpClientWithFilter_whenEndpointIsConsumed_thenRequestAndResponseLogged() {
|
||||||
|
WebClient
|
||||||
|
.builder()
|
||||||
|
.filters(exchangeFilterFunctions -> {
|
||||||
|
exchangeFilterFunctions.addAll(LogFilters.prepareFilters());
|
||||||
|
})
|
||||||
|
.build()
|
||||||
|
.post()
|
||||||
|
.uri(sampleUrl)
|
||||||
|
.body(BodyInserters.fromObject(post))
|
||||||
|
.exchange()
|
||||||
|
.block();
|
||||||
|
|
||||||
|
verify(mockAppender).doAppend(argThat(argument -> (((LoggingEvent) argument).getFormattedMessage()).contains("domain=.typicode.com;")));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,54 @@
|
||||||
|
package com.baeldung.reactive.logging.filters;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.web.reactive.function.client.ExchangeFilterFunction;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
public class LogFilters {
|
||||||
|
public static List<ExchangeFilterFunction> prepareFilters() {
|
||||||
|
return Arrays.asList(logRequest(), logResponse());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ExchangeFilterFunction logRequest() {
|
||||||
|
return ExchangeFilterFunction.ofRequestProcessor(clientRequest -> {
|
||||||
|
if (log.isDebugEnabled()) {
|
||||||
|
StringBuilder sb = new StringBuilder("Request: \n")
|
||||||
|
.append(clientRequest.method())
|
||||||
|
.append(" ")
|
||||||
|
.append(clientRequest.url());
|
||||||
|
clientRequest
|
||||||
|
.headers()
|
||||||
|
.forEach((name, values) -> values.forEach(value -> sb
|
||||||
|
.append("\n")
|
||||||
|
.append(name)
|
||||||
|
.append(":")
|
||||||
|
.append(value)));
|
||||||
|
log.debug(sb.toString());
|
||||||
|
}
|
||||||
|
return Mono.just(clientRequest);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ExchangeFilterFunction logResponse() {
|
||||||
|
return ExchangeFilterFunction.ofResponseProcessor(clientResponse -> {
|
||||||
|
if (log.isDebugEnabled()) {
|
||||||
|
StringBuilder sb = new StringBuilder("Response: \n")
|
||||||
|
.append("Status: ")
|
||||||
|
.append(clientResponse.rawStatusCode());
|
||||||
|
clientResponse
|
||||||
|
.headers()
|
||||||
|
.asHttpHeaders()
|
||||||
|
.forEach((key, value1) -> value1.forEach(value -> sb
|
||||||
|
.append("\n")
|
||||||
|
.append(key)
|
||||||
|
.append(":")
|
||||||
|
.append(value)));
|
||||||
|
log.debug(sb.toString());
|
||||||
|
}
|
||||||
|
return Mono.just(clientResponse);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,93 @@
|
||||||
|
package com.baeldung.reactive.logging.jetty;
|
||||||
|
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.nio.charset.Charset;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.Locale;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.eclipse.jetty.client.api.Request;
|
||||||
|
import org.eclipse.jetty.http.HttpField;
|
||||||
|
import org.eclipse.jetty.http.HttpFields;
|
||||||
|
import org.eclipse.jetty.http.HttpHeader;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
public class RequestLogEnhancer {
|
||||||
|
|
||||||
|
public static Request enhance(Request request) {
|
||||||
|
StringBuilder group = new StringBuilder();
|
||||||
|
request.onRequestBegin(theRequest -> group
|
||||||
|
.append("Request ")
|
||||||
|
.append(theRequest.getMethod())
|
||||||
|
.append(" ")
|
||||||
|
.append(theRequest.getURI())
|
||||||
|
.append("\n"));
|
||||||
|
request.onRequestHeaders(theRequest -> {
|
||||||
|
for (HttpField header : theRequest.getHeaders())
|
||||||
|
group
|
||||||
|
.append(header)
|
||||||
|
.append("\n");
|
||||||
|
});
|
||||||
|
request.onRequestContent((theRequest, content) -> {
|
||||||
|
group.append(toString(content, getCharset(theRequest.getHeaders())));
|
||||||
|
});
|
||||||
|
request.onRequestSuccess(theRequest -> {
|
||||||
|
log.debug(group.toString());
|
||||||
|
group.delete(0, group.length());
|
||||||
|
});
|
||||||
|
group.append("\n");
|
||||||
|
request.onResponseBegin(theResponse -> {
|
||||||
|
group
|
||||||
|
.append("Response \n")
|
||||||
|
.append(theResponse.getVersion())
|
||||||
|
.append(" ")
|
||||||
|
.append(theResponse.getStatus());
|
||||||
|
if (theResponse.getReason() != null) {
|
||||||
|
group
|
||||||
|
.append(" ")
|
||||||
|
.append(theResponse.getReason());
|
||||||
|
}
|
||||||
|
group.append("\n");
|
||||||
|
});
|
||||||
|
request.onResponseHeaders(theResponse -> {
|
||||||
|
for (HttpField header : theResponse.getHeaders())
|
||||||
|
group
|
||||||
|
.append(header)
|
||||||
|
.append("\n");
|
||||||
|
});
|
||||||
|
request.onResponseContent((theResponse, content) -> {
|
||||||
|
group.append(toString(content, getCharset(theResponse.getHeaders())));
|
||||||
|
});
|
||||||
|
request.onResponseSuccess(theResponse -> {
|
||||||
|
log.debug(group.toString());
|
||||||
|
});
|
||||||
|
return request;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String toString(ByteBuffer buffer, Charset charset) {
|
||||||
|
byte[] bytes;
|
||||||
|
if (buffer.hasArray()) {
|
||||||
|
bytes = new byte[buffer.capacity()];
|
||||||
|
System.arraycopy(buffer.array(), 0, bytes, 0, buffer.capacity());
|
||||||
|
} else {
|
||||||
|
bytes = new byte[buffer.remaining()];
|
||||||
|
buffer.get(bytes, 0, bytes.length);
|
||||||
|
}
|
||||||
|
return new String(bytes, charset);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Charset getCharset(HttpFields headers) {
|
||||||
|
String contentType = headers.get(HttpHeader.CONTENT_TYPE);
|
||||||
|
if (contentType != null) {
|
||||||
|
String[] tokens = contentType
|
||||||
|
.toLowerCase(Locale.US)
|
||||||
|
.split("charset=");
|
||||||
|
if (tokens.length == 2) {
|
||||||
|
String encoding = tokens[1].replaceAll("[;\"]", "");
|
||||||
|
return Charset.forName(encoding);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return StandardCharsets.UTF_8;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,42 @@
|
||||||
|
package com.baeldung.reactive.logging.netty;
|
||||||
|
|
||||||
|
import io.netty.buffer.ByteBuf;
|
||||||
|
import io.netty.channel.ChannelHandlerContext;
|
||||||
|
import io.netty.handler.logging.LoggingHandler;
|
||||||
|
import java.nio.charset.Charset;
|
||||||
|
|
||||||
|
import static io.netty.util.internal.PlatformDependent.allocateUninitializedArray;
|
||||||
|
import static java.lang.Math.max;
|
||||||
|
import static java.nio.charset.Charset.defaultCharset;
|
||||||
|
|
||||||
|
public class CustomLogger extends LoggingHandler {
|
||||||
|
public CustomLogger(Class<?> clazz) {
|
||||||
|
super(clazz);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected String format(ChannelHandlerContext ctx, String event, Object arg) {
|
||||||
|
if (arg instanceof ByteBuf) {
|
||||||
|
ByteBuf msg = (ByteBuf) arg;
|
||||||
|
return decode(msg, msg.readerIndex(), msg.readableBytes(), defaultCharset());
|
||||||
|
}
|
||||||
|
return super.format(ctx, event, arg);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String decode(ByteBuf src, int readerIndex, int len, Charset charset) {
|
||||||
|
if (len != 0) {
|
||||||
|
byte[] array;
|
||||||
|
int offset;
|
||||||
|
if (src.hasArray()) {
|
||||||
|
array = src.array();
|
||||||
|
offset = src.arrayOffset() + readerIndex;
|
||||||
|
} else {
|
||||||
|
array = allocateUninitializedArray(max(len, 1024));
|
||||||
|
offset = 0;
|
||||||
|
src.getBytes(readerIndex, array, 0, len);
|
||||||
|
}
|
||||||
|
return new String(array, offset, len, charset);
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,11 +6,15 @@
|
||||||
<Pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</Pattern>
|
<Pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</Pattern>
|
||||||
</layout>
|
</layout>
|
||||||
</appender>
|
</appender>
|
||||||
|
|
||||||
<logger name="org.springframework" level="INFO" />
|
<logger name="org.springframework" level="INFO" />
|
||||||
|
|
||||||
|
<logger name="com.baeldung.reactive.logging.jetty" level="DEBUG" />
|
||||||
|
<logger name="reactor.netty.http.client.HttpClient" level="DEBUG" />
|
||||||
|
<logger name="com.baeldung.reactive.logging" level="DEBUG" />
|
||||||
|
|
||||||
<root level="INFO">
|
<root level="INFO">
|
||||||
<appender-ref ref="stdout" />
|
<appender-ref ref="stdout" />
|
||||||
</root>
|
</root>
|
||||||
|
|
||||||
</configuration>
|
</configuration>
|
Loading…
Reference in New Issue