Handle Spring WebFlux WebClient timeout in Kotlin - spring

I am doing a get http call with Spring WebFlux WebClient (Boot 2.4.3) in Kotlin (1.4.30). When request times out it fails with exception but instead I'd like to return a default value. I see references to onError, onStatus etc. used after retrieve() but they don't seem to be available in my case (only body, toEntity, awaitExchange)
The call:
suspend fun conversation(id: String): Conversation =
client.get().uri("/conversation/{id}", id).retrieve().awaitBody()
WebClient configuration with connect and read timeouts:
fun webClient(url: String, connectTimeout: Int, readTimeout: Long, objectMapper: ObjectMapper): WebClient =
WebClient.builder()
.baseUrl(url)
.exchangeStrategies(
ExchangeStrategies.builder()
.codecs { configurer -> configurer.defaultCodecs().jackson2JsonDecoder(Jackson2JsonDecoder(objectMapper)) }
.build())
.clientConnector(
ReactorClientHttpConnector(
HttpClient.create()
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, connectTimeout)
.doOnConnected { connection ->
connection.addHandlerLast(ReadTimeoutHandler(readTimeout, TimeUnit.MILLISECONDS))
}))
.build()
Response model:
#JsonIgnoreProperties(ignoreUnknown = true)
data class Conversation(
val replyTimestamp: Map<String, String>,
)
How can I return default response (conversation with empty map) in case of timeout instead of failing with an exception?
Update:
I tried suggestion of JArgente below: updated the call with awaitExchange and set valid WireMock response with delay (1010 ms) that is longer that timeout (1000 ms).
Result is still ReadTimeoutException so looking at http status code does not help in this case.
private val defaultConversation = Conversation(emptyMap())
suspend fun conversation(id: String): Conversation =
client.get()
.uri("/conversation/{id}", id)
.awaitExchange {
response -> if (response.statusCode() == HttpStatus.OK) response.awaitBody() else defaultConversation
}
Response:
{
"replyTimestamp": {
"1": "2021-02-23T15:30:28.753Z",
"2": "2021-02-23T16:30:28.753Z"
}
}
Mock config for it:
{
"mappings":
[
{
"priority": 1,
"request": {
"method": "GET",
"urlPathPattern": "/conversation/1"
},
"response": {
"status": 200,
"fixedDelayMilliseconds": 1010,
"headers": {
"content-type": "application/json;charset=utf-8"
},
"bodyFileName": "conversation1.json"
}
}
]
}

You are getting an exception because in your method, you are expecting to get a response of type Conversation, but because you are receiving an error, the body is different.
The way you should handle the response, in this case, should be first, to look at the HTTP status code and then convert the body accordingly.
Here is an example from spring.io
https://docs.spring.io/spring-framework/docs/current/reference/html/web-reactive.html
In your case, when you receive the status code of the error you should create a new empty Conversation and return it
val entity = client.get()
.uri("/conversation/{id}", id)
.accept(MediaType.APPLICATION_JSON)
.awaitExchange {
if (response.statusCode() == HttpStatus.OK) {
return response.awaitBody<Conversation>()
}
else if (response.statusCode().is4xxClientError) {
return response.awaitBody<ErrorContainer>()
}
else {
throw response.createExceptionAndAwait()
}
}

As per Martin's suggestion ended up just wrapping call in a try/catch:
suspend inline fun <reified T : Any> WebClient.ResponseSpec.tryAwaitBodyOrElseLogged(default: T, log: Logger) : T =
try {
awaitBody()
} catch (e: Exception) {
log.warn("Remote request failed, returning default value ($default)", e)
default
}
private val log = LoggerFactory.getLogger(this::class.java)
private val default = Conversation(emptyMap())
suspend fun conversation(id: String): Conversation =
client.get()
.uri("/conversation/{id}", id)
.retrieve()
.tryAwaitBodyOrElseLogged(default, log)
I thought there is some idiomatic way but this works fine.

Related

How to write tescase for webclient onstatus method

