programing

Spring WebFlux에서 요청 및 응답 본문을 기록하는 방법

lastcode 2023. 3. 13. 20:31
반응형

Spring WebFlux에서 요청 및 응답 본문을 기록하는 방법

Spring WebFlux에서 Kotlin과 함께 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" } }
}

및 로그는 은 기기서 here음음음음음음음음음음음음음음음음음음이다.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응답 본체가 없습니다.

일부 필터에서 로깅을 위한 전체 요청 및 응답에 액세스할 수 있는 방법이 있습니까?내가 이해하지 못하는 게 뭐야?

이것은 봄 MVC의 상황과 거의 비슷합니다.

Spring MVC를 할 수 .AbstractRequestLoggingFilter 및 "filter" 와 "filter 및 "filter"ContentCachingRequestWrapper "/"/"ContentCachingResponseWrapper에는 많은 이 있습니다 여기에는 많은 단점이 있습니다.

  • 서블릿 요청 속성에 액세스하려면 실제로 요청 본문을 읽고 해석해야 합니다.
  • 요청 본문을 로깅하는 것은 요청 본문을 버퍼링하는 것을 의미하며, 이는 상당한 양의 메모리를 사용할 수 있습니다.
  • 응답 본문에 액세스하려면 나중에 검색하기 위해 응답 본문을 랩하고 작성 중인 응답 본문을 버퍼링해야 합니다.

ContentCaching*WrapperWebFlux에는 클래스가 없지만 비슷한 클래스를 만들 수 있습니다.러러음음음음음 음

  • 메모리 내의 데이터를 버퍼링하는 것은 리액티브스택에 반하는 것입니다.사용 가능한 자원을 효율적으로 사용하기 위해 노력하고 있기 때문입니다.
  • 실제 데이터 흐름을 조작하거나 예상보다 적은 빈도로 플러시하지 마십시오.그렇지 않으면 스트리밍 사용 사례를 위반할 위험이 있습니다.
  • 에서는, 「」, 「」의 액세스 에 할 수 .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
        }
}

이게 제가 자바용으로 생각해낸 거예요.

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());
    }

}

실제로 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

Spring Boot 2.2.x 이후 Spring Webflux는 Kotlin 코루틴을 지원합니다.코루틴을 사용하면 모노 및 플럭스로 둘러싸인 오브젝트를 처리할 필요 없이 논블로킹콜의 이점을 얻을 수 있습니다.ServerRequest ServerResponse에 확장을 추가하여 다음과 같은 메서드를 추가합니다.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 인터페이스를 사용하면 될 것 같습니다(테스트를 해보지 않았기 때문에 동작할지는 모르겠습니다).

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)은 이런 의미였던 것 같아요.

여기 webflux/java 기반 애플리케이션용 http 헤더와 함께 요청과 응답 본문을 모두 기록하는 완전구현이 있는 GitHub Repo가 있습니다.

Brian이 말한 것.또한 로깅 요청/응답 주체는 사후 스트리밍에 적합하지 않습니다.데이터가 파이프를 통해 스트림으로 흐른다고 가정할 경우 데이터를 버퍼링하지 않는 한 언제든지 전체 컨텐츠를 가질 수 없으며, 이로 인해 전체 포인트가 손실됩니다.작은 요청/응답에도 버퍼링에서 벗어날 수 있지만, (동료에게 깊은 인상을 주는 것 이외에는) 왜 사후 대응 모델을 사용합니까?

로깅 요청/응답이 생각나는 이유는 디버깅뿐이지만 리액티브 프로그래밍 모델에서는 디버깅 방식도 수정해야 합니다.Project Reactor 문서에는 다음 문서를 참조할 수 있는 디버깅에 관한 훌륭한 섹션이 있습니다.http://projectreactor.io/docs/core/snapshot/reference/ #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*/ }    

