diff --git a/spring-5-reactive-client/pom.xml b/spring-5-reactive-client/pom.xml index 1b71815eb4..70771f6832 100644 --- a/spring-5-reactive-client/pom.xml +++ b/spring-5-reactive-client/pom.xml @@ -56,7 +56,6 @@ - org.springframework.boot spring-boot-devtools @@ -89,6 +88,14 @@ org.projectlombok lombok + + + + org.eclipse.jetty + jetty-reactive-httpclient + ${jetty-reactive-httpclient.version} + test + @@ -110,6 +117,7 @@ 1.0 1.0 4.1 + 1.0.3 diff --git a/spring-5-reactive-client/src/test/java/com/baeldung/reactive/logging/WebClientLoggingIntegrationTest.java b/spring-5-reactive-client/src/test/java/com/baeldung/reactive/logging/WebClientLoggingIntegrationTest.java new file mode 100644 index 0000000000..95c63f267f --- /dev/null +++ b/spring-5-reactive-client/src/test/java/com/baeldung/reactive/logging/WebClientLoggingIntegrationTest.java @@ -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;"))); + } + + +} diff --git a/spring-5-reactive-client/src/test/java/com/baeldung/reactive/logging/filters/LogFilters.java b/spring-5-reactive-client/src/test/java/com/baeldung/reactive/logging/filters/LogFilters.java new file mode 100644 index 0000000000..c1c3d3e895 --- /dev/null +++ b/spring-5-reactive-client/src/test/java/com/baeldung/reactive/logging/filters/LogFilters.java @@ -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 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); + }); + } +} diff --git a/spring-5-reactive-client/src/test/java/com/baeldung/reactive/logging/jetty/RequestLogEnhancer.java b/spring-5-reactive-client/src/test/java/com/baeldung/reactive/logging/jetty/RequestLogEnhancer.java new file mode 100644 index 0000000000..43e3660743 --- /dev/null +++ b/spring-5-reactive-client/src/test/java/com/baeldung/reactive/logging/jetty/RequestLogEnhancer.java @@ -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; + } + +} + diff --git a/spring-5-reactive-client/src/test/java/com/baeldung/reactive/logging/netty/CustomLogger.java b/spring-5-reactive-client/src/test/java/com/baeldung/reactive/logging/netty/CustomLogger.java new file mode 100644 index 0000000000..9f2a4d127f --- /dev/null +++ b/spring-5-reactive-client/src/test/java/com/baeldung/reactive/logging/netty/CustomLogger.java @@ -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 ""; + } +} diff --git a/spring-5-reactive-client/src/test/resources/logback-test.xml b/spring-5-reactive-client/src/test/resources/logback-test.xml index 7072369b8d..42cb0865c5 100644 --- a/spring-5-reactive-client/src/test/resources/logback-test.xml +++ b/spring-5-reactive-client/src/test/resources/logback-test.xml @@ -6,11 +6,15 @@ %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n - + + + + + - + \ No newline at end of file