
日志链路串连方案
背景
最近接手了个新项目,在搭项目的整体框架时,想到以后的开发过程中会对接口进行自测,在测试接口时看控制台日志,会发现日志的打印没有上下文关系,没办法清晰的看整个请求链路的日志,响应时间等。
看了下项目可能要用到的依赖,只有rest和mq模块,还好工作量不算多。
解决方案
先说解决方案,不同模块之间的日志想要串联,需要有一个唯一标识:暂时把这个链路标识定为traceId,所以如果一个请求或者一个事物入口就生成一个唯一的traceId沿着链路一直传递给下游,打印日志的时候把这个traceId打印出来,那么上下游的日志都能清晰可见了。
1.Rest模块
需要在log模块输出日志时获取到上游或者前端传过来的traceId信息并打印即可。这里可以选择通过AOP的方式或者通过一些日志实现自带的Convert来实现,我这里用log4j2的LogEventPatternConvert来做,比较简单:
日志配置输出格式(已经配置打印traceId参数):
[%d{yyyy-MM-dd HH:mm:ss.SSS}][%t][%level][%C:%L][%traceId] %m%n
- 定义一个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("~");
}
}
2.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("~");
}
}
结尾
经过我的一波coding后,链路日志终于被串联起来了,以后的开发调式就更方便l!
- 感谢你赐予我前进的力量