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?
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:
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:
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)Autres réponses à votre question:
WebFilter
est probablement la meilleure approcheflatMap
sur la demande et mettre les données en mémoire tampon dans les opérateurs doOn
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
}
}
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
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.
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*/ }