I am new to spring webclient and i have written a generic which can be used to consume rest apis in my application:
private Function<ClientResponse, Mono<? extends Throwable>> errorStrategy() {
return response -> {
return response.bodyToMono(Errors.class).flatMap(errorResponse -> {
log.info("Track Error ----> {}", errorResponse.getErrorCode());
Errors errors = new Errors(errorResponse.getErrorMsg());
return Mono.error(errors);
});
};
}
public Mono<EnterpriseSearchResponse> getCustomerID(EnterpriseSearchRequest searchRequest) {
Mono<EnterpriseSearchResponse> response = this.client.method(HttpMethod.GET)
.uri(enterpriseSearchURI + enterpriseSearchContext)
.header("Authorization", "Bearer " + enterpriseSearchAuthToken)
.accept(new MediaType[] { MediaType.APPLICATION_JSON }).bodyValue(searchRequest).retrieve()
.onStatus(HttpStatus::is5xxServerError, errorStrategy())
.onStatus(HttpStatus::is4xxClientError, errorStrategy()).bodyToMono(EnterpriseSearchResponse.class);
return response;
}
i wanted to write junit test case for if consumed rest-api return 404 or 500 error.
can someone suggest how to achieve that?

Okhttp3.14 Stream closed

I have some usage issue about okhttp in 3.14.9 release
if i want add LoggingInterceptor for each request, how can i get response body, which can only consume once?
And follow is my attemp
public class LoggingRequestInterceptor implements Interceptor {
#Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
Response response = chain.proceed(request);
log.debug(
"{}, {}, {}, {}, {}, {}, {}",
request.url(),
request.method(),
JSONUtil.toJsonStr(request.body()),
request.headers(),
dup.body() == null ? null : dup.body().string());
return response;
}
}
It will throw exception of stream closed, how to fix it?
Use peekBody for this
val client = OkHttpClient.Builder()
.addInterceptor {
val response = it.proceed(it.request())
println(response.peekBody(1000000).string())
response
}
.build()
I do research for this issue. We can use buffer, which is in RequestBody-source-getBuffer.
Working code is below:
public String getResponseBody(Response response) {
try {
ResponseBody responseBody = response.body();
if (!ObjectUtil.isNull(responseBody)) {
BufferedSource source = responseBody.source();
source.request(Long.MAX_VALUE);
Buffer buffer = source.getBuffer();
return buffer.clone().readString(UTF8);
}
} catch (IOException e) {
log.error("get response body failed: ", e);
}
return null;
}
From ernest-kiwele:
Using a try with a resources block with the response causes this "closed" problem when the response body is read outside of the try block:
try (Response response = client.newCall(request.build()).execute()) {
return response;
} //response.close() called implicitly by the JVM
The fix is to restructure the code to only use the response within the try block.

How to handle exceptions thrown by the webclient?

I'm trying to figure out how to log exceptions from the webclient, whatever the error status code that is returned from the api that gets called.
I've seen the following implementation:
.onStatus(status -> status.value() != HttpStatus.OK.value(),
rs -> rs.bodyToMono(String.class).map(body -> new IOException(String.format(
"Response HTTP code is different from 200: %s, body: '%s'", rs.statusCode(), body))))
Another example I've seen uses a filter. I guess this filter could be used to log errors as well, aside from requests like in this example:
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();
}
But are we serious that there is no dedicated exception handler to this thing?
Found it.
bodyToMono throws a WebClientException if the status code is 4xx (client error) or 5xx (Server error).
Full implementation of the service:
#Service
public class FacebookService {
private static final Logger LOG = LoggerFactory.getLogger(FacebookService.class);
private static final String URL_DEBUG = "https://graph.facebook.com/debug_token";
private WebClient webClient;
public FacebookService() {
webClient = WebClient.builder()
.filter(logRequest())
.build();
}
public Mono<DebugTokenResponse> verifyFbAccessToken(String fbAccessToken, String fbAppToken) {
LOG.info("verifyFacebookToken for " + String.format("fbAccessToken: %s and fbAppToken: %s", fbAccessToken, fbAppToken));
UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(URL_DEBUG)
.queryParam("input_token", fbAccessToken)
.queryParam("access_token", fbAppToken);
return this.webClient.get()
.uri(builder.toUriString())
.retrieve()
.bodyToMono(DebugTokenResponse.class);
}
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);
});
}
#ExceptionHandler(WebClientResponseException.class)
public ResponseEntity<String> handleWebClientResponseException(WebClientResponseException ex) {
LOG.error("Error from WebClient - Status {}, Body {}", ex.getRawStatusCode(), ex.getResponseBodyAsString(), ex);
return ResponseEntity.status(ex.getRawStatusCode()).body(ex.getResponseBodyAsString());
}
}

