web-dev-qa-db-fra.com

Comment enregistrer les corps de requête et de réponse dans Spring WebFlux

Je souhaite une journalisation centralisée des demandes et des réponses dans mon API REST sous Spring WebFlux avec Kotlin. Jusqu'à présent, j'ai essayé cette approche

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

Ici, la méthode de demande et le chemin se connectent avec succès mais le corps est Mono, alors comment dois-je le consigner? Devrait-il en être autrement et je dois souscrire au corps de la demande Mono et le consigner dans le rappel? Un autre problème est que l'interface ServerResponse n'a pas accès au corps de la réponse. Comment puis-je l'obtenir ici?


Une autre approche que j'ai essayée utilise 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
        }

Même problème ici: le corps de la requête est Flux et aucun corps de réponse.

Existe-t-il un moyen d'accéder aux requêtes et réponses complètes pour la journalisation à partir de certains filtres? Qu'est-ce que je ne comprends pas?

13
Koguro

Ceci est plus ou moins similaire à la situation dans Spring MVC.

Dans Spring MVC, vous pouvez utiliser un filtre AbstractRequestLoggingFilter et ContentCachingRequestWrapper et/ou ContentCachingResponseWrapper. Beaucoup de compromis ici:

  • si vous souhaitez accéder aux attributs de requête servlet, vous devez réellement lire et analyser le corps de la requête.
  • enregistrer le corps de la requête signifie mettre en mémoire tampon le corps de la requête, qui peut utiliser une quantité de mémoire importante
  • si vous souhaitez accéder au corps de la réponse, vous devez envelopper la réponse et la mettre en tampon au fur et à mesure de son écriture, pour une récupération ultérieure.

Les classes ContentCaching*Wrapper n'existent pas dans WebFlux mais vous pouvez en créer de semblables. Mais gardez à l'esprit d'autres points ici:

  • la mise en mémoire tampon des données en mémoire va en quelque sorte à l'encontre de la pile réactive, car nous essayons d'être très efficaces avec les ressources disponibles.
  • vous ne devez pas altérer le flux réel de données et vider plus/moins souvent que prévu, sinon vous risqueriez de rompre les cas d'utilisation de la diffusion en continu
  • à ce niveau, vous avez uniquement accès aux instances DataBuffer, qui sont (en gros) des tableaux d'octets efficaces en mémoire. Celles-ci appartiennent à des pools de mémoire tampon et sont recyclées pour d'autres échanges. Si ceux-ci ne sont pas correctement conservés/libérés, des fuites de mémoire sont créées (et la mise en mémoire tampon des données pour une consommation ultérieure correspond certainement à ce scénario)
  • là encore, à ce niveau, il ne s'agit que d'octets et vous n'avez accès à aucun codec pour analyser le corps HTTP. J'oublierais de mettre le contenu en mémoire tampon s'il n'est pas lisible par l'homme en premier lieu

Autres réponses à votre question:

  • oui, la WebFilter est probablement la meilleure approche
  • non, vous ne devriez pas souscrire au corps de la requête, sinon vous consommeriez des données que le gestionnaire ne pourrait pas lire; vous pouvez flatMap sur la demande et mettre les données en mémoire tampon dans les opérateurs doOn
  • envelopper la réponse devrait vous donner accès au corps de la réponse au moment de son écriture; n'oubliez pas les fuites de mémoire, bien que
9
Brian Clozel

Je n'ai pas trouvé un bon moyen de consigner le corps des demandes/réponses, mais si vous êtes simplement intéressé par les métadonnées, vous pouvez le faire comme suit.

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
        }
}
7
Dariusz Bacinski

Vous pouvez réellement activer la journalisation DEBUG pour Netty et Reactor-Netty afin d’avoir une image complète de ce qui se passe. Vous pouvez jouer avec ce qui suit et voir ce que vous voulez et ce que vous ne voulez pas. C'était le mieux que j'ai pu.

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
1
ROCKY

Spring WebFlux est relativement nouveau pour moi et je ne sais pas comment le faire sous Kotlin, mais il devrait être identique à Java avec 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());
                    }

                }
            }
        };
    }

}

Voici quelques tests sur ceci: github

Je pense que c’est ce que Brian Clozel (@ brian-clozel) voulait dire.

0
Silvmike

En supposant qu'il s'agisse d'une simple réponse JSON ou XML, si le niveau debug pour les enregistreurs correspondants ne suffit pas, vous pouvez utiliser la représentation sous forme de chaîne avant de la transformer en objet:

Mono<Response> mono = WebClient.create()
                               .post()
                               .body(Mono.just(request), Request.class)
                               .retrieve()
                               .bodyToMono(String.class)
                               .doOnNext(this::sideEffectWithResponseAsString)
                               .map(this::transformToResponse);

voici les méthodes des effets secondaires et de la transformation:

private void sideEffectWithResponseAsString(String response) { ... }
private Response transformToResponse(String response) { /*use Jackson or JAXB*/ }    
0
jihor