Backend Development 7 min read

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

This article explains how to generate a unique traceId at request entry and propagate it through Rest, MQ, and RPC modules using Log4j2, MDC, and custom converters, enabling clear, linked log output across the entire request 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 Recently I took over a project that had no maintenance and required major functional changes. During debugging, the console logs lacked context, making it impossible to view the full request chain.

The project only depends on Rest and MQ modules, so the solution focuses on those two (RPC would be added later).

Solution Use a unique identifier called traceId . Generate a traceId at the entry of each request or transaction, pass it downstream, and print it in every log line so that upstream and downstream logs can be correlated.

1. Rest Module

Retrieve the incoming traceId (from the upstream or front‑end) in the logging layer and print it. This can be done via AOP or a custom Log4j2 converter. The Log4j2 pattern used is:

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

Steps:

Define a 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 extends Log4j's AbstractMessageFactory and overrides newMessage to return a TraceMessage with the current traceId :

public class TraceMessageFactory extends AbstractMessageFactory {
    public TraceMessageFactory() {}
    @Override
    public Message newMessage(String message, Object... params) {
        // obtain traceId from upstream context
        String traceId = "...";
        return new TraceMessage(traceId, message, params);
    }
    @Override
    public Message newMessage(CharSequence message) { return newMessage(message); }
    @Override
    public Message newMessage(Object message) { return super.newMessage(message); }
    @Override
    public Message newMessage(String message) { return newMessage(message, null); }
}

3. Implement a Log4j2 pattern converter plugin 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

For RocketMQ, the consumer receives a msgId . Replace the traceId with this msgId so that logs can be linked to the MQ management console. An aspect stores the msgId in MDC before the consumer method runs.

@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 pjp) throws Throwable {
        try {
            if (pjp.getSignature().getName().equals("consumeMessage")) {
                List
list = (List
) pjp.getArgs()[0];
                String messageId = list.stream().map(MessageExt::getMsgId).collect(Collectors.joining("-"));
                MDC.put("msgId", messageId);
            }
            return pjp.proceed(pjp.getArgs());
        } finally {
            MDC.clear();
        }
    }
}

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

3. RPC Module

Although the current project has no RPC, an example for Dubbo shows how to put the traceId into the RPC context's attachment so that both provider and consumer can access it.

@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 = "..."; // obtain from upstream
            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 the above configurations, the distributed logs are linked by a common traceId , making the entire request chain 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.