How to log request and response bodies in Spring WebFlux

前端 未结 9 1056
一向
一向 2020-11-27 16:41

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         


        
相关标签:
9条回答
  • 2020-11-27 16:59

    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
    0 讨论(0)
  • 2020-11-27 17:05

    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
    
    0 讨论(0)
  • 2020-11-27 17:05

    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

    0 讨论(0)
  • 2020-11-27 17:08

    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
            }
    }
    
    0 讨论(0)
  • 2020-11-27 17:09

    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());
        }
    
    }
    
    0 讨论(0)
  • 2020-11-27 17:09

    Here is the GitHub Repo with complete implementation to log both request and response body along with http headers for webflux/java based application...

    0 讨论(0)
提交回复
热议问题