Backend Development 13 min read

Implementing Request Logging with Spring AOP and TraceId

This article demonstrates how to create a Spring AOP request‑logging aspect that captures IP, URL, HTTP method, class method, parameters, results and execution time, and extends it with high‑concurrency handling, error logging, and TraceId propagation for better traceability.

Top Architect
Top Architect
Top Architect
Implementing Request Logging with Spring AOP and TraceId

Introduction

The author, a senior architect, shares a practical guide for adding a request‑logging aspect to a Spring project, focusing on logging request details, handling high concurrency, and integrating TraceId for end‑to‑end tracing.

Aspect Overview

Aspect‑Oriented Programming (AOP) separates cross‑cutting concerns (e.g., logging, transaction management) from core business logic. Using AOP, you can log requests with minimal intrusion.

Centralized handling of a specific concern

Easy addition/removal of concerns

Low invasiveness improves code readability and maintainability

Using Annotations

Declare the aspect class with @Aspect and define a pointcut using @Pointcut . The following annotations are used:

@Before – execute before the pointcut

@After – execute after the pointcut

@AfterReturning – execute after successful return

@AfterThrowing – execute when an exception is thrown

@Around – wrap the pointcut execution

Basic Request Logging Aspect

@Pointcut("execution(* your_package.controller..*(..))")
public void requestServer() {}

@Before("requestServer()")
public void doBefore(JoinPoint joinPoint) {
    ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
    HttpServletRequest request = attributes.getRequest();
    LOGGER.info("===============================Start========================");
    LOGGER.info("IP : {}", request.getRemoteAddr());
    LOGGER.info("URL : {}", request.getRequestURL().toString());
    LOGGER.info("HTTP Method : {}", request.getMethod());
    LOGGER.info("Class Method : {}.{}", joinPoint.getSignature().getDeclaringTypeName(), joinPoint.getSignature().getName());
}

@Around("requestServer()")
public Object doAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
    long start = System.currentTimeMillis();
    Object result = proceedingJoinPoint.proceed();
    LOGGER.info("Request Params : {}", getRequestParams(proceedingJoinPoint));
    LOGGER.info("Result : {}", result);
    LOGGER.info("Time Cost : {} ms", System.currentTimeMillis() - start);
    return result;
}

@After("requestServer()")
public void doAfter(JoinPoint joinPoint) {
    LOGGER.info("===============================End========================");
}

private Map
getRequestParams(ProceedingJoinPoint proceedingJoinPoint) {
    String[] paramNames = ((MethodSignature) proceedingJoinPoint.getSignature()).getParameterNames();
    Object[] paramValues = proceedingJoinPoint.getArgs();
    Map
requestParams = new HashMap<>();
    for (int i = 0; i < paramNames.length; i++) {
        Object value = paramValues[i];
        if (value instanceof MultipartFile) {
            MultipartFile file = (MultipartFile) value;
            value = file.getOriginalFilename();
        }
        requestParams.put(paramNames[i], value);
    }
    return requestParams;
}

High‑Concurrency Improvement

To avoid log interleaving under heavy load, the author aggregates all request information into a single RequestInfo object and logs it as a JSON string.

@Data
public class RequestInfo {
    private String ip;
    private String url;
    private String httpMethod;
    private String classMethod;
    private Object requestParams;
    private Object result;
    private Long timeCost;
}

@Around("requestServer()")
public Object doAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
    long start = System.currentTimeMillis();
    ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
    HttpServletRequest request = attributes.getRequest();
    Object result = proceedingJoinPoint.proceed();
    RequestInfo requestInfo = new RequestInfo();
    requestInfo.setIp(request.getRemoteAddr());
    requestInfo.setUrl(request.getRequestURL().toString());
    requestInfo.setHttpMethod(request.getMethod());
    requestInfo.setClassMethod(String.format("%s.%s", proceedingJoinPoint.getSignature().getDeclaringTypeName(), proceedingJoinPoint.getSignature().getName()));
    requestInfo.setRequestParams(getRequestParamsByProceedingJoinPoint(proceedingJoinPoint));
    requestInfo.setResult(result);
    requestInfo.setTimeCost(System.currentTimeMillis() - start);
    LOGGER.info("Request Info : {}", JSON.toJSONString(requestInfo));
    return result;
}

Error Logging

An @AfterThrowing advice captures exception details in a RequestErrorInfo object.

@Data
public class RequestErrorInfo {
    private String ip;
    private String url;
    private String httpMethod;
    private String classMethod;
    private Object requestParams;
    private RuntimeException exception;
}

@AfterThrowing(pointcut = "requestServer()", throwing = "e")
public void doAfterThrow(JoinPoint joinPoint, RuntimeException e) {
    ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
    HttpServletRequest request = attributes.getRequest();
    RequestErrorInfo requestErrorInfo = new RequestErrorInfo();
    requestErrorInfo.setIp(request.getRemoteAddr());
    requestErrorInfo.setUrl(request.getRequestURL().toString());
    requestErrorInfo.setHttpMethod(request.getMethod());
    requestErrorInfo.setClassMethod(String.format("%s.%s", joinPoint.getSignature().getDeclaringTypeName(), joinPoint.getSignature().getName()));
    requestErrorInfo.setRequestParams(getRequestParamsByJoinPoint(joinPoint));
    requestErrorInfo.setException(e);
    LOGGER.info("Error Request Info : {}", JSON.toJSONString(requestErrorInfo));
}

TraceId Propagation

To trace a request across services, a LogInterceptor generates a UUID‑based TraceId, stores it in Log4j2’s ThreadContext , and removes it after completion.

public class LogInterceptor implements HandlerInterceptor {
    private static final String TRACE_ID = "traceId";
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        String traceId = java.util.UUID.randomUUID().toString().replaceAll("-", "").toUpperCase();
        ThreadContext.put(TRACE_ID, traceId);
        return true;
    }
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
        ThreadContext.remove(TRACE_ID);
    }
}

Update the Log4j2 pattern to include the TraceId placeholder:

<property name="pattern">[TRACEID:%X{traceId}] %d{HH:mm:ss.SSS} %-5level %class{-1}.%M()/%L - %msg%xEx%n</property>

Conclusion

By integrating the AOP logging aspect, high‑concurrency aggregation, error handling, and TraceId propagation, developers obtain comprehensive, single‑line logs that simplify debugging and performance analysis in backend services.

backendjavaAOPSpringlog4j2TraceIdRequest Logging
Top Architect
Written by

Top Architect

Top Architect focuses on sharing practical architecture knowledge, covering enterprise, system, website, large‑scale distributed, and high‑availability architectures, plus architecture adjustments using internet technologies. We welcome idea‑driven, sharing‑oriented architects to exchange and learn together.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.