likes
comments
collection
share

玩个锤子,两小时撸完日志链路串连方案

作者站长头像
站长
· 阅读数 31

背景

最近接手了个项目,由于项目没人维护,又需要对功能进行大改,开发过程中对接口进行自测,在启动项目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
评论
请登录