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
@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());
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.