MDC 日志跟踪笔记

一、前言

本文主要解析应用中比较实在的一个log4j的链路应用,在经过一段时间的应用后发现还是很稳的,就记录一下这个MDC 代表Mapped Diagnostic Context(映射式诊断上下文)的情况。

Tisp: 它是一个线程安全的存放诊断日志的容器

Tisp: 它是一个线程安全的存放诊断日志的容器

Tisp: 它是一个线程安全的存放诊断日志的容器

二、接口相关内容

相关slf4j 官方的接口文档: https://www.slf4j.org/api/org/slf4j/MDC.html

1、常用直接调用接口

  • clear() :移除所有MDC

  • get (String key) :获取当前线程MDC中指定key的值

  • getContext() :获取当前线程MDC的MDC

  • put(String key, Object o) :往当前线程的MDC中存入指定的键值对

  • remove(String key) :删除当前线程MDC中指定的键值对

2、调试用接口

  • pushByKey(String key, String value) :将指定的键值对推入 MDC 上下文信息的栈

​ 可以在之后 在之后通过 popByKey 恢复原始值

  • popByKey(String key) :从 MDC 上下文信息的栈中弹出指定键的值

​ 如果在没有先前推送的情况下调用 popByKey,则会将键从 MDC 中移除。

3、配置用接口

  • getCopyOfContextMap() : 一个包含当前线程的 MDC 上下文信息的 不可修改的 映射副本

  • getMDCAdapter() :获取当前线程的 MDC 适配器,可进行 MDC 上下文信息的设置和清除

  • setContextMap(Map<String,String> contextMap) : 设置整个 MDC 上下文信息的映射

三、应用配置

下属内容中

TraceUtils 仅为生成traceId

TraceConstant 对应枚举类

logging:
  pattern:
    console: "%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %X{traceId} - %msg%n"

logging.pattern.console=%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %X{traceId} - %msg%n
<?xml version="1.0" encoding="UTF-8"?>
<configuration>

    <property name="logDirectory" value="${LOG_DIRECTORY:-logs}"/>

    <appender name="Console" class="ch.qos.logback.core.ConsoleAppender">
        <layout>
            <Pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %X{traceId} - %msg%n</Pattern>
        </layout>
    </appender>

    <appender name="File" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>${logDirectory}/app.log</file>
        <!-- 其他 RollingFileAppender 配置省略 -->
    </appender>

    <root level="info">
        <appender-ref ref="Console"/>
        <appender-ref ref="File"/>
    </root>

</configuration>

1、异步任务线程配置

针对独立线程修饰

TaskDecorator 接口允许在任务执行之前和之后对执行线程进行修改或装饰

public class MdcTaskDecorator implements TaskDecorator {
   

    @Override
    public Runnable decorate(Runnable runnable) {
   
        // 获取当前线程的 MDC 上下文信息
        //final var contextMap = MDC.getCopyOfContextMap();
		Map<String, String> contextMap = MDC.getCopyOfContextMap();
        return () -> {
   
            try {
   
                // 设置新的执行线程的 MDC 上下文信息
                if (contextMap != null) {
   
                    MDC.setContextMap(contextMap);
                }
                // 执行任务
                runnable.run();
            } finally {
   
                // 清除 MDC 上下文信息
                MDC.clear();
            }
        };
    }
}

针对线程工厂修饰

利用 ThreadFactory 创建线程针对内容修饰MDC

public static class MdcThreadFactory implements ThreadFactory {
   
    @Override
    public Thread newThread(Runnable r) {
   
        // 在创建线程时设置 MDC 上下文信息
        return new Thread(() -> {
   
            // 获取当前线程的 MDC 上下文信息
            final Map<String, String> contextMap = MDC.getCopyOfContextMap();

            try {
   
                // 在新线程中设置 MDC 上下文信息
                if (contextMap != null) {
   
                    MDC.setContextMap(contextMap);
                }

                // 执行任务
                r.run();
            } finally {
   
                // 清除 MDC 上下文信息
                MDC.clear();
            }
        });
    }
}

1.1、设置注解线程池的配置

1、利用配置异步方法的执行器 AsyncConfigurer 来配置注解线程池

2、 setTaskDecoratorsetThreadFactory 会以最后一个配置的为准

@Component
public class TraceAsyncConfigurer implements AsyncConfigurer {
   
        @Override
    public Executor getAsyncExecutor() {
   
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(8);
        executor.setMaxPoolSize(16);
        executor.setQueueCapacity(100);
        executor.setThreadNamePrefix("async-pool-");
        //①用来直接修改装饰器
        executor.setTaskDecorator(new MdcTaskDecorator());
        //② 也可以直接用ThreadFactory
        executor.setThreadFactory(new MdcThreadFactory());
        executor.setWaitForTasksToCompleteOnShutdown(true);
        executor.initialize();
        return executor;
    }
    
}

