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

我想用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()}" } 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和没有响应的身体。

有没有办法从一些filter访问完整的请求和响应? 我不明白什么?

这与Spring MVC中的情况差不多。

在Spring MVC中,可以使用AbstractRequestLoggingFilterfilter和ContentCachingRequestWrapper和/或ContentCachingResponseWrapper 。 许多折中在这里:

  • 如果您想访问servlet请求属性,则需要实际读取和解析请求主体
  • 记录请求主体意味着缓冲请求主体,这可以使用大量的内存
  • 如果您想访问响应主体,则需要将响应封装起来,并在写入响应主体时进行缓冲,以供日后检索

ContentCaching*Wrapper类在WebFlux中不存在,但可以创建类似的包。 但请牢记这里的其他要点:

  • 内存中的数据缓存不利于反应堆栈,因为我们正在尝试使用可用的资源非常有效
  • 你不应该篡改数据的实际流量,并且比预期更频繁地刷新,否则你将冒险打破流式使用案例
  • 在那个级别上,你只能访问DataBuffer实例,这些实例是(大致)内存高效的字节数组。 那些属于缓冲池并被回收用于其他交换。 如果没有正确保留/释放这些内存,将会创建内存泄漏(并且缓冲数据以供以后使用当然适合该场景)
  • 再次在这个级别,它只是字节,你不能访问任何编解码器来解析HTTP正文。 如果这些内容不是人类可读的,我会忘记缓冲内容

你的问题的其他答案:

  • 是的, WebFilter可能是最好的方法
  • 不,你不应该订阅请求体,否则你会使用处理程序将无法读取的数据; 您可以对请求进行flatMap并在doOn运算符中缓冲数据
  • 包装响应应该让你访问正在写入的响应主体; 不过不要忘记内存泄漏

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

 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 { 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 } } 

我对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 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 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)的意思。

Brian说的。 另外,记录请求/响应主体对于反应流也是没有意义的。 如果您想象数据流经过管道,那么除非您缓冲它, 否则您没有任何全部内容,从而导致整个问题失败。 对于小的请求/响应,你可以放弃缓冲,但为什么使用反应模型(除了打动你的同事:-))?

记录请求/响应的唯一理由是调试,但对于反应式编程模型,调试方法也必须修改。 项目反应堆文档有一个很好的调试部分,您可以参考: http : //projectreactor.io/docs/core/snapshot/reference/#debugging