1. Introduction

This tutorial explains the challenges of request/response body logging in Spring WebFlux and then shows how to use a custom WebFilter to achieve the goal.

2. Limitations and Challenges

Spring WebFlux doesn’t provide any out-of-the-box logging utility to log the body of incoming calls. Therefore, we have to create our custom WebFilter to add a log decoration to the requests and responses. As soon as we read the request or response body for logging, the input stream is consumed, so the controller or client doesn’t receive the body.

Hence, the solution is to cache the request and response in the decorator or copy the InputStream to a new stream and pass it to the logger. However, we should be careful with this duplication that could increase memory usage, especially with incoming calls with a heavy payload.

3. WebFilter for Logging

Let’s start with LoggingWebFilter, which wraps the instance of ServerWebExchange in our custom ServerWebExchangeDecorator enhanced for logging:

@Component
class LoggingWebFilter : WebFilter {
    @Autowired
    lateinit var log: Logger

    override fun filter(exchange: ServerWebExchange, chain: WebFilterChain) = chain.filter(LoggingWebExchange(log, exchange))
}

Then we have LoggingWebExchange that decorates request and response instances:

class LoggingWebExchange(log: Logger, delegate: ServerWebExchange) : ServerWebExchangeDecorator(delegate) {
    private val requestDecorator: LoggingRequestDecorator = LoggingRequestDecorator(log, delegate.request)
    private val responseDecorator: LoggingResponseDecorator = LoggingResponseDecorator(log, delegate.response)
    override fun getRequest(): ServerHttpRequest {
        return requestDecorator
    }

    override fun getResponse(): ServerHttpResponse {
        return responseDecorator
    }
}

LoggingRequestDecorator and LoggingResponseDecorator encapsulate the logic of logging, and we see them accordingly in the following sections.

4. Logging the Request

We could extend LoggingRequestDecorator from ServerHttpRequestDecorator, then override the getBody method to enhance the request:

class LoggingRequestDecorator internal constructor(log: Logger, delegate: ServerHttpRequest) : ServerHttpRequestDecorator(delegate) {

    private val body: Flux<DataBuffer>?

    override fun getBody(): Flux<DataBuffer> {
        return body!!
    }

    init {
        if (log.isDebugEnabled) {
            val path = delegate.uri.path
            val query = delegate.uri.query
            val method = Optional.ofNullable(delegate.method).orElse(HttpMethod.GET).name
            val headers = delegate.headers.asString()
            log.debug(
                "{} {}\n {}", method, path + (if (StringUtils.hasText(query)) "?$query" else ""), headers
            )
            body = super.getBody().doOnNext { buffer: DataBuffer ->
                    val bodyStream = ByteArrayOutputStream()
                    Channels.newChannel(bodyStream).write(buffer.asByteBuffer().asReadOnlyBuffer())
                    log.debug("{}: {}", "request", String(bodyStream.toByteArray()))
            }
        } else {
            body = super.getBody()
        }
    }
}

We check the log level, and if the log level is matched, we add logging logic to the onNext of getBody. For logging, we use ByteBuffer#asReadOnlyBuffer to duplicate the InputStream, and we consume it with our Logger.

5. Logging the Response

Let’s continue with our LoggingResponseDecorator class. It extends ServerHttpResponseDecorator and then overrides the writeWith method:

class LoggingResponseDecorator internal constructor(val log: Logger, delegate: ServerHttpResponse) : ServerHttpResponseDecorator(delegate) {

    override fun writeWith(body: Publisher<out DataBuffer>): Mono<Void> {
        return super.writeWith(Flux.from(body)
            .doOnNext { buffer: DataBuffer ->
                if (log.isDebugEnabled) {
                    val bodyStream = ByteArrayOutputStream()
                    Channels.newChannel(bodyStream).write(buffer.asByteBuffer().asReadOnlyBuffer())
                    log.debug("{}: {} - {} : {}", "response", String(bodyStream.toByteArray()), 
                      "header", delegate.headers.asString())
                }
            })
    }
}

As we can see, the logging logic for the response is the same as the logic for logging the request. We get the Flux instance of the body publisher, then log it in the onNext method.

6. Conclusion

In this article, we presented a solution for logging requests and responses of a Spring WebFlux application in Kotlin. We first created a WebFilter, and then we learned how to decorate incoming calls to have the ability to log their request and response bodies using our preferred logging format.

As always, the implementation is available over on GitHub.

Comments are open for 30 days after publishing a post. For any issues past this date, use the Contact form on the site.