1.2、设置直接使用的线程配置

在工厂类直接配置MDC即可

ThreadPoolExecutor executor = new ThreadPoolExecutor(8, 
        16, 
        15000, 
        TimeUnit.MILLISECONDS, 
        new SynchronousQueue<Runnable>(),
        new MdcThreadFactory(), 
        new ThreadPoolExecutor.AbortPolicy());

2、针对web上下文配置

利用 HandlerInterceptor 直接拦截

public class TraceHandlerInterceptor implements HandlerInterceptor {
   

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
   
        String traceId = request.getHeader(TraceConstant.TRACE_ID);
        if (!StringUtils.hasText(traceId)) {
   
            traceId = TraceUtils.traceIdGenerator();
        }
        MDC.put(TraceConstant.TRACE_ID, traceId);
        return true;
    }


    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
   
        MDC.remove(TraceConstant.TRACE_ID);
        HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
    }
}

3、Feign客户端的配置

RequestInterceptor 是Feign客户端库中的一个接口,用于拦截Feign客户端发出的HTTP请求,利用这个特性包装

@Component
public class FeignRequestInterceptor implements RequestInterceptor {

    private final Logger logger = LoggerFactory.getLogger(FeignRequestInterceptor.class);

    public FeignRequestInterceptor(){
        logger.info("Initializing feign trace interceptor");
    }

    @Override
    public void apply(RequestTemplate requestTemplate) {
        requestTemplate.header(TraceConstant.TRACE_ID, TraceContextUtils.getTraceId());
    }
}

4、利用javaagent的思路配置

这里可以看做是相对无侵入式的一种模式把,偷懒是要慢慢来

其实这里存在 静态注入动态注入 ,但是走静态把

动态注入的方式: 实际上需要改造一下代码,使得可以后续动态java-agent内容。

**静态注入的方式:**只需要变更jvm,增加 -javaagent: /路径 即可

4.1、基础环境内容

1、全限定类名: org.examlpe.MDCAgent

2、 测试端点包名 :org.example.controller

3、 打包后路径地址 :/opt/java-agent-1.0-SNAPSHOT-jar-with-dependencies.jar

<!-- https://mvnrepository.com/artifact/org.javassist/javassist
 当然这里需要引入一个依赖方便下面对类的魔改 
-->
<dependency>
    <groupId>org.javassist</groupId>
    <artifactId>javassist</artifactId>
    <version>3.29.1-GA</version>
</dependency>

构建打包插件

  <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-assembly-plugin</artifactId>
                <version>3.1.1</version>
                <configuration>
                    <descriptorRefs>
	                      <!--  这个为最终输出的名称-->
                        <descriptorRef>jar-with-dependencies</descriptorRef>
                    </descriptorRefs>
                    <archive>
                        <!-- 设置manifest配置文件-->
                        <manifestEntries>
                            <!--Premain-Class: 代表 Agent 静态加载时会调用的类全路径名。-->
                            <Premain-Class>org.example.MDCAgent</Premain-Class>
                            <!--Agent-Class: 代表 Agent 动态加载时会调用的类全路径名。-->
                            <Agent-Class>org.example.MDCAgent</Agent-Class>
                            <!--Can-Redefine-Classes: 是否可进行类定义。-->
                            <Can-Redefine-Classes>true</Can-Redefine-Classes>
                            <!--Can-Retransform-Classes: 是否可进行类转换。-->
                            <Can-Retransform-Classes>true</Can-Retransform-Classes>
                        </manifestEntries>
                    </archive>
                </configuration>
                <executions>
                    <execution>
                        <!--绑定到package生命周期阶段上-->
                        <phase>package</phase>
                        <goals>
                            <!--绑定到package生命周期阶段上-->
                            <goal>single</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

4.2、利用 premain 预加载

利用 premain 的特性提前于主方法前加载 Instrumentation

使得我们可以方便操作类的一些内容(悄悄改)

public class MDCAgent {
   
    private static final Logger logger = LoggerFactory.getLogger(MDCAgent.class);
    /**
     * @todo 这里实际上我们只需要针对控制层进行补充即可
     */
    private static final String TRANSFORM_PREIFX = "org/example/controller";

    public static void premain(String args, Instrumentation instrumentation) {
   

        System.out.println("premain start!");
        addTransformer(instrumentation);
        System.out.println("premain end!");

    }
    
