如何在Spring WebFlux中记录请求和响应主体

时间:2017-07-21 14:17:34

标签: spring-boot kotlin project-reactor spring-webflux

我希望使用Kotlin在Spring WebFlux上的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并将其记录在回调中? 另一个问题是此处的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且没有回复正文。

有没有办法访问来自某些过滤器的完整请求和响应?我不懂什么?

9 个答案:

答案 0 :(得分:10)

这或多或少类似于Spring MVC中的情况。

在Spring MVC中,您可以使用AbstractRequestLoggingFilter过滤器和ContentCachingRequestWrapper和/或ContentCachingResponseWrapper。这里有许多权衡:

  • 如果您想访问servlet请求属性,则需要实际读取并解析请求正文
  • 记录请求正文意味着缓冲请求正文,这可能会占用大量内存
  • 如果您想访问响应正文,则需要包装响应并在写入时缓冲响应正文,以便日后检索
WebFlux中不存在

ContentCaching*Wrapper个类,但您可以创建类似的类。但请记住其他要点:

  • 缓存内存中的数据以某种方式违背了被动堆栈,因为我们正在尝试使用可用资源非常高效
  • 你不应该篡改实际的数据流量,并且比预期更频繁/更少地冲洗,否则你就有可能破坏流媒体用例
  • 在该级别,您只能访问DataBuffer个实例,这些实例是(大致)内存高效的字节数组。那些属于缓冲池并被回收用于其他交换。如果没有正确保留/释放它们,则会创建内存泄漏(缓冲数据以供以后使用,这当然适合这种情况)
  • 再次在该级别,它只是字节,并且您无权访问任何解析HTTP正文的编解码器。如果内容首先不是人类可读的话,我会忘记缓冲内容

您问题的其他答案:

  • 是的,WebFilter可能是最好的方法
  • 不,您不应订阅请求正文,否则您将使用处理程序无法读取的数据;您可以在flatMap运算符
  • doOn查询请求和缓冲区数据
  • 包装响应应该允许您在写入时访问响应主体;不要忘记内存泄漏,但

答案 1 :(得分:8)

我没有找到记录请求/响应主体的好方法,但如果您只对元数据感兴趣,那么您可以像下面这样做。

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

答案 2 :(得分:3)

这是我为Java想到的。

public class RequestResponseLoggingFilter implements WebFilter {

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
        ServerHttpRequest httpRequest = exchange.getRequest();
        final String httpUrl = httpRequest.getURI().toString();

        ServerHttpRequestDecorator loggingServerHttpRequestDecorator = new ServerHttpRequestDecorator(exchange.getRequest()) {
            String requestBody = "";

            @Override
            public Flux<DataBuffer> getBody() {
                return super.getBody().doOnNext(dataBuffer -> {
                    try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()) {
                        Channels.newChannel(byteArrayOutputStream).write(dataBuffer.asByteBuffer().asReadOnlyBuffer());
                        requestBody = IOUtils.toString(byteArrayOutputStream.toByteArray(), "UTF-8");
                        commonLogger.info(LogMessage.builder()
                                .step(httpUrl)
                                .message("log incoming http request")
                                .stringPayload(requestBody)
                                .build());
                    } catch (IOException e) {
                        commonLogger.error(LogMessage.builder()
                                .step("log incoming request for " + httpUrl)
                                .message("fail to log incoming http request")
                                .errorType("IO exception")
                                .stringPayload(requestBody)
                                .build(), e);
                    }
                });
            }
        };

        ServerHttpResponseDecorator loggingServerHttpResponseDecorator = new ServerHttpResponseDecorator(exchange.getResponse()) {
            String responseBody = "";
            @Override
            public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
                Mono<DataBuffer> buffer = Mono.from(body);
                return super.writeWith(buffer.doOnNext(dataBuffer -> {
                    try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()) {
                        Channels.newChannel(byteArrayOutputStream).write(dataBuffer.asByteBuffer().asReadOnlyBuffer());
                        responseBody = IOUtils.toString(byteArrayOutputStream.toByteArray(), "UTF-8");
                        commonLogger.info(LogMessage.builder()
                                .step("log outgoing response for " + httpUrl)
                                .message("incoming http request")
                                .stringPayload(responseBody)
                                .build());
                    } catch (Exception e) {
                        commonLogger.error(LogMessage.builder()
                                .step("log outgoing response for " + httpUrl)
                                .message("fail to log http response")
                                .errorType("IO exception")
                                .stringPayload(responseBody)
                                .build(), e);
                    }
                }));
            }
        };
        return chain.filter(exchange.mutate().request(loggingServerHttpRequestDecorator).response(loggingServerHttpResponseDecorator).build());
    }

}

