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
であり、応答の本文はありません。
いくつかのフィルターからのロギングの完全な要求と応答にアクセスする方法はありますか?何がわからないの?
これは、Spring MVCの状況にほぼ似ています。
Spring MVCでは、AbstractRequestLoggingFilter
フィルターとContentCachingRequestWrapper
および/またはContentCachingResponseWrapper
を使用できます。ここで多くのトレードオフ:
ContentCaching*Wrapper
クラスはWebFluxには存在しませんが、同様のクラスを作成できます。ただし、ここで他の点に留意してください。
DataBuffer
インスタンスにのみアクセスできます。これは、(おおよそ)メモリ効率の高いバイト配列です。それらはバッファプールに属し、他の交換のためにリサイクルされます。それらが適切に保持/解放されない場合、メモリリークが発生します(そして後で消費するためにデータをバッファリングすることは確かにそのシナリオに適合します)あなたの質問に対する他の回答:
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でそれを行う方法はわかりませんが、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)の意味だと思います。
ブライアンが言ったこと。さらに、リクエスト/レスポンスのロギングは、リアクティブストリーミングでは意味がありません。パイプを介して流れるデータがストリームであると想像すると、いつでも完全なコンテンツを取得することはできませんnlessバッファリングすると、ポイント全体が無効になります。小さいリクエスト/レスポンスの場合、バッファリングを回避できますが、なぜリアクティブモデルを使用するのですか(同僚に印象を与える以外に:-))。
私が思いつくことができるリクエスト/レスポンスを記録する唯一の理由はデバッグですが、リアクティブプログラミングモデルでは、デバッグ方法も変更する必要があります。 Project Reactorのドキュメントには、デバッグに関する優れたセクションがあり、参照できます。 http://projectreactor.io/docs/core/snapshot/reference/#debugging
実際に、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
単純な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*/ }