Get API response error message using Web Client Mono in Spring Boot

I am using webflux Mono (in Spring boot 5) to consume an external API. I am able to get data well when the API response status code is 200, but when the API returns an error I am not able to retrieve the error message from the API. Spring webclient error handler always display the message as
ClientResponse has erroneous status code: 500 Internal Server Error, but when I use PostMan the API returns this JSON response with status code 500.
{
"error": {
"statusCode": 500,
"name": "Error",
"message":"Failed to add object with ID:900 as the object exists",
"stack":"some long message"
}
}
My request using WebClient is as follows
webClient.getWebClient()
.post()
.uri("/api/Card")
.body(BodyInserters.fromObject(cardObject))
.retrieve()
.bodyToMono(String.class)
.doOnSuccess( args -> {
System.out.println(args.toString());
})
.doOnError( e ->{
e.printStackTrace();
System.out.println("Some Error Happend :"+e);
});
My question is, how can I get access to the JSON response when the API returns an Error with status code of 500?
If you want to retrieve the error details:
WebClient webClient = WebClient.builder()
.filter(ExchangeFilterFunction.ofResponseProcessor(clientResponse -> {
if (clientResponse.statusCode().isError()) {
return clientResponse.bodyToMono(ErrorDetails.class)
.flatMap(errorDetails -> Mono.error(new CustomClientException(clientResponse.statusCode(), errorDetails)));
}
return Mono.just(clientResponse);
}))
.build();
with
class CustomClientException extends WebClientException {
private final HttpStatus status;
private final ErrorDetails details;
CustomClientException(HttpStatus status, ErrorDetails details) {
super(status.getReasonPhrase());
this.status = status;
this.details = details;
}
public HttpStatus getStatus() {
return status;
}
public ErrorDetails getDetails() {
return details;
}
}
and with the ErrorDetails class mapping the error body
Per-request variant:
webClient.get()
.exchange()
.map(clientResponse -> {
if (clientResponse.statusCode().isError()) {
return clientResponse.bodyToMono(ErrorDetails.class)
.flatMap(errorDetails -> Mono.error(new CustomClientException(clientResponse.statusCode(), errorDetails)));
}
return clientResponse;
})
Just as #Frischling suggested, I changed my request to look as follows
return webClient.getWebClient()
.post()
.uri("/api/Card")
.body(BodyInserters.fromObject(cardObject))
.exchange()
.flatMap(clientResponse -> {
if (clientResponse.statusCode().is5xxServerError()) {
clientResponse.body((clientHttpResponse, context) -> {
return clientHttpResponse.getBody();
});
return clientResponse.bodyToMono(String.class);
}
else
return clientResponse.bodyToMono(String.class);
});
I also noted that there's a couple of status codes from 1xx to 5xx, which is going to make my error handling easier for different cases
Look at .onErrorMap(), that gives you the exception to look at. Since you might also need the body() of the exchange() to look at, don't use retrieve, but
.exchange().flatMap((ClientResponse) response -> ....);

How to log request and response bodies in Spring WebFlux

