专栏名称: 芋道源码
纯 Java 源码分享公众号,目前有「Dubbo」「SpringCloud」「Java 并发」「RocketMQ」「Sharding-JDBC」「MyCAT」「Elastic-Job」「SkyWalking」「Spring」等等
目录
相关文章推荐
芋道源码  ·  玩个锤子,两小时撸完日志链路串连方案 ·  昨天  
芋道源码  ·  李飞飞团队50美元训练出DeepSeek R1? ·  昨天  
芋道源码  ·  SpringBoot3.4.0 结构化日志详解 ·  2 天前  
芋道源码  ·  裁员天花板:全员降薪40%,“闲置员工” ... ·  2 天前  
芋道源码  ·  Nginx 部署负载均衡服务全解析 ·  3 天前  
51好读  ›  专栏  ›  芋道源码

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

芋道源码  · 公众号  · Java  · 2025-02-07 09:49

正文

👉 这是一个或许对你有用 的社群

🐱 一对一交流/面试小册/简历优化/求职解惑,欢迎加入 芋道快速开发平台 知识星球。 下面是星球提供的部分资料:

👉 这是一个或许对你有用的开源项目

国产 Star 破 10w+ 的开源项目,前端包括管理后台 + 微信小程序,后端支持单体和微服务架构。

功能涵盖 RBAC 权限、SaaS 多租户、数据权限、 商城 、支付、工作流、大屏报表、微信公众号、 ERP CRM AI 大模型 等等功能:

  • Boot 多模块架构:https://gitee.com/zhijiantianya/ruoyi-vue-pro
  • Cloud 微服务架构:https://gitee.com/zhijiantianya/yudao-cloud
  • 视频教程:https://doc.iocoder.cn
【国内首批】支持 JDK 17/21 + SpringBoot 3.3、JDK 8/11 + Spring Boot 2.7 双版本

来源:juejin.cn/post/
7389651543740465152


背景

最近接手了个项目,由于项目没人维护,又需要对功能进行大改,开发过程中对接口进行自测,在启动项目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打印出来,那么上下游的日志都能清晰可见了。好,开干。

一、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 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 + "]"






请到「今天看啥」查看全文