答案 3 :(得分:3)

从 Spring Boot 2.2.x 开始,Spring Webflux 支持 Kotlin coroutines。使用协程,您可以拥有非阻塞调用的优势,而无需处理 Mono 和 Flux 包装对象。它为 ServerRequestServerResponse 添加了扩展,添加了 ServerRequest#awaitBody()ServerResponse.BodyBuilder.bodyValueAndAwait(body: Any) 等方法。所以你可以像这样重写你的代码:

@Bean
fun apiRouter() = coRouter {
    (accept(MediaType.APPLICATION_JSON) and "/api").nest {
        "/user".nest {
            /* the handler methods now use ServerRequest and ServerResponse directly
             you just need to add suspend before your function declaration:
             suspend fun listUsers(ServerRequest req, ServerResponse res) */ 
            GET("/", userHandler::listUsers)
            POST("/{userId}", userHandler::updateUser)
        }
    }

    // this filter will be applied to all routes built by this coRouter
    filter { request, next ->
      // using non-blocking request.awayBody<T>()
      logger.info("Processing $request with body ${request.awaitBody<String>()}")
        val res = next(request)
        logger.info("Handling with Content-Type ${res.headers().contentType} and status code ${res.rawStatusCode()}")
        res 
    }
}

为了创建一个带有coRoutines的WebFilter Bean,我觉得你可以使用这个CoroutineWebFilter接口(我没有测试过,不知道是否有效)。

答案 4 :(得分:1)

我是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)的含义。

答案 5 :(得分:1)

此处是GitHub Repo,具有完整的实现,用于针对基于Webflux / java的应用程序记录请求和响应正文以及http标头 ...

答案 6 :(得分:0)

布莱恩说。此外,日志记录请求/响应主体对于响应式流式传输没有意义。如果您想象数据作为流在管道中流动,那么您在任何时候都没有完整的内容,除非您缓冲它,这会使整个点失败。对于小的请求/响应,你可以逃避缓冲,但为什么要使用反应模型(除了给你的同事留下深刻印象:-))?

我可以想到的记录请求/响应的唯一原因是调试,但是对于反应式编程模型,调试方法也必须进行修改。 Project Reactor doc有一个很好的调试部分,您可以参考:http://projectreactor.io/docs/core/snapshot/reference/#debugging

答案 7 :(得分:0)

假设我们正在处理一个简单的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*/ }    

答案 8 :(得分:0)

如果您使用控制器而不是处理程序的最佳方法是aop并使用@Log注释对您的控制器类进行注释,则仅供参考,这将纯json对象作为请求而不是单声道。

@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
annotation class Log

@Aspect
@Component
class LogAspect {
    companion object {
        val log = KLogging().logger
    }

    @Around("@annotation(Log)")
    @Throws(Throwable::class)
    fun logAround(joinPoint: ProceedingJoinPoint): Any? {
        val start = System.currentTimeMillis()
        val result = joinPoint.proceed()
        return if (result is Mono<*>) result.doOnSuccess(getConsumer(joinPoint, start)) else result
    }

    fun getConsumer(joinPoint: ProceedingJoinPoint, start: Long): Consumer<Any>? {
        return Consumer {
            var response = ""
            if (Objects.nonNull(it)) response = it.toString()
            log.info(
                "Enter: {}.{}() with argument[s] = {}",
                joinPoint.signature.declaringTypeName, joinPoint.signature.name,
                joinPoint.args
            )
            log.info(
                "Exit: {}.{}() had arguments = {}, with result = {}, Execution time = {} ms",
                joinPoint.signature.declaringTypeName, joinPoint.signature.name,
                joinPoint.args[0],
                response, System.currentTimeMillis() - start
            )
        }
    }
}