Spring boot 如何在SpringWebFlux中记录请求和响应主体
我希望使用Kotlin在SpringWebFlux上集中记录RESTAPI中的请求和响应。到目前为止,我已经尝试过这种方法Spring boot 如何在SpringWebFlux中记录请求和响应主体,spring-boot,kotlin,project-reactor,spring-webflux,Spring Boot,Kotlin,Project Reactor,Spring Webflux,我希望使用Kotlin在SpringWebFlux上集中记录RESTAPI中的请求和响应。到目前为止,我已经尝试过这种方法 @Bean fun apiRouter() = router { (accept(MediaType.APPLICATION_JSON) and "/api").nest { "/user".nest { GET("/", userHandler::listUsers) POST("/{userId}"
@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" } }
}
同样的问题:请求主体是Flux
,没有响应主体
是否有一种方法可以从某些筛选器访问日志记录的完整请求和响应?我不明白什么?这与Spring MVC中的情况差不多 在SpringMVC中,您可以使用
AbstractRequestLoggingFilter
过滤器和ContentCachingRequestWrapper
和/或ContentCachingResponseWrapper
。这里有许多权衡:
- 如果希望访问servlet请求属性,则需要实际读取和解析请求主体
- 记录请求主体意味着缓冲请求主体,这可能会占用大量内存
- 如果希望访问响应体,则需要在编写响应体时包装响应并缓冲响应体,以便以后检索
ContentCaching*Wrapper
类在WebFlux中不存在,但您可以创建类似的类。但请记住这里的其他要点:
- 在内存中缓冲数据在某种程度上违背了反应堆栈,因为我们正试图在那里利用可用资源实现非常高效
- 您不应该篡改实际的数据流,并且刷新的频率不应该超过预期,否则您就有可能破坏流使用案例
- 在这个级别上,您只能访问
实例,这些实例(大致上)是内存效率高的字节数组。这些属于缓冲池,可循环用于其他交换。如果没有正确地保留/释放这些数据,就会产生内存泄漏(缓冲数据以供以后使用当然适合这种情况)DataBuffer
- 同样,在这个级别上,只有字节,您无法访问任何编解码器来解析HTTP正文。如果内容一开始不是人类可读的,我会忘记缓冲
- 是的,
可能是最好的方法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
}
}
import org.springframework.http.HttpHeaders
导入org.springframework.http.HttpStatus
导入org.springframework.http.server.reactive.ServerHttpResponse
导入org.springframework.stereotype.Component
导入org.springframework.web.server.ServerWebExchange
导入org.springframework.web.server.WebFilter
导入org.springframework.web.server.WebFilterChain
导入reactor.core.publisher.Mono
@组成部分
类LoggingFilter(val-requestLogger:requestLogger,val-requestIdFactory:requestIdFactory):WebFilter{
val logger=logger()
覆盖有趣的过滤器(exchange:ServerWebExchange,链:WebFilterChain):Mono{
logger.info(requestLogger.getRequestMessage(交换))
val过滤器=链过滤器(交换)
exchange.response.beforeCommit{
logger.info(requestLogger.getResponseMessage(交换))
Mono.empty()
}
回流过滤器
}
}
@组成部分
类请求记录器{
fun getRequestMessage(exchange:ServerWebExchange):字符串{
val request=exchange.request
val method=request.method
val path=request.uri.path
val acceptableMediaTypes=request.headers.accept
val contentType=request.headers.contentType
返回“>>>$method$path${HttpHeaders.ACCEPT}:$acceptableMediaTypes${HttpHeaders.CONTENT\u TYPE}:$contentType”
}
趣味getResponseMessage(exchange:ServerWebExchange):字符串{
val request=exchange.request
val response=exchange.response
val method=request.method
val path=request.uri.path
val statusCode=getStatus(响应)
val contentType=response.headers.contentType
return“我对SpringWebFlux非常陌生,我不知道如何在Kotlin中实现它,但应该与在Java中使用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());
}
}
}
};
}
}
公共类PayloadLoggingWebFilter实现WebFilter{
公共静态final ByteArrayOutputStream空\字节\数组\输出\流=新ByteArrayOutputStream(0);
私人最终记录器;
私有最终布尔编码字节;
公共PayloadLoggingWebFilter(记录器){
这(记录器,错误);
}
公共PayloadLoggingWebFilter(日志记录器,布尔编码字节){
this.logger=记录器;
this.encodeBytes=encodeBytes;
}
@凌驾
公共Mono筛选器(服务器WebExchange exchange、WebFilterChain链){
如果(logger.IsInfo已启用()){
返回链。过滤器(装饰(交换));
}否则{
返回链。过滤器(交换);
}
}
专用服务器WebExchange(服务器WebExchange){
final ServerHttpRequest装饰=新的ServerHttpRequestDecorator(exchange.getRequest()){
@凌驾
公共机构({
if(logger.isDebugEnabled()){
最终ByteArrayOutputStream bas=新ByteArrayOutputStream();
返回super.getBody().map(数据缓冲->{
试一试{
Channels.newChannel(baos.write(dataBuffer.asByteBuffer().asReadOnlyBuffer());
}捕获(IOE异常){
logger.error(“由于错误而无法记录输入请求”,e);
}
返回数据
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());
}
}
}
};
}
}
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
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*/ }
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());
}
}
@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
)
}
}
}
@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
}
}