玩个锤子,两小时撸完日志链路串连方案
背景
最近接手了个项目,由于项目没人维护,又需要对功能进行大改,开发过程中对接口进行自测,在启动项目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);
}
}
结尾:
一波整顿后,链路日志就被刷刷的串联起来了。终于舒服了~~~~
转载自:https://juejin.cn/post/7389651543740465152