1. 序章

このチュートリアルでは、 Spring WebFlux でのリクエスト/レスポンスの本文ロギングの課題について説明し、次にカスタムWebFilterを使用して目標を達成する方法を示します。

2. 制限と課題

Spring WebFluxは、着信コールの本文をログに記録するための、すぐに使用できるログユーティリティを提供していません。 したがって、カスタム WebFilter を作成して、要求と応答にログ装飾を追加する必要があります。 ロギングのためにリクエストまたはレスポンスの本文を読み取るとすぐに、入力ストリームが消費されるため、コントローラーまたはクライアントは本文を受信しません。

したがって、解決策は、デコレータに要求と応答をキャッシュするか、InputStreamを新しいストリームにコピーしてロガーに渡すことです。 ただし、特にペイロードが重い着信呼び出しでは、メモリ使用量が増える可能性があるこの重複に注意する必要があります。

3. ロギング用のWebFilter

LoggingWebFilter から始めましょう。これは、ロギング用に拡張されたカスタムServerWebExchangeDecoratorServerWebExchangeのインスタンスをラップします。

@Component
class LoggingWebFilter : WebFilter {
    override fun filter(exchange: ServerWebExchange, chain: WebFilterChain) = chain.filter(LoggingWebExchange(exchange))
}

次に、リクエストインスタンスとレスポンスインスタンスを装飾するLoggingWebExchangeがあります。

class LoggingWebExchange(delegate: ServerWebExchange) : ServerWebExchangeDecorator(delegate) {
    private val requestDecorator: LoggingRequestDecorator = LoggingRequestDecorator(delegate.request)
    private val responseDecorator: LoggingResponseDecorator = LoggingResponseDecorator(delegate.response)
    override fun getRequest(): ServerHttpRequest {
        return requestDecorator
    }

    override fun getResponse(): ServerHttpResponse {
        return responseDecorator
    }
}

LoggingRequestDecoratorおよびLoggingResponseDecoratorは、ロギングのロジックをカプセル化します。これに応じて、次のセクションで説明します。

4. リクエストのログ

LoggingRequestDecoratorServerHttpRequestDecoratorから拡張し、getBodyメソッドをオーバーライドしてリクエストを拡張できます。

class LoggingRequestDecorator internal constructor(delegate: ServerHttpRequest) : ServerHttpRequestDecorator(delegate) {

    private val log = LoggerFactory.getLogger(LoggingRequestDecorator::class.java)

    private val body: Flux<DataBuffer>?

    override fun getBody(): Flux<DataBuffer> {
        return body!!
    }

    init {
        if (log.isDebugEnabled) {
            val path = delegate.uri.path
            val query = delegate.uri.query
            val method = Optional.ofNullable(delegate.method).orElse(HttpMethod.GET).name
            val headers = delegate.headers.asString()
            log.debug(
                "{} {}\n {}", method, path + (if (StringUtils.hasText(query)) "?$query" else ""), headers
            )
            body = super.getBody().doOnNext { buffer: DataBuffer ->
                    val bodyStream = ByteArrayOutputStream()
                    Channels.newChannel(bodyStream).write(buffer.asByteBuffer().asReadOnlyBuffer())
                    log.debug("{}: {}", "request", String(bodyStream.toByteArray()))
            }
        } else {
            body = super.getBody()
        }
    }
}

ログレベルを確認し、ログレベルが一致している場合は、getBodyonNextにログロジックを追加します。 ロギングには、 ByteBuffer#asReadOnlyBufferを使用してInputStream を複製し、Loggerで使用します。

5. 応答のログ

LoggingResponseDecoratorクラスを続けましょう。 ServerHttpResponseDecorator を拡張し、writeWithメソッドをオーバーライドします。

class LoggingResponseDecorator internal constructor(delegate: ServerHttpResponse) : ServerHttpResponseDecorator(delegate) {
    private val log = LoggerFactory.getLogger(LoggingResponseDecorator::class.java)

    override fun writeWith(body: Publisher<out DataBuffer>): Mono<Void> {
        return super.writeWith(Flux.from(body)
            .doOnNext { buffer: DataBuffer ->
                if (log.isDebugEnabled) {
                    val bodyStream = ByteArrayOutputStream()
                    Channels.newChannel(bodyStream).write(buffer.asByteBuffer().asReadOnlyBuffer())
                    log.debug("{}: {}", "response", String(bodyStream.toByteArray()))
                }
            })
    }

    init {
        if (log.isDebugEnabled) {
            log.debug("{}", delegate.headers.asString())
        }
    }
}

ご覧のとおり、応答のロギングロジックは、要求のロギングロジックと同じです。 ボディパブリッシャーのFluxインスタンスを取得し、onNextメソッドにログインします。

6. 結論

この記事では、KotlinでSpringWebFluxアプリケーションのリクエストとレスポンスをログに記録するためのソリューションを紹介しました。 最初にWebFilterを作成し、次に着信コールを装飾して、推奨されるログ形式を使用してリクエストとレスポンスの本文をログに記録する方法を学びました。

いつものように、実装はGitHub利用できます。