⭐⭐⭐ Spring Boot 项目实战 ⭐⭐⭐ Spring Cloud 项目实战
《Dubbo 实现原理与源码解析 —— 精品合集》 《Netty 实现原理与源码解析 —— 精品合集》
《Spring 实现原理与源码解析 —— 精品合集》 《MyBatis 实现原理与源码解析 —— 精品合集》
《Spring MVC 实现原理与源码解析 —— 精品合集》 《数据库实体设计合集》
《Spring Boot 实现原理与源码解析 —— 精品合集》 《Java 面试题 + Java 学习指南》

摘要: 原创出处 juejin.cn/post/7389651543740465152 「盛夏果实」欢迎转载,保留摘要,谢谢!


🙂🙂🙂关注微信公众号:【芋道源码】有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
  3. 您对于源码的疑问每条留言都将得到认真回复。甚至不知道如何读源码也可以请教噢。
  4. 新的源码解析文章实时收到通知。每周更新一篇左右。
  5. 认真的源码交流微信群。

背景

最近接手了个项目,由于项目没人维护,又需要对功能进行大改,开发过程中对接口进行自测,在启动项目Dedug时,我一看控制台日志,蒙了,日志的打印没有上下文关系,完全没法清晰地看整个请求链路的日志。

看了下项目依赖,好在只有rest和mq相关的模块,如果多了rpc,还得把rpc的也串起来。虽然不是俺们的项目,基建不搞后期维护起来也挺难受的,脑袋一拍,也就两小时的活。

方案

不同模块之间的日志想要串联,需要有一个唯一标识:暂时把这个链路标识定为traceId,所以如果一个请求或者一个事物入口就生成一个唯一的traceId沿着链路一直传递给下游,打印日志的时候把这个traceId打印出来,那么上下游的日志都能清晰可见了。好,开干。

一、Rest模块

这个比较好做,只需要在log模块输出日志时获取到上游或者前端传过来的traceId信息并打印即可。这里可以选择通过AOP的方式或者通过一些日志实现自带的Convert来实现,我这里用log4j2的LogEventPatternConvert来做,比较简单:

日志配置输出格式(已经配置打印traceId参数):

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

1、先定义一个TraceMessage

@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;
}
}

2、TraceMessageFactory继承Log4j的MessageFactory工厂类,重写newMessage方法

public class TraceMessageFactory extends AbstractMessageFactory {

public TraceMessageFactory() {
}

@Override
public Message newMessage(String message, Object... params) {
//..这里通过你的方式获取从上游传过来的那个traceId参数, 生成一个自定义的TraceMessage
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、再实现一个Log4j的Convert插件就可以了

@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 traceMessage = (TraceMessage) message;
toAppendTo.append("[" + ObjectUtil.defaultIfBlank(traceMessage.getTraceId(), "") + "]")
return;
}
toAppendTo.append("~");
}
}

二、MQ模块

mq处理起来也比较简单,以rocketMq为例,作为mq的消费端,因为mq消息过来时有自带的msgId,日志打印的时候也把msgId打印出来方便与mq管理后台关联,因为mq消息透传traceId比较麻烦,因此这里直接把traceId替换成mq的msgId即可。这里加了个切面,为了在mq消息消费之前打印msgId

这里使用MDC存储traceId,以便传递给log4j,当然也可以用LogContext对象传递值

@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<MessageExt> messageExtList = (List<MessageExt>) proceedingJoinPoint.getArgs()[0];
String messageId = messageExtList.stream().map(MessageExt::getMsgId).collect(Collectors.joining("-"));
MDC.put("msgId", messageId);
}
return proceedingJoinPoint.proceed(proceedingJoinPoint.getArgs());
} finally {
MDC.clear();
}
}
}

这里先获取msgId,然后作为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 traceMessage = (TraceMessage) message;
toAppendTo.append(StringUtils.isBlank(msgId) ? "[" + ObjectUtil.defaultIfBlank(traceMessage.getTraceId(), "") + "]" : "[" + msgId + "]")
return;
}
toAppendTo.append("~");
}
}

三、RPC模块

虽然该项目中没有RPC模块,这里提供dubbo接入的参考,主要是通过把traceId放入dubbo的RCPContext的attachment里

@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()) {
//这里是从上游获取的已经设置好的traceId,通过你的方式拿到
String traceId = "...";
if (StringUtils.isBlank(traceId)) {
traceId = UuidUtils.getUuid();
}
context.setAttachment("traceId", traceId);
} else if (context.isProviderSide()) {
//此处通过LogContext或者MDC都可设置traceId
LogContext.setTraceId(context.getAttachment("traceId"));
}
return invoker.invoke(invocation);
}
}

结尾

一波整顿后,链路日志就被刷刷的串联起来了。终于舒服了~~~~

文章目录
  1. 1. 背景
  2. 2. 方案
    1. 2.1. 一、Rest模块
    2. 2.2. 二、MQ模块
    3. 2.3. 三、RPC模块
  3. 3. 结尾