In modern applications, encountering errors is inevitable, and developers often rely on server logs to investigate and resolve issues. Unfortunately, crucial details are sometimes missing, making debugging more challenging. To address this, comprehensive logging is essential, especially for tracking user actions and system changes.

This article explores how you can enhance trace logging in a Spring Boot application, particularly when using the Axon Framework in an event-driven architecture. The Axon Framework allows you to integrate and manage event logs efficiently, providing deeper insights into application behavior.

 

Setting Up a Default Logging Configuration

The first step is to configure the Mapped Diagnostic Context (MDC), which adds contextual information, such as a unique request ID and the username, to your logs. This ensures that logs are traceable and relevant.

Here’s how you can implement MDC in a Spring Boot filter:

@Slf4j
@Component
public class FilterConfig implements Filter {
    public static final String REQUEST_ID = "X-Request-Id";
    public static final String USER_ID = "X-User-Id";

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        // Define a unique request id to identify each request
        MDC.put(REQUEST_ID, UUID.randomUUID().toString());
        // Retrieve the current username
        MDC.put(USER_ID, getCurrentUsername());

        // pass the request
        chain.doFilter(request, response);
    }
}

 

Please note that the method getCurrentUsername depends on your implementation and should be created by yourself, where you return the current user id or name to identify the user in your system, normally by using the Spring Boot Security Context.

 

Customize Logging Patterns

To include MDC attributes in your logs, update the application.yml file as follows:

logging:
    pattern:
        console: "%clr(TS=){faint}%clr(%d{yyyy-MM-dd\'T\'HH:mm:ss.SSS\'Z\', UTC}){faint} [%t] %clr(APP=){faint}%clr(${spring.application.name:null}){yellow} %clr(LEVEL=){faint}%clr(${LOG_LEVEL_PATTERN:%p}) %clr(CLASS=){faint}%clr(%logger{26}){cyan} %clr(USERID=){faint}%clr(%X{X-User-Id:-null}) %clr(REQUESTID=){faint}%clr(%X{X-Request-Id:-null}){reverse} | %m%n%ex"

 

Intercepting and Logging HTTP Requests

To log HTTP traffic, we’ll use interceptors and advice adapters to capture incoming requests and outgoing responses.

 

LogInterceptor for GET Requests

Here’s an interceptor that logs all GET requests to endpoints prefixed with /api:

@Slf4j
@Component
public class LogInterceptor implements HandlerInterceptor {
    LoggingService loggingService;

    public LogInterceptor(LoggingService loggingService) {
        this.loggingService = loggingService;
    }

    @Override
    public boolean preHandle(HttpServletRequest request,
                             HttpServletResponse response,
                             Object handler) {
        if (DispatcherType.REQUEST.name().equals(request.getDispatcherType().name())
            && request.getMethod().equals(HttpMethod.GET.name())
            // Only log requests to our API
            && request.getRequestURI().contains("/api/")
        ) {
            loggingService.logRequest(request, null);
        }

        return true;
    }
}

 

Log POST Requests and Responses

Use advice adapters to capture POST requests and their responses:

  • Request Body Logging:
@ControllerAdvice
@AllArgsConstructor
public class CustomResponseBodyAdviceAdapter implements ResponseBodyAdvice {

    private LoggingService loggingService;

    @Override
    public boolean supports(MethodParameter methodParameter,
                            Class<? extends HttpMessageConverter> aClass) {
        return true;
    }

