Backend Development 7 min read

Implementing TraceId-Based Distributed Logging for Rest, MQ, and RPC Modules with Log4j2

This article explains how to introduce a unique traceId for request tracing across Rest, MQ, and RPC modules using Log4j2, custom message factories, converters, and AOP, enabling clear, linked logs throughout the service chain.

Selected Java Interview Questions
Selected Java Interview Questions
Selected Java Interview Questions
Implementing TraceId-Based Distributed Logging for Rest, MQ, and RPC Modules with Log4j2

Background The author inherited a project lacking maintenance and needed to perform major functional changes. During debugging, the console logs were unstructured, making it impossible to follow the request chain.

Solution Introduce a unique identifier called traceId that is generated at the entry point of each request or transaction and propagated downstream. By printing this traceId in every log entry, logs from upstream and downstream become correlated.

1. Rest Module

For the Rest module, the traceId can be added to the log output using Log4j2’s LogEventPatternConverter . The log pattern is configured as:

[%d{yyyy-MM-dd HH:mm:ss.SSS}][%t][%level][%C:%L][%traceId] %m%n

Define a custom TraceMessage class extending ParameterizedMessage :

@Getter
@Setter
public class TraceMessage extends ParameterizedMessage {
    private String traceId;
    public TraceMessage(String traceId, String spanId, String messagePattern, Object... arguments) {
        super(messagePattern, arguments);
        this.traceId = traceId;
    }
}

Create a TraceMessageFactory that overrides newMessage to produce TraceMessage instances:

public class TraceMessageFactory extends AbstractMessageFactory {
    @Override
    public Message newMessage(String message, Object... params) {
        // obtain traceId from upstream
        String traceId = "...";
        return new TraceMessage(traceId, message, params);
    }
    // other overloads omitted for brevity
}

Implement a Log4j2 converter to output the traceId :

@Plugin(name = "TraceIdPatternConverter", category = PatternConverter.CATEGORY)
@ConverterKeys({"traceId"})
public class TraceIdPatternConverter extends LogEventPatternConverter {
    private TraceIdPatternConverter(String name, String style) {
        super(name, style);
    }
    public static TraceIdPatternConverter newInstance() {
        return new TraceIdPatternConverter("TraceIdPatternConverter", "TraceIdPatternConverter");
    }
    @Override
    public void format(LogEvent event, StringBuilder toAppendTo) {
        Message message = event.getMessage();
        if (message instanceof TraceMessage) {
            TraceMessage tm = (TraceMessage) message;
            toAppendTo.append("[" + ObjectUtil.defaultIfBlank(tm.getTraceId(), "") + "]");
            return;
        }
        toAppendTo.append("~");
    }
}

2. MQ Module

Using RocketMQ as an example, the consumer side receives a msgId from the message. An AOP aspect stores this msgId in MDC so that Log4j2 can treat it as the traceId :

@Slf4j
@Aspect
@Component
public class LogRocketMQAspect {
    @Pointcut("execution(* org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently.consumeMessage(..))")
    public void pointCut() {}
    @Around("pointCut()")
    public Object injectTraceId(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        try {
            if (proceedingJoinPoint.getSignature().getName().equals("consumeMessage")) {
                List
list = (List
) proceedingJoinPoint.getArgs()[0];
                String messageId = list.stream().map(MessageExt::getMsgId).collect(Collectors.joining("-"));
                MDC.put("msgId", messageId);
            }
            return proceedingJoinPoint.proceed(proceedingJoinPoint.getArgs());
        } finally {
            MDC.clear();
        }
    }
}

The same TraceIdPatternConverter is reused, but it now prefers the MDC msgId when present:

@Plugin(name = "TraceIdPatternConverter", category = PatternConverter.CATEGORY)
@ConverterKeys({"traceId"})
public class TraceIdPatternConverter extends LogEventPatternConverter {
    // constructor omitted
    @Override
    public void format(LogEvent event, StringBuilder toAppendTo) {
        Message message = event.getMessage();
        if (message instanceof TraceMessage) {
            TraceMessage tm = (TraceMessage) message;
            String id = StringUtils.isBlank(msgId) ? tm.getTraceId() : msgId;
            toAppendTo.append("[" + id + "]");
            return;
        }
        toAppendTo.append("~");
    }
}

3. RPC Module

Although the current project does not contain RPC, an example for Dubbo shows how to propagate traceId via RpcContext attachments:

@Activate(order = 99, group = {Constants.PROVIDER_PROTOCOL, Constants.CONSUMER_PROTOCOL})
public class LogAttachmentFilter implements Filter {
    @Override
    public Result invoke(Invoker
invoker, Invocation invocation) throws RpcException {
        RpcContext context = RpcContext.getContext();
        if (context.isConsumerSide()) {
            String traceId = "...";
            if (StringUtils.isBlank(traceId)) {
                traceId = UuidUtils.getUuid();
            }
            context.setAttachment("traceId", traceId);
        } else if (context.isProviderSide()) {
            LogContext.setTraceId(context.getAttachment("traceId"));
        }
        return invoker.invoke(invocation);
    }
}

Conclusion After applying these changes, the distributed logs are linked by the same traceId , making the request flow easy to follow.

backendJavaloggingDistributed TracingMQlog4j2TraceId
Selected Java Interview Questions
Written by

Selected Java Interview Questions

A professional Java tech channel sharing common knowledge to help developers fill gaps. Follow us!

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.