web-dev-qa-db-ja.com

Spring WebFluxでリクエストとレスポンスの本文を記録する方法

Kotlinを使用したSpring WebFluxのAPI 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でサブスクライブし、コールバックに記録する必要がありますか?もう1つの問題は、ここの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であり、応答の本文はありません。

いくつかのフィルターからのロギングの完全な要求と応答にアクセスする方法はありますか?何がわからないの?

20
Koguro

これは、Spring MVCの状況にほぼ似ています。

Spring MVCでは、AbstractRequestLoggingFilterフィルターとContentCachingRequestWrapperおよび/またはContentCachingResponseWrapperを使用できます。ここで多くのトレードオフ:

  • サーブレットのリクエスト属性にアクセスする場合は、リクエスト本文を実際に読み取って解析する必要があります
  • リクエストボディのログ記録は、リクエストボディをバッファリングすることを意味し、大量のメモリを使用する可能性があります
  • 応答本文にアクセスする場合は、後で取得するために、応答をラップし、書き込まれているとおりに応答本文をバッファリングする必要があります

ContentCaching*WrapperクラスはWebFluxには存在しませんが、同様のクラスを作成できます。ただし、ここで他の点に留意してください。

  • メモリにデータをバッファリングすることは、リアクティブスタックに対して何らかの形で影響します。
  • データの実際の流れを改ざんしたり、予想よりも頻繁に/少ない頻度でフラッシュしたりしないでください。そうしないと、ストリーミングのユースケースを壊す危険があります。
  • そのレベルでは、DataBufferインスタンスにのみアクセスできます。これは、(おおよそ)メモリ効率の高いバイト配列です。それらはバッファプールに属し、他の交換のためにリサイクルされます。それらが適切に保持/解放されない場合、メモリリークが発生します(そして後で消費するためにデータをバッファリングすることは確かにそのシナリオに適合します)
  • このレベルでも、バイト単位であり、HTTPボディを解析するためのコーデックにアクセスできません。そもそも人間が読めない場合、コンテンツをバッファリングすることを忘れてしまいます

あなたの質問に対する他の回答:

  • はい、WebFilterがおそらく最良のアプローチです
  • いいえ、リクエスト本文をサブスクライブしないでください。サブスクライブしないと、ハンドラーが読み取れないデータを消費します。リクエストでflatMapでき、doOn演算子でデータをバッファできます
  • 応答をラップすると、作成中の応答本文にアクセスできます。ただし、メモリリークを忘れないでください
13
Brian Clozel

リクエスト/レスポンスの本文を記録する良い方法を見つけられませんでしたが、メタデータに興味があるだけなら、次のようにできます。

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

私は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)の意味だと思います。

1
Silvmike

ブライアンが言ったこと。さらに、リクエスト/レスポンスのロギングは、リアクティブストリーミングでは意味がありません。パイプを介して流れるデータがストリームであると想像すると、いつでも完全なコンテンツを取得することはできませんnlessバッファリングすると、ポイント全体が無効になります。小さいリクエスト/レスポンスの場合、バッファリングを回避できますが、なぜリアクティブモデルを使用するのですか(同僚に印象を与える以外に:-))。

私が思いつくことができるリクエスト/レスポンスを記録する唯一の理由はデバッグですが、リアクティブプログラミングモデルでは、デバッグ方法も変更する必要があります。 Project Reactorのドキュメントには、デバッグに関する優れたセクションがあり、参照できます。 http://projectreactor.io/docs/core/snapshot/reference/#debugging

0
Abhijit Sarkar

実際に、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
0
ROCKY

単純なJSONまたはXML応答を処理していると仮定すると、対応するロガーのdebugレベルが何らかの理由で十分でない場合、オブジェクトに変換する前に文字列表現を使用できます。

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

副作用と変換方法は次のとおりです。

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