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 apiR
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:
ContentCaching*Wrapper
classes don't exist in WebFlux but you could create similar ones. But keep in mind other points here:
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)Other answers to your question:
WebFilter
is probably the best approachflatMap
on the request and buffer data in doOn
operatorsYou 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
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
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());
}
}
Here is the GitHub Repo with complete implementation to log both request and response body along with http headers for webflux/java based application...