首页 文章

如何在Spring WebFlux中记录请求和响应主体

提问于
浏览
11

我希望使用Kotlin在Spring WebFlux上的REST API中集中记录请求和响应 . 到目前为止,我已经尝试过这种方法

@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" } }
}

这里请求方法和路径日志成功,但正文是 Mono ,那么我应该如何记录呢?应该是相反的方式,我必须订阅请求正文 Mono 并将其记录在回调中?另一个问题是这里的 ServerResponse 接口无法访问响应主体 . 我怎么能在这里得到它?


我试过的另一种方法是使用 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
        }

同样的问题:请求正文是 Flux ,没有响应正文 .

有没有办法从某些过滤器访问完整的请求和响应?我不明白什么?

5 回答

  • 0

    布莱恩说的话 . 此外,日志记录请求/响应主体在任何时候都不具有完整内容,除非您缓冲它,这会破坏整个点 . 对于小的请求/响应,你可以逃避缓冲,但为什么要使用反应模型(除了给你的同事留下深刻印象:-))?

    我可以想到的记录请求/响应的唯一原因是调试,但是使用反应式编程模型,调试方法也必须进行修改 . Project Reactor doc有一个很好的调试部分,你可以参考:http://projectreactor.io/docs/core/snapshot/reference/#debugging

  • 1

    这或多或少类似于Spring MVC中的情况 .

    在Spring MVC中,您可以使用 AbstractRequestLoggingFilter 过滤器和 ContentCachingRequestWrapper 和/或 ContentCachingResponseWrapper . 这里有许多权衡:

    • 如果您想访问servlet请求属性,则需要实际读取和解析请求主体

    • 记录请求体意味着缓冲请求体,它可以使用大量的内存

    • 如果您正在编写'd like to access the response body, you need to wrap the response and buffer the response body as it',以便以后检索

    WebFlux中不存在 ContentCaching*Wrapper 类,但您可以创建类似的类 . 但请记住其他要点:

    内存中的

    • 缓冲数据以某种方式与反应堆栈相反,因为我们正在尝试使用可用资源非常高效

    • 你不应该篡改实际的数据流并且比预期更多/更少的冲洗,否则你冒着打破流式使用案例的风险

    • 在该级别,您只能访问 DataBuffer 实例,这些实例是(大致)内存高效的字节数组 . 那些属于缓冲池,并被回收用于其他交换 . 如果没有正确保留/释放它们,则会创建内存泄漏(缓冲数据以供以后使用,这当然适合这种情况)

    • 再次在该级别,它可以访问任何解析HTTP正文的编解码器 . 我首先不是人类可读的

    您问题的其他答案:

    • 是的, WebFilter 可能是最好的方法

    • 不,你不应该使用处理程序无法读取的数据;你可以 flatMapdoOn 运算符中的请求和缓冲区数据

    • 包装响应应该让你访问响应主体,因为它忘记了内存泄漏,尽管

  • 1

    我没有找到记录请求/响应主体的好方法,但如果您只对元数据感兴趣,那么您可以像下面这样做 .

    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
            }
    }
    
  • 8

    我是Spring WebFlux的新手,我不知道如何在Kotlin中做到这一点,但应该与使用WebFilter的Java相同:

    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());
                        }
    
                    }
                }
            };
        }
    
    }
    

    这里有一些测试:github

    我认为这就是 Brian Clozel (@ brian-clozel)的含义 .

  • 7

    您实际上可以为Netty和Reactor-Netty启用DEBUG日志记录,以查看正在发生的事情的全貌 . 你可以玩下面的内容,看看你想要什么,不要 . 那是我能做的最好的 .

    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
    

相关问题