最近接手了个项目,由于项目没人维护,又需要对功能进行大改,开发过程中对接口进行自测,在启动项目Dedug时,我一看控制台日志,蒙了,日志的打印没有上下文关系,完全没法清晰地看整个请求链路的日志。
看了下项目依赖,好在只有rest和mq相关的模块,如果多了rpc,还得把rpc的也串起来。虽然不是俺们的项目,基建不搞后期维护起来也挺难受的,脑袋一拍,也就两小时的活。
基于 Spring Boot + MyBatis Plus + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能
-
项目地址:https://github.com/YunaiV/ruoyi-vue-pro
-
视频教程:https://doc.iocoder.cn/video/
不同模块之间的日志想要串联,需要有一个唯一标识:暂时把这个链路标识定为traceId,所以如果一个请求或者一个事物入口就生成一个唯一的traceId沿着链路一直传递给下游,打印日志的时候把这个traceId打印出来,那么上下游的日志都能清晰可见了。好,开干。
这个比较好做,只需要在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处理起来也比较简单,以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 messageExtList = (List) 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 + "]"