I want to have centralised logging for requests and responses in my REST API on Spring WebFlux with Kotlin. So far I've tried this approaches
#Bean
fun apiRouter() = router {
(accept(MediaType.APPLICATION_JSON) and "/api").nest {
"/user".nest {
GET("/", userHandler::listUsers)
POST("/{userId}", userHandler::updateUser)
}
}
}.filter { request, next ->
logger.info { "Processing request $request with body ${request.bodyToMono<String>()}" }
next.handle(request).doOnSuccess { logger.info { "Handling with response $it" } }
}
Here request method and path log successfully but the body is Mono, so how should I log it? Should it be the other way around and I have to subscribe on request body Mono and log it in the callback?
Another problem is that ServerResponse interface here doesn't have access to the response body. How can I get it here?
Another approach I've tried is using WebFilter
#Bean
fun loggingFilter(): WebFilter =
WebFilter { exchange, chain ->
val request = exchange.request
logger.info { "Processing request method=${request.method} path=${request.path.pathWithinApplication()} params=[${request.queryParams}] body=[${request.body}]" }
val result = chain.filter(exchange)
logger.info { "Handling with response ${exchange.response}" }
return#WebFilter result
}
Same problem here: request body is Flux and no response body.
Is there a way to access full request and response for logging from some filters? What don't I understand?
This is more or less similar to the situation in Spring MVC.
In Spring MVC, you can use a AbstractRequestLoggingFilter filter and ContentCachingRequestWrapper and/or ContentCachingResponseWrapper. Many tradeoffs here:
if you'd like to access servlet request attributes, you need to actually read and parse the request body
logging the request body means buffering the request body, which can use a significant amount of memory
if you'd like to access the response body, you need to wrap the response and buffer the response body as it's being written, for later retrieval
ContentCaching*Wrapper classes don't exist in WebFlux but you could create similar ones. But keep in mind other points here:
buffering data in memory somehow goes against the reactive stack, since we're trying there to be very efficient with the available resources
you should not tamper with the actual flow of data and flush more/less often than expected, otherwise you'd risk breaking streaming uses cases
at that level, you only have access to DataBuffer instances, which are (roughly) memory-efficient byte arrays. Those belong to buffer pools and are recycled for other exchanges. If those aren't properly retained/released, memory leaks are created (and buffering data for later consumption certainly fits that scenario)
again at that level, it's only bytes and you don't have access to any codec to parse the HTTP body. I'd forget about buffering the content if it's not human-readable in the first place
Other answers to your question:
yes, the WebFilter is probably the best approach
no, you shouldn't subscribe to the request body otherwise you'd consume data that the handler won't be able to read; you can flatMap on the request and buffer data in doOn operators
wrapping the response should give you access to the response body as it's being written; don't forget about memory leaks, though
I didn't find a good way to log request/response bodies, but if you are just interested in meta data then you can do it like follows.
import org.springframework.http.HttpHeaders
import org.springframework.http.HttpStatus
import org.springframework.http.server.reactive.ServerHttpResponse
import org.springframework.stereotype.Component
import org.springframework.web.server.ServerWebExchange
import org.springframework.web.server.WebFilter
import org.springframework.web.server.WebFilterChain
import reactor.core.publisher.Mono
#Component
class LoggingFilter(val requestLogger: RequestLogger, val requestIdFactory: RequestIdFactory) : WebFilter {
val logger = logger()
override fun filter(exchange: ServerWebExchange, chain: WebFilterChain): Mono<Void> {
logger.info(requestLogger.getRequestMessage(exchange))
val filter = chain.filter(exchange)
exchange.response.beforeCommit {
logger.info(requestLogger.getResponseMessage(exchange))
Mono.empty()
}
return filter
}
}
#Component
class RequestLogger {
fun getRequestMessage(exchange: ServerWebExchange): String {
val request = exchange.request
val method = request.method
val path = request.uri.path
val acceptableMediaTypes = request.headers.accept
val contentType = request.headers.contentType
return ">>> $method $path ${HttpHeaders.ACCEPT}: $acceptableMediaTypes ${HttpHeaders.CONTENT_TYPE}: $contentType"
}
fun getResponseMessage(exchange: ServerWebExchange): String {
val request = exchange.request
val response = exchange.response
val method = request.method
val path = request.uri.path
val statusCode = getStatus(response)
val contentType = response.headers.contentType
return "<<< $method $path HTTP${statusCode.value()} ${statusCode.reasonPhrase} ${HttpHeaders.CONTENT_TYPE}: $contentType"
}
private fun getStatus(response: ServerHttpResponse): HttpStatus =
try {
response.statusCode
} catch (ex: Exception) {
HttpStatus.CONTINUE
}
}
This is what I came up with for java.
public class RequestResponseLoggingFilter implements WebFilter {
#Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
ServerHttpRequest httpRequest = exchange.getRequest();
final String httpUrl = httpRequest.getURI().toString();
ServerHttpRequestDecorator loggingServerHttpRequestDecorator = new ServerHttpRequestDecorator(exchange.getRequest()) {
String requestBody = "";
#Override
public Flux<DataBuffer> getBody() {
return super.getBody().doOnNext(dataBuffer -> {
try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()) {
Channels.newChannel(byteArrayOutputStream).write(dataBuffer.asByteBuffer().asReadOnlyBuffer());
requestBody = IOUtils.toString(byteArrayOutputStream.toByteArray(), "UTF-8");
commonLogger.info(LogMessage.builder()
.step(httpUrl)
.message("log incoming http request")
.stringPayload(requestBody)
.build());
} catch (IOException e) {
commonLogger.error(LogMessage.builder()
.step("log incoming request for " + httpUrl)
.message("fail to log incoming http request")
.errorType("IO exception")
.stringPayload(requestBody)
.build(), e);
}
});
}
};
ServerHttpResponseDecorator loggingServerHttpResponseDecorator = new ServerHttpResponseDecorator(exchange.getResponse()) {
String responseBody = "";
#Override
public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
Mono<DataBuffer> buffer = Mono.from(body);
return super.writeWith(buffer.doOnNext(dataBuffer -> {
try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()) {
Channels.newChannel(byteArrayOutputStream).write(dataBuffer.asByteBuffer().asReadOnlyBuffer());
responseBody = IOUtils.toString(byteArrayOutputStream.toByteArray(), "UTF-8");
commonLogger.info(LogMessage.builder()
.step("log outgoing response for " + httpUrl)
.message("incoming http request")
.stringPayload(responseBody)
.build());
} catch (Exception e) {
commonLogger.error(LogMessage.builder()
.step("log outgoing response for " + httpUrl)
.message("fail to log http response")
.errorType("IO exception")
.stringPayload(responseBody)
.build(), e);
}
}));
}
};
return chain.filter(exchange.mutate().request(loggingServerHttpRequestDecorator).response(loggingServerHttpResponseDecorator).build());
}
}
You can actually enable DEBUG logging for Netty and Reactor-Netty related to see full picture of what's happening. You could play with the below and see what you want and don't. That was the best I could.
reactor.ipc.netty.channel.ChannelOperationsHandler: DEBUG
reactor.ipc.netty.http.server.HttpServer: DEBUG
reactor.ipc.netty.http.client: DEBUG
io.reactivex.netty.protocol.http.client: DEBUG
io.netty.handler: DEBUG
io.netty.handler.proxy.HttpProxyHandler: DEBUG
io.netty.handler.proxy.ProxyHandler: DEBUG
org.springframework.web.reactive.function.client: DEBUG
reactor.ipc.netty.channel: DEBUG
Since Spring Boot 2.2.x, Spring Webflux supports Kotlin coroutines. With coroutines, you can have the advantages of non-blocking calls without having to handle Mono and Flux wrapped objects. It adds extensions to ServerRequest and ServerResponse, adding methods like ServerRequest#awaitBody() and ServerResponse.BodyBuilder.bodyValueAndAwait(body: Any). So you could rewrite you code like this:
#Bean
fun apiRouter() = coRouter {
(accept(MediaType.APPLICATION_JSON) and "/api").nest {
"/user".nest {
/* the handler methods now use ServerRequest and ServerResponse directly
you just need to add suspend before your function declaration:
suspend fun listUsers(ServerRequest req, ServerResponse res) */
GET("/", userHandler::listUsers)
POST("/{userId}", userHandler::updateUser)
}
}
// this filter will be applied to all routes built by this coRouter
filter { request, next ->
// using non-blocking request.awayBody<T>()
logger.info("Processing $request with body ${request.awaitBody<String>()}")
val res = next(request)
logger.info("Handling with Content-Type ${res.headers().contentType} and status code ${res.rawStatusCode()}")
res
}
}
In order to create a WebFilter Bean with coRoutines, I think you can use this CoroutineWebFilter interface (I haven't tested it, I don't know if it works).
I am pretty new to Spring WebFlux, and I don't know how to do it in Kotlin, but should be the same as in Java using WebFilter:
public class PayloadLoggingWebFilter implements WebFilter {
public static final ByteArrayOutputStream EMPTY_BYTE_ARRAY_OUTPUT_STREAM = new ByteArrayOutputStream(0);
private final Logger logger;
private final boolean encodeBytes;
public PayloadLoggingWebFilter(Logger logger) {
this(logger, false);
}
public PayloadLoggingWebFilter(Logger logger, boolean encodeBytes) {
this.logger = logger;
this.encodeBytes = encodeBytes;
}
#Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
if (logger.isInfoEnabled()) {
return chain.filter(decorate(exchange));
} else {
return chain.filter(exchange);
}
}
private ServerWebExchange decorate(ServerWebExchange exchange) {
final ServerHttpRequest decorated = new ServerHttpRequestDecorator(exchange.getRequest()) {
#Override
public Flux<DataBuffer> getBody() {
if (logger.isDebugEnabled()) {
final ByteArrayOutputStream baos = new ByteArrayOutputStream();
return super.getBody().map(dataBuffer -> {
try {
Channels.newChannel(baos).write(dataBuffer.asByteBuffer().asReadOnlyBuffer());
} catch (IOException e) {
logger.error("Unable to log input request due to an error", e);
}
return dataBuffer;
}).doOnComplete(() -> flushLog(baos));
} else {
return super.getBody().doOnComplete(() -> flushLog(EMPTY_BYTE_ARRAY_OUTPUT_STREAM));
}
}
};
return new ServerWebExchangeDecorator(exchange) {
#Override
public ServerHttpRequest getRequest() {
return decorated;
}
private void flushLog(ByteArrayOutputStream baos) {
ServerHttpRequest request = super.getRequest();
if (logger.isInfoEnabled()) {
StringBuffer data = new StringBuffer();
data.append('[').append(request.getMethodValue())
.append("] '").append(String.valueOf(request.getURI()))
.append("' from ")
.append(
Optional.ofNullable(request.getRemoteAddress())
.map(addr -> addr.getHostString())
.orElse("null")
);
if (logger.isDebugEnabled()) {
data.append(" with payload [\n");
if (encodeBytes) {
data.append(new HexBinaryAdapter().marshal(baos.toByteArray()));
} else {
data.append(baos.toString());
}
data.append("\n]");
logger.debug(data.toString());
} else {
logger.info(data.toString());
}
}
}
};
}
}
Here some tests on this: github
I think this is what Brian Clozel (#brian-clozel) meant.
Here is the GitHub Repo with complete implementation to log both request and response body along with http headers for webflux/java based application...
What Brian said. In addition, logging request/response bodies don't make sense for reactive streaming. If you imagine the data flowing through a pipe as a stream, you don't have the full content at any time unless you buffer it, which defeats the whole point. For small request/response, you can get away with buffering, but then why use the reactive model (other than to impress your coworkers :-) )?
The only reason for logging request/response that I could conjure up is debugging, but with the reactive programming model, debugging method has to be modified too. Project Reactor doc has an excellent section on debugging that you can refer to: http://projectreactor.io/docs/core/snapshot/reference/#debugging
Assuming we are dealing with a simple JSON or XML response, if debug level for corresponding loggers is not sufficient for some reason, one can use string representation before transforming it to object:
Mono<Response> mono = WebClient.create()
.post()
.body(Mono.just(request), Request.class)
.retrieve()
.bodyToMono(String.class)
.doOnNext(this::sideEffectWithResponseAsString)
.map(this::transformToResponse);
the following are the side-effect and transformation methods:
private void sideEffectWithResponseAsString(String response) { ... }
private Response transformToResponse(String response) { /*use Jackson or JAXB*/ }
If your using controller instead of handler best way is aop with annotating you controller class with #Log annotation.And FYI this takes plain json object as request not mono.
#Target(AnnotationTarget.FUNCTION)
#Retention(AnnotationRetention.RUNTIME)
annotation class Log
#Aspect
#Component
class LogAspect {
companion object {
val log = KLogging().logger
}
#Around("#annotation(Log)")
#Throws(Throwable::class)
fun logAround(joinPoint: ProceedingJoinPoint): Any? {
val start = System.currentTimeMillis()
val result = joinPoint.proceed()
return if (result is Mono<*>) result.doOnSuccess(getConsumer(joinPoint, start)) else result
}
fun getConsumer(joinPoint: ProceedingJoinPoint, start: Long): Consumer<Any>? {
return Consumer {
var response = ""
if (Objects.nonNull(it)) response = it.toString()
log.info(
"Enter: {}.{}() with argument[s] = {}",
joinPoint.signature.declaringTypeName, joinPoint.signature.name,
joinPoint.args
)
log.info(
"Exit: {}.{}() had arguments = {}, with result = {}, Execution time = {} ms",
joinPoint.signature.declaringTypeName, joinPoint.signature.name,
joinPoint.args[0],
response, System.currentTimeMillis() - start
)
}
}
}
I think the appropriate thing to do here is to write the contents of each request to a file in an asynchronous manner (java.nio) and set up an interval that reads those request body files asynchrolusly and writes them to the log in a memory usage aware manner (atleast one file at a time but up too 100 mb at a time) and after logging them removes the files from disk.
Ivan Lymar's answer but in Kotlin:
import org.apache.commons.io.IOUtils
import org.reactivestreams.Publisher
import org.springframework.core.io.buffer.DataBuffer
import org.springframework.http.server.reactive.ServerHttpRequestDecorator
import org.springframework.http.server.reactive.ServerHttpResponseDecorator
import org.springframework.stereotype.Component
import org.springframework.web.server.ServerWebExchange
import org.springframework.web.server.WebFilter
import org.springframework.web.server.WebFilterChain
import reactor.core.publisher.Flux
import reactor.core.publisher.Mono
import java.io.ByteArrayOutputStream
import java.io.IOException
import java.nio.channels.Channels
#Component
class LoggingWebFilter : WebFilter {
override fun filter(exchange: ServerWebExchange, chain: WebFilterChain): Mono<Void> {
val httpRequest = exchange.request
val httpUrl = httpRequest.uri.toString()
val loggingServerHttpRequestDecorator: ServerHttpRequestDecorator =
object : ServerHttpRequestDecorator(exchange.request) {
var requestBody = ""
override fun getBody(): Flux<DataBuffer> {
return super.getBody().doOnNext { dataBuffer: DataBuffer ->
try {
ByteArrayOutputStream().use { byteArrayOutputStream ->
Channels.newChannel(byteArrayOutputStream)
.write(dataBuffer.asByteBuffer().asReadOnlyBuffer())
requestBody =
IOUtils.toString(
byteArrayOutputStream.toByteArray(),
"UTF-8"
)
log.info(
"Logging Request Filter: {} {}",
httpUrl,
requestBody
)
}
} catch (e: IOException) {
log.error(
"Logging Request Filter Error: {} {}",
httpUrl,
requestBody,
e
)
}
}
}
}
val loggingServerHttpResponseDecorator: ServerHttpResponseDecorator =
object : ServerHttpResponseDecorator(exchange.response) {
var responseBody = ""
override fun writeWith(body: Publisher<out DataBuffer>): Mono<Void> {
val buffer: Mono<DataBuffer> = Mono.from(body)
return super.writeWith(
buffer.doOnNext { dataBuffer: DataBuffer ->
try {
ByteArrayOutputStream().use { byteArrayOutputStream ->
Channels.newChannel(byteArrayOutputStream)
.write(
dataBuffer
.asByteBuffer()
.asReadOnlyBuffer()
)
responseBody = IOUtils.toString(
byteArrayOutputStream.toByteArray(),
"UTF-8"
)
log.info(
"Logging Response Filter: {} {}",
httpUrl,
responseBody
)
}
} catch (e: Exception) {
log.error(
"Logging Response Filter Error: {} {}",
httpUrl,
responseBody,
e
)
}
}
)
}
}
return chain.filter(
exchange.mutate().request(loggingServerHttpRequestDecorator)
.response(loggingServerHttpResponseDecorator)
.build()
)
}
}

Resources