    @Override
    public Object beforeBodyWrite(Object o,
                                  @NotNull MethodParameter methodParameter,
                                  @NotNull MediaType mediaType,
                                  @NotNull Class<? extends HttpMessageConverter> aClass,
                                  @NotNull ServerHttpRequest serverHttpRequest,
                                  @NotNull ServerHttpResponse serverHttpResponse) {
        if (serverHttpRequest instanceof ServletServerHttpRequest ​

 

  • Response Body Logging:
@ControllerAdvice
@AllArgsConstructor
public class CustomResponseBodyAdviceAdapter implements ResponseBodyAdvice {

    private LoggingService loggingService;

    @Override
    public boolean supports(MethodParameter methodParameter,
                            Class<? extends HttpMessageConverter> aClass) {
        return true;
    }

    @Override
    public Object beforeBodyWrite(Object o,
                                  @NotNull MethodParameter methodParameter,
                                  @NotNull MediaType mediaType,
                                  @NotNull Class<? extends HttpMessageConverter> aClass,
                                  @NotNull ServerHttpRequest serverHttpRequest,
                                  @NotNull ServerHttpResponse serverHttpResponse) {
        if (serverHttpRequest instanceof ServletServerHttpRequest request &&
            serverHttpResponse instanceof ServletServerHttpResponse response &&
            request.getServletRequest().getRequestURI().contains("/api/")) {
                loggingService.logResponse(
                    request.getServletRequest(),
                    response.getServletResponse(),
                    o
                );
            }

        return o;
    }
}

 

 

The LoggingService consolidates request and response logging logic, ensuring consistency across the application:

 

@Slf4j
@Service
public class LoggingService {
    public void logRequest(HttpServletRequest httpServletRequest, Object body) {
        StringBuilder stringBuilder = new StringBuilder();
        Map parameters = buildParametersMap(httpServletRequest);

        stringBuilder.append("REQUEST ");
        stringBuilder.append("method=[").append(httpServletRequest.getMethod()).append("] ");
        stringBuilder.append("path=[").append(httpServletRequest.getRequestURI()).append("] ");

        if (!parameters.isEmpty()) {
            stringBuilder.append("parameters=[").append(parameters).append("] ");
        }

        if (body != null) {
            stringBuilder.append("body=[").append(body).append("]");
        }

        log.info(stringBuilder.toString());
    }

    public void logResponse(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object body) {
        String stringBuilder = "RESPONSE " +
            "method=[" + httpServletRequest.getMethod() + "] " +
            "path=[" + httpServletRequest.getRequestURI() + "] " +
            "responseBody=[" + body + "] ";

        log.info(stringBuilder);
    }

    private Map buildParametersMap(HttpServletRequest httpServletRequest) {
        Map resultMap = new HashMap();
        Enumeration parameterNames = httpServletRequest.getParameterNames();

        while (parameterNames.hasMoreElements()) {
            String key = parameterNames.nextElement();
            String value = httpServletRequest.getParameter(key);
            resultMap.put(key, value);
        }

        return resultMap;
    }
}

 

Register the Interceptor

Finally, integrate the LogInterceptor into your application using the WebMvcConfigurer class:

@Configuration
public class WebConfig implements WebMvcConfigurer {
    LoggingService loggingService;

    WebConfig(LoggingService loggingService) {
        this.loggingService = loggingService;
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LogInterceptor(loggingService));
    }
}

 

Intercepting and Logging Axon Commands and Events

To ensure complete visibility into the flow of commands and events within an Axon-based application, we can extend logging to capture detailed information about dispatched messages. This section describes how to implement interceptors for commands and events, leveraging Axon's MessageDispatchInterceptor and MessageHandlerInterceptor.

Additionally, because the Axon framework doesn’t guarantee that commands and events execute within the same processing group, it’s possible for the Mapped Diagnostic Context (MDC) to lose traceability. To address this, we store contextual information, such as the request ID and username, in Axon’s MetaData.

 

Command Dispatch Interceptor

The CommandDispatchInterceptor enriches each dispatched command with metadata, including the current username and request ID, while logging the command details.

@Slf4j
public class CommandDispatchInterceptor implements MessageDispatchInterceptor<CommandMessage> {

    @NotNull
    @Override
    public BiFunction<Integer, CommandMessage, CommandMessage> handle(@NotNull List<? extends CommandMessage> messages) {
        return (index, command) -> {
            // Enrich the command with additional metadata
            var newCommand = command.andMetaData(
                command.getMetaData()
                    .and("user", getCurrentUsername())
                    .and("requestId", MDC.get("X-Request-Id"))
            );

            log.info("Dispatching a command {}.", newCommand);

            return newCommand;
        };
    }
}

 

Event Dispatch Interceptor

The EventDispatchInterceptor logs all events that are about to be published. It provides critical traceability for event publishing.

@Slf4j
public class EventDispatchInterceptor
                implements MessageDispatchInterceptor<EventMessage> {

    @Override
    public BiFunction<Integer, EventMessage, EventMessage> handle(
                List<? extends EventMessage> messages) {
        return (index, event) -> {

            log.info("Publishing event: [{}].", event);

            return event;
        };
    }
}

 

Restoring MDC Context with Event Handler Interceptor

When processing events, we can restore the MDC context using metadata previously added during command or event dispatch. This ensures consistent traceability throughout the event-processing lifecycle.

@Slf4j
public class EventHandlerInterceptor
    implements MessageHandlerInterceptor<EventMessage> {
    
    @Override
    public Object handle(UnitOfWork<? extends EventMessage> unitOfWork,
                        @NotNull InterceptorChain interceptorChain) throws Exception {
        EventMessage event = unitOfWork.getMessage();
        var metaData = event.getMetaData();

        // Restore MDC attributes from metadata
        var requestId = metaData.get("requestId");
        if (requestId != null) {
            MDC.put("requestId", requestId.toString());
        }

        var user = metaData.get("user");
        if (user != null) {
            MDC.put("user", user.toString());
        }

        return interceptorChain.proceed();
    }
}

 

Registering Dispatch Interceptors

To activate the dispatch interceptors, define a configuration class. This registers the CommandDispatchInterceptor and EventDispatchInterceptor with the Axon framework.

@Configuration
public class AxonGatewayInterceptor {
    private final CommandBus commandBus;

    public AxonGatewayInterceptor(CommandBus commandBus, EventBus eventBus) {
        this.commandBus = commandBus;

        // Register the Event Dispatch Interceptor
        eventBus.registerDispatchInterceptor(new EventDispatchInterceptor());
    }

    @PostConstruct
    public void registerInterceptor() {
        // Register the Command Dispatch Interceptor
        commandBus.registerDispatchInterceptor(new CommandDispatchInterceptor());
    }
}

 

Configuring Event Processing

To apply the EventHandlerInterceptor to a specific processing group, update the Axon configuration. This ensures the MDC context is restored during event handling.

@Configuration
public class AxonGatewayInterceptor {
    private final CommandBus commandBus;

    public AxonGatewayInterceptor(CommandBus commandBus, EventBus eventBus) {
        this.commandBus = commandBus;

        // Register the Event Dispatch Interceptor
        eventBus.registerDispatchInterceptor(new EventDispatchInterceptor());
    }

    @PostConstruct
    public void registerInterceptor() {
        // Register the Command Dispatch Interceptor
        commandBus.registerDispatchInterceptor(new CommandDispatchInterceptor());
    }
}

 

By using Axon framework and the provided command and event dispatch interceptors, the application keeps contextual information across processing boundaries and can capture logs at critical points in the Axon command and event lifecycle. This enhanced logging setup empowers developers to diagnose and monitor applications with improved granularity, fostering a more reliable and maintainable system.