핸들러 대신 컨트롤러를 사용하는 가장 좋은 방법은 @Log 주석을 사용하여 컨트롤러 클래스에 주석을 다는 것입니다.참고로 이것은 mono가 아닌 request로서 플레인 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
            )
        }
    }
}

여기서 해야 할 일은 각 요청의 내용을 파일에 비동기식으로 쓰고(java.nio), 이러한 요청 본문 파일을 비동기식으로 읽고 메모리 사용량 인식 방식으로 로그에 쓰는 간격을 설정하는 것입니다(한 번에 적어도1개의 파일이 있지만 한 번에 100MB가 넘는 파일이 기록되면 해당 파일이 삭제됨).e파일을 디스크에서 가져옵니다.

Ivan Lymar의 답변은 Kotlin:

import org.apache.commons.io.IOUtils
import org.reactivestreams.Publisher
import org.springframework.core.io.buffer.DataBuffer
import org.springframework.http.server.reactive.ServerHttpRequestDecorator
import org.springframework.http.server.reactive.ServerHttpResponseDecorator
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.Flux
import reactor.core.publisher.Mono
import java.io.ByteArrayOutputStream
import java.io.IOException
import java.nio.channels.Channels

@Component
class LoggingWebFilter : WebFilter {

    override fun filter(exchange: ServerWebExchange, chain: WebFilterChain): Mono<Void> {
        val httpRequest = exchange.request
        val httpUrl = httpRequest.uri.toString()
        val loggingServerHttpRequestDecorator: ServerHttpRequestDecorator =
            object : ServerHttpRequestDecorator(exchange.request) {
                var requestBody = ""
                override fun getBody(): Flux<DataBuffer> {
                    return super.getBody().doOnNext { dataBuffer: DataBuffer ->
                        try {
                            ByteArrayOutputStream().use { byteArrayOutputStream ->
                                Channels.newChannel(byteArrayOutputStream)
                                    .write(dataBuffer.asByteBuffer().asReadOnlyBuffer())
                                requestBody =
                                    IOUtils.toString(
                                        byteArrayOutputStream.toByteArray(),
                                        "UTF-8"
                                    )
                                log.info(
                                    "Logging Request Filter: {} {}",
                                    httpUrl,
                                    requestBody
                                )
                            }
                        } catch (e: IOException) {
                            log.error(
                                "Logging Request Filter Error: {} {}",
                                httpUrl,
                                requestBody,
                                e
                            )
                        }
                    }
                }
            }

        val loggingServerHttpResponseDecorator: ServerHttpResponseDecorator =
            object : ServerHttpResponseDecorator(exchange.response) {
                var responseBody = ""
                override fun writeWith(body: Publisher<out DataBuffer>): Mono<Void> {
                    val buffer: Mono<DataBuffer> = Mono.from(body)
                    return super.writeWith(
                        buffer.doOnNext { dataBuffer: DataBuffer ->
                            try {
                                ByteArrayOutputStream().use { byteArrayOutputStream ->
                                    Channels.newChannel(byteArrayOutputStream)
                                        .write(
                                            dataBuffer
                                                .asByteBuffer()
                                                .asReadOnlyBuffer()
                                        )
                                    responseBody = IOUtils.toString(
                                        byteArrayOutputStream.toByteArray(),
                                        "UTF-8"
                                    )
                                    log.info(
                                        "Logging Response Filter: {} {}",
                                        httpUrl,
                                        responseBody
                                    )
                                }
                            } catch (e: Exception) {
                                log.error(
                                    "Logging Response Filter Error: {} {}",
                                    httpUrl,
                                    responseBody,
                                    e
                                )
                            }
                        }
                    )
                }
            }
        return chain.filter(
            exchange.mutate().request(loggingServerHttpRequestDecorator)
                .response(loggingServerHttpResponseDecorator)
                .build()
        )
    }
}

언급URL : https://stackoverflow.com/questions/45240005/how-to-log-request-and-response-bodies-in-spring-webflux

반응형