Как регистрировать тела запросов и ответов в 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