javaspring-bootspring-webfluxspring-boot-admin

How to log request body from a Spring WebClient request using an InstanceExchangeFilterFunction in Spring Boot Admin?


Issue: Unable to Read Request or Payload in Exchange SBA

I'm facing an issue where I cannot read the request or payload within an exchange, apparently due to its basis in the Reactor stack. Unfortunately, the documentation is quite poor, and I need to intercept this request payload.

Referenced Documentation:

https://docs.spring-boot-admin.com/3.4.2/docs/customize/customize_interceptors

I've tried using handmade Spring filters, but encountered an issue: it's impossible to read the request twice in a synchronous filter.

Open Questions:

Is there a way to read the request payload in a Reactor exchange in SBA ? Are there any code examples or tutorials that demonstrate how to intercept and read the request payload in a similar context? my paylaod is in json

enter image description here

enter image description here

@Configuration
public class AuditLogInterceptorReactorConfig {

    private static final Logger log = LoggerFactory.getLogger(AuditLogInterceptorReactorConfig.class);

    @Bean
    public InstanceExchangeFilterFunction auditLog() {
        return (instance, request, next) -> next.exchange(request)
                .doOnSubscribe(subscription -> {
                    if (HttpMethod.POST.equals(request.method())
                            && request.url().getPath().contains("/actuator/loggers/")) {

                        request.body()
                        // Récupérer le nom de l'application depuis l'instance enregistrée
                        String appName = instance.getRegistration().getName();

                        // Extraire le nom du logger depuis l'URL
                        String path = request.url().getPath();
                        String loggerName = path.substring(path.indexOf("/actuator/loggers/") + "/actuator/loggers/".length());

Solution

  • This does not have much to do with Spring Boot Admin, but with Spring's WebClient (which is used by Spring Boot Admin). The challenge is to read the body from the ClientRequest and then put it back in, because otherwise it would be lost.

    Based on an example in the Spring WebClient documentation, here is a basic solution which works in Spring Boot Admin:

    @Bean
    public InstanceExchangeFilterFunction auditLog() {
        return (instance, request, next) -> {
            String path = request.url().getPath();
            if (HttpMethod.POST.equals(request.method()) 
                && path.contains("/actuator/loggers/")) {
                String appName = instance.getRegistration().getName();
                String loggerName = path.substring(path.indexOf("/actuator/loggers/") 
                    + "/actuator/loggers/".length());
    
                return next.exchange(ClientRequest.from(request)
                    .body((outputMessage, context) -> request.body()
                        .insert(new BodyLoggingDecorator(appName, loggerName, outputMessage), context))
                    .build());
            }
            else {
                return next.exchange(request);
            }
        };
    }
    
    
    private static final class BodyLoggingDecorator extends ClientHttpRequestDecorator {
    
        private final String appName;
        private final String loggerName;
    
        private BodyLoggingDecorator(String appName, String loggerName, ClientHttpRequest delegate) {
            super(delegate);
            this.appName = appName;
            this.loggerName = loggerName;
        }
    
        @Override
        public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
            return DataBufferUtils.join(body).flatMap(dataBuffer -> {
                // read original DataBuffer and release it
                byte[] bytes = new byte[dataBuffer.readableByteCount()];
                dataBuffer.read(bytes);
                DataBufferUtils.release(dataBuffer);
    
                String bodyString = new String(bytes, StandardCharsets.UTF_8);
                log.info("[{}] {}: {}", appName, loggerName, bodyString);
    
                // create new DataBuffer with the same content
                DataBuffer newDataBuffer = new DefaultDataBufferFactory().wrap(bytes);
    
                return super.writeWith(Mono.just(newDataBuffer));
            });
        }
    
    }
    

    This results in log messages like:

    [spring-boot-admin-sample-servlet] de.codecentric.boot.admin.sample: {"configuredLevel":"DEBUG"}
    

    Be aware: This reads the whole request body in memory which might be a problem for large payloads.