Как регистрировать тела запросов и ответов в Spring WebFlux
Я хочу централизованное ведение журнала для запросов и ответов в моем REST API на Spring WebFlux с Kotlin. До сих пор я пробовал эти подходы
@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 ответов
это более или менее похоже на ситуацию весной MVC.
весной MVC вы можете использовать и ContentCachingRequestWrapper
и/или ContentCachingResponseWrapper
. Много компромиссов здесь:
- если вы хотите получить доступ к атрибутам запроса сервлета, вам нужно фактически прочитать и проанализировать тело запроса
- Регистрация тела запроса означает буферизацию тела запроса, которое может использовать значительный объем памяти
- если вы хотите получить доступ к ответ тело, вам нужно обернуть ответ и буферизировать тело ответа, как оно пишется, для последующего извлечения
ContentCaching*Wrapper
классы не существуют в WebFlux, но вы можете создать аналогичные. Но имейте в виду другие моменты здесь:
- буферизация данных в памяти как-то идет против реактивного стека, так как мы пытаемся быть очень эффективными с доступными ресурсами
- вы не должны вмешиваться в фактический поток данных и промывать более / менее часто, чем ожидалось, в противном случае вы рискуете нарушить потоковое использование cases
- на этом уровне, у вас есть только доступ к
DataBuffer
экземпляры, которые являются (грубо) эффективными для памяти байтовыми массивами. Они принадлежат буферным пулам и рециркулируются для других обменов. Если они неправильно сохранены/выпущены, создаются утечки памяти (и буферизация данных для последующего использования, безусловно, соответствует этому сценарию) - снова на этом уровне, это только байты, и у вас нет доступа к любому кодеку для анализа тела HTTP. Я бы забыл о буферизации контента, если он не читается человеком в первую очередь
другие ответы на ваш вопрос:
- да
WebFilter
это, вероятно, лучший подход - нет, вы не должны подписываться на тело запроса, иначе вы будете потреблять данные, которые обработчик не сможет прочитать; вы можете
flatMap
по запросу и буферным данным вdoOn
операторы - упаковка ответ должен предоставить вам доступ к телу ответа по мере его записи; не забывайте об утечках памяти, хотя
Я не нашел хорошего способа регистрировать тела запросов/ответов, но если вас просто интересуют метаданные, вы можете сделать это следующим образом.
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
}
}
Я довольно новичок в Spring WebFlux, и я не знаю, как это сделать в Kotlin, но должен быть таким же, как в Java с помощью WebFilter:
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
Я думаю, что это Брайан Clozel (@brian-clozel) означало.
вы можете включить ведение журнала отладки для Netty и Reactor-Netty, чтобы увидеть полную картину происходящего. Вы можете играть с нижеприведенным и видеть, что вы хотите, а чего нет. Это было лучшее, что я мог.
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
то, что сказал Брайан. Кроме того, регистрирующие тела запросов/ответов не имеют смысла для реактивной потоковой передачи. Если вы представляете данные, протекающие по трубе как поток, у вас нет полного содержимого в любое время если вы это буфер, который теряется весь смысл. Для небольшого запроса / ответа вы можете уйти с буферизацией, но тогда зачем использовать реактивную модель (кроме как произвести впечатление на своих коллег :-) )?
единственная причина для регистрации запроса / ответа, который я может вызвать отладку, но с реактивной моделью программирования Метод отладки также должен быть изменен. Project Reactor doc имеет отличный раздел по отладке, который вы можете обратиться к:http://projectreactor.io/docs/core/snapshot/reference/#debugging