Logging Incoming Requests in Spring WebFlux
In the world of modern software development, meticulous monitoring and robust debugging are paramount. With the rise of reactive programming paradigms, Spring Webflux has emerged as a powerful framework for building reactive, scalable, and highly performant applications. However, as complexity grows, so does the need for effective logging mechanisms. Enter the realm of logging input requests in Spring Webflux — a practice that serves as a critical foundation for both diagnosing issues and ensuring application security.
Logging, often regarded as the unsung hero of software development, provides developers with invaluable insights into their applications’ inner workings. Through comprehensive logs, developers can peer into the execution flow, troubleshoot errors, and track the journey of each request as it traverses through the intricate layers of their Spring Webflux application. But logging is not a one-size-fits-all solution; it requires thoughtful configuration and strategic implementation to strike the balance between informative insights and performance overhead.
In this article, we embark on a journey through the landscape of Spring Webflux and delve into the art of logging input requests. We’ll explore the nuances of intercepting and capturing crucial details of incoming requests, all while maintaining security and privacy standards. By the end, you’ll be equipped with the knowledge to empower your Spring Webflux application with insightful logs, fostering enhanced debugging, streamlined monitoring, and a fortified security posture.
So, fasten your seatbelts as we unravel the techniques, best practices, and considerations for logging input requests in Spring Webflux, and learn how this practice can elevate your application development to new heights.
Action
Although WebFilters are frequently employed to log web requests, we will choose to utilize AspectJ for this scenario.
Assuming that all the endpoints in our project are located within a package named “controller” and that Controller classes end with the term “Controller,” we can craft an advice method as depicted below.
@Aspect
@Component
public class RequestLoggingAspect {
@Around("execution (* my.cool.project.controller.*..*.*Controller.*(..))")
public Object logInOut(ProceedingJoinPoint joinPoint) {
Class<?> clazz = joinPoint.getTarget().getClass();
Logger logger = LoggerFactory.getLogger(clazz);
Date start = new Date();
Object result = null;
Throwable exception = null;
try {
result = joinPoint.proceed();
if (result instanceof Mono<?> monoOut) {
return logMonoResult(joinPoint, clazz, logger, start, monoOut);
} else if (result instanceof Flux<?> fluxOut) {
return logFluxResult(joinPoint, clazz, logger, start, fluxOut);
} else {
return result;
}
} catch (Throwable e) {
exception = e;
throw e;
} finally {
if (!(result instanceof Mono<?>) && !(result instanceof Flux<?>)) {
doOutputLogging(joinPoint, clazz, logger, start, result, exception);
}
}
}
}
The RequestLoggingAspect
stands out for its adept handling of diverse return types, including Flux, Mono, and non-Webflux, within a Spring Webflux framework. Employing the AspectJ @Around
annotation, it seamlessly intercepts methods in "Controller" classes, offering tailored logging for each return type.
Below is the logMonoResult
method, which efficiently logs with contextView to retrieve contextual data from the Webflux environment. This method adeptly handles Mono
return types, capturing various scenarios while maintaining a structured logging approach. It gracefully integrates deferred contextual information and ensures seamless logging of different outcomes. From handling empty results to tracking successes and errors, the logMonoResult
method seamlessly facilitates detailed logging within the Spring Webflux context:
private <T, L> Mono<T> logMonoResult(ProceedingJoinPoint joinPoint, Class<L> clazz, Logger logger, Date start, Mono<T> monoOut) {
return Mono.deferContextual(contextView ->
monoOut
.switchIfEmpty(Mono.<T>empty()
.doOnSuccess(logOnEmptyConsumer(contextView, () -> doOutputLogging(joinPoint, clazz, logger, start, "[empty]", null))))
.doOnEach(logOnNext(v -> doOutputLogging(joinPoint, clazz, logger, start, v, null)))
.doOnEach(logOnError(e -> doOutputLogging(joinPoint, clazz, logger, start, null, e)))
.doOnCancel(logOnEmptyRunnable(contextView, () -> doOutputLogging(joinPoint, clazz, logger, start, "[cancelled]", null)))
);
}
Likewise, the logFluxResult
method is presented below. This method is orchestrating comprehensive logging while seamlessly incorporating the contextView to obtain contextual information from the Webflux environment. By accommodating diverse scenarios, such as empty results or cancellations, the logFluxResult
method optimizes logging within the Spring Webflux ecosystem:
private <T> Flux<T> logFluxResult(ProceedingJoinPoint joinPoint, Class<?> clazz, Logger logger, Date start, Flux<T> fluxOut) {
return Flux.deferContextual(contextView ->
fluxOut
.switchIfEmpty(Flux.<T>empty()
.doOnComplete(logOnEmptyRunnable(contextView, () -> doOutputLogging(joinPoint, clazz, logger, start, "[empty]", null))))
.doOnEach(logOnNext(v -> doOutputLogging(joinPoint, clazz, logger, start, v, null)))
.doOnEach(logOnError(e -> doOutputLogging(joinPoint, clazz, logger, start, null, e)))
.doOnCancel(logOnEmptyRunnable(contextView, () -> doOutputLogging(joinPoint, clazz, logger, start, "[cancelled]", null)))
);
}
Let’s delve into the details of the logOnNext
, logOnError
, logOnEmptyConsumer
, and logOnEmptyRunnable
methods, explaining how they contribute to comprehensive request logging. These methods encapsulate intricate logging procedures and utilize the contextView to maintain contextual information from the Webflux environment. The combination of MDC (Mapped Diagnostic Context) and signal processing ensures precise logging under various scenarios:
logOnNext
Method: The logOnNext
method is designed to log information when a signal indicates a successful next event. It uses the signal's contextView to extract contextual variables such as transaction ID (TRX_ID
) and path URI (PATH_URI
). Later we will describe how such values can be put to context. These variables are then included in the MDC to enable consistent tracking throughout the logging process. The logging statement is encapsulated within the MDC context, guaranteeing that the correct transaction and path details are associated with the log statement. This approach ensures that successful events are accurately logged within the relevant context.
private static <T> Consumer<Signal<T>> logOnNext(Consumer<T> logStatement) {
return signal -> {
if (!signal.isOnNext()) return;
String trxIdVar = signal.getContextView().getOrDefault(TRX_ID, "");
String pathUriVar = signal.getContextView().getOrDefault(PATH_URI, "");
try (MDC.MDCCloseable trx = MDC.putCloseable(TRX_ID, trxIdVar);
MDC.MDCCloseable path = MDC.putCloseable(PATH_URI, pathUriVar)) {
T t = signal.get();
logStatement.accept(t);
}
};
}
logOnError
Method: The logOnError
method mirrors the behavior of logOnNext
, but it focuses on error events. It extracts the contextual variables from the signal's contextView and places them in the MDC. This ensures that errors are logged in the proper context, making it easier to identify the specific transaction and path associated with the error event. By encapsulating the error log statement within the MDC, this method ensures that error logs are informative and appropriately contextualized.
public static <T> Consumer<Signal<T>> logOnError(Consumer<Throwable> errorLogStatement) {
return signal -> {
if (!signal.isOnError()) return;
String trxIdVar = signal.getContextView().getOrDefault(TRX_ID, "");
String pathUriVar = signal.getContextView().getOrDefault(PATH_URI, "");
try (MDC.MDCCloseable trx = MDC.putCloseable(TRX_ID, trxIdVar);
MDC.MDCCloseable path = MDC.putCloseable(PATH_URI, pathUriVar)) {
errorLogStatement.accept(signal.getThrowable());
}
};
}
logOnEmptyConsumer
and logOnEmptyRunnable
Methods: Both of these methods deal with scenarios where the signal is empty, indicating that there's no result to process. The logOnEmptyConsumer
method is designed to accept a Consumer and executes it when the signal is empty. It retrieves the contextual variables from the provided contextView and incorporates them into the MDC before executing the log statement.
private static <T> Consumer<T> logOnEmptyConsumer(final ContextView contextView, Runnable logStatement) {
return signal -> {
if (signal != null) return;
String trxIdVar = contextView.getOrDefault(TRX_ID, "");
String pathUriVar = contextView.getOrDefault(PATH_URI, "");
try (MDC.MDCCloseable trx = MDC.putCloseable(TRX_ID, trxIdVar);
MDC.MDCCloseable path = MDC.putCloseable(PATH_URI, pathUriVar)) {
logStatement.run();
}
};
}
private static Runnable logOnEmptyRunnable(final ContextView contextView, Runnable logStatement) {
return () -> {
String trxIdVar = contextView.getOrDefault(TRX_ID, "");
String pathUriVar = contextView.getOrDefault(PATH_URI, "");
try (MDC.MDCCloseable trx = MDC.putCloseable(TRX_ID, trxIdVar);
MDC.MDCCloseable path = MDC.putCloseable(PATH_URI, pathUriVar)) {
logStatement.run();
}
};
}
In both cases, these methods ensure that the correct context, including transaction and path details, is established through MDC before executing the log statements. This allows for consistent and meaningful logging even in situations where there is no explicit result to process.
To introduce the transaction ID and path variables into the Webflux context, consider the following WebFilter
configuration. As a @Bean
with highest priority, the slf4jMdcFilter
extracts the request's unique ID and path URI, incorporating them into the context. This ensures that subsequent processing stages, including the RequestLoggingAspect
, can seamlessly access this enriched context for precise and comprehensive request handling.
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
WebFilter slf4jMdcFilter() {
return (exchange, chain) -> {
String requestId = exchange.getRequest().getId();
return chain.filter(exchange)
.contextWrite(Context.of(Constants.TRX_ID, requestId)
.put(Constants.PATH_URI, exchange.getRequest().getPath()));
};
}
Ultimately, for comprehensive logging of diverse request types, the inclusion of a method named doOutputLogging
becomes essential. While a detailed implementation of this method is beyond our scope, it serves as a conduit for logging incoming expressions, either via a tailored logger to match your scenario or potentially routed to a database or alternate platform. This method can be customized to align precisely with your distinct necessities and specifications.
private <T> void doOutputLogging(final ProceedingJoinPoint joinPoint, final Class<?> clazz, final Logger logger,
final Date start, final T result, final Throwable exception) {
//log(...);
//db.insert(...);
}
In summary, effective request logging in Spring Webflux is pivotal for debugging and enhancing application performance. By leveraging AspectJ and WebFilters, developers can simplify the process of logging input and output across diverse endpoints. The showcased RequestLoggingAspect
efficiently handles different return types, while the slf4jMdcFilter
WebFilter enriches logs with transaction and path data.
Although the logMonoResult
, logFluxResult
, and doOutputLogging
methods serve as adaptable templates, they offer customization options to suit specific needs. This empowers developers to tailor logging to their preferences, whether for internal logs or external data storage.