     private static void addTransformer(Instrumentation instrumentation) {
   
      	....   
     }
}

4.3、利用 Instrumentation 对类加载前魔改

1、 Modifier.isNative(method.getModifiers()) 这部分是搜罗的经验之谈

2、主要目的是针对 测试端点包名 内容进行注入

 instrumentation.addTransformer(new ClassFileTransformer() {
   
            public byte[] transform(ClassLoader l, String className, Class<?> c, ProtectionDomain pd, byte[] b) {
   
                try {
   
					//判断是否为目标包下的类
                    if (className != null && className.startsWith(TRANSFORM_PREIFX)) {
   
                        // 类属于目标包名下的处理逻辑
                        System.out.println("Class " + className + " belongs to target package.");
                        // 其他的字节码转换逻辑
                        final ClassPool classPool = ClassPool.getDefault();
                        final CtClass clazz = classPool.get(className.replace("/", "."));

                        for (CtMethod method : clazz.getMethods()) {
   
                            /*
                             * Modifier.isNative(methods[i].getModifiers())过滤本地方法,否则会报
                             * javassist.CannotCompileException: no method body  at javassist.CtBehavior.addLocalVariable()
                             */
                            if (Modifier.isNative(method.getModifiers())) {
   
                                continue;
                            }


                            String traceId = MDC.get(TraceConstant.TRACE_ID);
                            if (!StringUtils.hasText(traceId)) {
   
                                traceId = TraceUtils.traceIdGenerator();
                            }
                            MDC.put(TraceConstant.TRACE_ID, traceId);
//							这里正常不需要,仅用于查看效果                            
//                            method.insertBefore("System.out.println(\"" + clazz.getSimpleName() + "."
//                                    + method.getName() + "-" + traceId + " start.\");");
//
//                            method.insertAfter("System.out.println(\"" + clazz.getSimpleName() + "."
//                                    + method.getName() + "-" + MDC.get("haha") + " end.\");", false);
                        }

                        return clazz.toBytecode();
                    }
                } catch (Exception e) {
   
                    e.printStackTrace();
                }

                return b;
            }
        }, true);

    }

4.4、构建测试

目标vm参数 :-javaagent: /opt/java-agent-1.0-SNAPSHOT-jar-with-dependencies.jar

@GetMapping("ping")
public String ping() throws InterruptedException {
   
    return "pong";
}

//样例测试结果
//DemoController.ping1-ab83c3c3-08aa-4e9d-9d6f-513967eb28ff start.
//DemoController.ping1-ab83c3c3-08aa-4e9d-9d6f-513967eb28ff end.

相关推荐

  1. MDC 日志跟踪笔记

    2024-02-20 22:00:03       27 阅读
  2. 趋势跟踪-笔记

    2024-02-20 22:00:03       15 阅读
  3. 分布式微服务架构日志调用链路跟踪-traceId

    2024-02-20 22:00:03       46 阅读
  4. springboot单体项目链路日志跟踪及接口耗时

    2024-02-20 22:00:03       18 阅读
  5. LinuxBasicsForHackers笔记 -- 日志系统

    2024-02-20 22:00:03       36 阅读

最近更新

  1. TCP协议是安全的吗?

    2024-02-20 22:00:03       16 阅读
  2. 阿里云服务器执行yum,一直下载docker-ce-stable失败

    2024-02-20 22:00:03       16 阅读
  3. 【Python教程】压缩PDF文件大小

    2024-02-20 22:00:03       15 阅读
  4. 通过文章id递归查询所有评论(xml)

    2024-02-20 22:00:03       18 阅读

热门阅读

  1. 汽车研发与制造中英文对照

    2024-02-20 22:00:03       32 阅读
  2. C#面:.NET中所有类型的基类是什么

    2024-02-20 22:00:03       30 阅读
  3. HTML世界核心

    2024-02-20 22:00:03       26 阅读
  4. c编译器学习01:tcc、chibicc简介

    2024-02-20 22:00:03       24 阅读
  5. Docker Compose 的安装方

    2024-02-20 22:00:03       27 阅读
  6. 华纳云:Nginx的内存池如何实现,有哪些特点

    2024-02-20 22:00:03       30 阅读
  7. 【Vue3】defineExpose

    2024-02-20 22:00:03       31 阅读
  8. P14 前缀和原理和特点

    2024-02-20 22:00:03       26 阅读
  9. element-plus_message.js

    2024-02-20 22:00:03       30 阅读
  10. docker安装milvus后,无法打开attu,日志报错

    2024-02-20 22:00:03       29 阅读
  11. 力扣题目-178. 分数排名

    2024-02-20 22:00:03       27 阅读