SpringBoot 集成logback日志链路追踪

news2025/3/16 9:27:04

项目场景

有时候一个业务调用链场景,很长,调了各种各样的方法,看日志的时候,各个接口的日志穿插,确实让人头大。

为了解决这个痛点,就使用了 TraceId,根据 TraceId 关键字进入服务器查询日志中是否有这个 TraceId, 这样就把同一次的业务调用链上的日志串起来了。

实现步骤

1、pom.xml 依赖

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-logging</artifactId>
    </dependency>
    <!--lombok配置-->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>1.16.10</version>
    </dependency>
</dependencies>

2、整合 logback,打印日志,logback-spring.xml (简单配置下)

关键代码:[traceId:%X{traceId}], traceId 是通过拦截器里 MDC.put(traceId, tid) 添加

<?xml version="1.0" encoding="UTF-8"?>
<configuration debug="false">
    <!--日志存储路径-->
    <property name="log" value="D:/test/log" />
    <!-- 控制台输出 -->
    <appender name="console" class="ch.qos.logback.core.ConsoleAppender">
        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
            <!--输出格式化-->
            <pattern>[traceId:%X{traceId}]  %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
        </encoder>
    </appender>
    <!-- 按天生成日志文件 -->
    <appender name="file" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <!--日志文件名-->
            <FileNamePattern>${log}/%d{yyyy-MM-dd}.log</FileNamePattern>
            <!--保留天数-->
            <MaxHistory>30</MaxHistory>
        </rollingPolicy>
        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
            <pattern>[traceId:%X{traceId}]  %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
        </encoder>
        <!--日志文件最大的大小-->
        <triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
            <MaxFileSize>10MB</MaxFileSize>
        </triggeringPolicy>
    </appender>

    <!-- 日志输出级别 -->
    <root level="INFO">
        <appender-ref ref="console" />
        <appender-ref ref="file" />
    </root>
</configuration>

3、application.yml

server:
  port: 8826
logging:
  config: classpath:logback-spring.xml

4、自定义日志拦截器 LogInterceptor.java

用途:每一次链路,线程维度,添加最终的链路 ID traceId.

MDC(Mapped Diagnostic Context) 诊断上下文映射,是 @Slf4j 提供的一个支持动态打印日志信息的工具。

import org.slf4j.MDC;
import org.springframework.lang.Nullable;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.UUID;

/**
 * 日志拦截器
 */
publicclass LogInterceptor implements HandlerInterceptor {

    privatestaticfinal String traceId = "traceId";

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        String tid = UUID.randomUUID().toString().replace("-", "");
        //可以考虑让客户端传入链路ID,但需保证一定的复杂度唯一性;如果没使用默认UUID自动生成
        if (!StringUtils.isEmpty(request.getHeader("traceId"))){
            tid=request.getHeader("traceId");
        }
        MDC.put(traceId, tid);
        returntrue;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,
                                @Nullable Exception ex) {
        // 请求处理完成后,清除MDC中的traceId,以免造成内存泄漏
        MDC.remove(traceId);
    }

}

5、WebConfigurerAdapter.java 添加拦截器

其实这个拦截的部分改为使用自定义注解 + AOP 也是很灵活的。
import javax.annotation.Resource;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;


@Configuration
publicclass WebConfigurerAdapter extends WebMvcConfigurationSupport {
    @Resource
private LogInterceptor logInterceptor;

@Override
public void addInterceptors(InterceptorRegistry registry) {
  registry.addInterceptor(logInterceptor);
//可以具体制定哪些需要拦截,哪些不拦截,其实也可以使用自定义注解更灵活完成
//                .addPathPatterns("/**")
//                .excludePathPatterns("/testxx.html");
 }
}

6、测试接口

import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;

@RestController
@Api(tags = "测试接口")
@RequestMapping("/test")
@Slf4j
publicclass TestController {


@RequestMapping(value = "/log", method = RequestMethod.GET)
@ApiOperation(value = "测试日志")
public String sign() {
  log.info("这是一行info日志");
  log.error("这是一行error日志");
return"success";
 }
}

异步场景

使用线程的场景,写一个异步线程,加入这个调用里面。再次执行看开效果,我们会发现显然子线程丢失了 traceId。

所以我们需要针对子线程使用情形,做调整,思路:将父线程的 traceId 传递下去给子线程即可。

1、ThreadMdcUtil.java

import org.slf4j.MDC;

import java.util.Map;
import java.util.UUID;
import java.util.concurrent.Callable;

/**
 * @Author: JCccc
 * @Date: 2022-5-30 11:14
 * @Description:
 */
publicfinalclass ThreadMdcUtil {
    privatestaticfinal String traceId = "traceId";

    // 获取唯一性标识
    public static String generateTraceId() {
        return UUID.randomUUID().toString().replace("-", "");
    }

    public static void setTraceIdIfAbsent() {
        if (MDC.get(traceId) == null) {
            MDC.put(traceId, generateTraceId());
        }
    }

    /**
     * 用于父线程向线程池中提交任务时,将自身MDC中的数据复制给子线程
     *
     * @param callable
     * @param context
     * @param <T>
     * @return
     */
    publicstatic <T> Callable<T> wrap(final Callable<T> callable, final Map<String, String> context) {
        return () -> {
            if (context == null) {
                MDC.clear();
            } else {
                MDC.setContextMap(context);
            }
            setTraceIdIfAbsent();
            try {
                return callable.call();
            } finally {
                MDC.clear();
            }
        };
    }

    /**
     * 用于父线程向线程池中提交任务时,将自身MDC中的数据复制给子线程
     *
     * @param runnable
     * @param context
     * @return
     */
    public static Runnable wrap(final Runnable runnable, final Map<String, String> context) {
        return () -> {
            if (context == null) {
                MDC.clear();
            } else {
                MDC.setContextMap(context);
            }
            setTraceIdIfAbsent();
            try {
                runnable.run();
            } finally {
                MDC.clear();
            }
        };
    }
}

2、MyThreadPoolTaskExecutor.java 是我们自己写的,重写了一些方法

import org.slf4j.MDC;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

import java.util.concurrent.Callable;
import java.util.concurrent.Future;

publicfinalclass MyThreadPoolTaskExecutor  extends ThreadPoolTaskExecutor  {
    public MyThreadPoolTaskExecutor() {
        super();
    }
    
    @Override
    public void execute(Runnable task) {
        super.execute(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap()));
    }


    @Override
    public <T> Future<T> submit(Callable<T> task) {
        returnsuper.submit(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap()));
    }

    @Override
    public Future<?> submit(Runnable task) {
        returnsuper.submit(ThreadMdcUtil.wrap(task, MDC.getCopyOfContextMap()));
    }
}

3、ThreadPoolConfig.java 定义线程池,交给 Spring 管理

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;

import java.util.concurrent.Executor;

@EnableAsync
@Configuration
publicclass ThreadPoolConfig {
    /**
     * 声明一个线程池
     */
    @Bean("taskExecutor")
    public Executor taskExecutor() {
        MyThreadPoolTaskExecutor executor = new MyThreadPoolTaskExecutor();
        //核心线程数5:线程池创建时候初始化的线程数
        executor.setCorePoolSize(5);
        //最大线程数5:线程池最大的线程数,只有在缓冲队列满了之后才会申请超过核心线程数的线程
        executor.setMaxPoolSize(5);
        //缓冲队列500:用来缓冲执行任务的队列
        executor.setQueueCapacity(500);
        //允许线程的空闲时间60秒:当超过了核心线程出之外的线程在空闲时间到达之后会被销毁
        executor.setKeepAliveSeconds(60);
        //线程池名的前缀:设置好了之后可以方便我们定位处理任务所在的线程池
        executor.setThreadNamePrefix("taskExecutor-");
        executor.initialize();
        return executor;
    }
}

4、Service

import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;

/**
 * 测试Service
 */
@Service("testService")
@Slf4j
publicclass TestService {

/**
  * 异步操作测试
  */
@Async("taskExecutor")
public void asyncTest() {
try {
   log.info("模拟异步开始......");
   Thread.sleep(3000);
   log.info("模拟异步结束......");
  } catch (InterruptedException e) {
   log.error("异步操作出错:"+e);
  }

 }

}

5、测试接口

import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;

@RestController
@Api(tags = "测试接口")
@RequestMapping("/test")
@Slf4j
publicclass TestController {

@Resource
private TestService testService;

@RequestMapping(value = "/log", method = RequestMethod.GET)
@ApiOperation(value = "测试日志")
public String sign() {
  log.info("这是一行info日志");
  log.error("这是一行error日志");

//异步操作测试
  testService.asyncTest();
return"success";
 }
}

结果:

我们可以看到,子线程的日志也被串起来了。

定时任务

如果使用了定时任务 @Scheduled, 这时候执行定时任务,不会走上面的拦截器逻辑,所以定时任务需要单独创建个 AOP 切面。

1、创建个定时任务线程池

import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.SchedulingConfigurer;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;

import java.util.concurrent.Executors;

/**
 * 定时任务线程池
 */
@EnableScheduling
@Configuration
publicclass SeheduleConfig implements SchedulingConfigurer{
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
  taskRegistrar.setScheduler(Executors.newScheduledThreadPool(5));
 }
}

2、创建 AOP 切面

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.MDC;
import org.springframework.context.annotation.Configuration;

import java.util.UUID;

@Aspect   //定义一个切面
@Configuration
publicclass SeheduleTaskAspect {

    // 定义定时任务切点Pointcut
    @Pointcut("@annotation(org.springframework.scheduling.annotation.Scheduled)")
    public void seheduleTask() {
    }

    @Around("seheduleTask()")
    public void doAround(ProceedingJoinPoint joinPoint) throws Throwable {
try {
   String traceId = UUID.randomUUID().toString().replace("-", "");
   //用于日志链路追踪,logback配置:%X{traceId}
   MDC.put("traceId", traceId);
   //执行定时任务方法
         joinPoint.proceed();
  } finally {
   //请求处理完成后,清除MDC中的traceId,以免造成内存泄漏
   MDC.remove("traceId");
  }
    }

}

3、创建定时任务测试

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;

import java.util.Date;

@Service
publicclass SeheduleTasks {

private Logger logger = LoggerFactory.getLogger(SeheduleTasks.class);

/**
  * 1分钟执行一次
  */
@Scheduled(cron = "0 0/1 * * * ?")
public void testTask() {
  logger.info("执行定时任务>"+new Date());
 }

}

总结

服务启动的时候 traceId 是空的,这是正常的,因为还没到拦截器这一层。

API 说明

  • clear() => 移除所有 MDC

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

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

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

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2315943.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【Python办公】Excel通用匹配工具(双表互匹)

目录 专栏导读1、背景介绍2、库的安装3、核心代码4、完整代码总结专栏导读 🌸 欢迎来到Python办公自动化专栏—Python处理办公问题,解放您的双手 🏳️‍🌈 博客主页:请点击——> 一晌小贪欢的博客主页求关注 👍 该系列文章专栏:请点击——>Python办公自动化专…

【JAVA】七、基础知识“if+switch+循环结构”详细讲解~简单易懂!

目录 7、逻辑控制 7.1 分支结构 7.1.1 if 语句 语法格式1 语法格式2 语法格式3 7.1.2 switch语句 基本语法 执行流程 7.2 循环结构 7.2.1 while循环 语法格式 7.2.2 Break 7.2.3 Continue 7.2.4 for循环 语法格式 执行过程 7.2.5 do while循环 语法格式 7.3 …

【C++】每日一练(轮转数组)

本篇博客给大家带来的是用C语言来解答轮转数组&#xff01; &#x1f41f;&#x1f41f;文章专栏&#xff1a;每日一练 &#x1f680;&#x1f680;若有问题评论区下讨论&#xff0c;我会及时回答 ❤❤欢迎大家点赞、收藏、分享&#xff01; 今日思想&#xff1a;不服输的少年啊…

Python(学习一)

做网站有成熟的框架像FLASK、DJANGO、TORNADO&#xff0c;写爬虫有好用到哭的REQUESTS&#xff0c;还有强大到没盆友的SCRAPY 随着NUMPY、SCIPY、MATLOTLIB等众多第三方模块的开发和完善&#xff0c;不仅支持py支持各种数学运算&#xff0c;还可以绘制高质量的2D和3D图像&…

Java中类和对象

类和对象 面向对象的认识类的定义和使用1 类的定义2 类的创建3 类的实例化 构造方法1 构造方法的概念2 构造方法的注意事项 this关键字 面向对象的认识 前言 何为面向对象何为面向过程呢&#xff1f;&#xff0c;C语言是最经典的面向过程的语言,但是C语言虽然可以解决一定的问…

文本组件+Image组件+图集

Canvas部分知识补充 元素渲染顺序 以Hierarchy参考 下方物体在上方物体前显示 子物体在父物体前显示 下方物体永远在前显示&#xff0c;无论上方的层次结构 资源导入 绝对路径&#xff1a;C:\Windows\Fonts下的许多字体可以用做UIText的字体资源 图片导入&#xff1a; 1.图…

PyCharm 2019.1.3使用python3.9创建虚拟环境setuptools-40.8.0报错处理

目录 前置&#xff1a; 一劳永逸方法&#xff08;缺最后一步&#xff0c;没有成行&#xff09; step one: 下载高版本的pip、setuptools、virtualenv的tar.gz包 step two: 进入PyCharm安装目录的 helpers 目录下 step three: 下载并安装grep和sed命令&#xff0c;然后执行 …

服务器部署RocketMQ----Docker方式

拉取镜像并创建docker network 按照官方文档提供的方式拉取镜像&#xff1a;docker pull apache/rocketmq:4.9.6 创建一个docker网络&#xff1a;docker network create rocketmq 启动NameServer以及Broker 启动NameServer # 启动NameServer docker run -d --name rmqnames…

【推荐项目】052-用水监控管理系统

052-用水监控管理系统 介绍 用水监控管理系统 springboot java vuejs jdk1.8 当然&#xff0c;以下是一个简洁的用水监控管理系统的功能模块划分&#xff0c;基于Spring Boot&#xff08;JDK 1.8&#xff09;后端和Vue.js前端&#xff1a; 用水监控管理系统功能模块 后端&…

零基础上手Python数据分析 (2):Python核心语法快速入门

写在前面 场景:每周销售数据报表整理 任务描述: 你需要每周从多个Excel文件中汇总销售数据,计算各项指标(销售额、订单量、客单价等),并生成周报。Excel操作痛点: 文件太多,手动打开复制粘贴,效率低下,容易出错。 多个Excel文件,每个都要打开、筛选、复制数据,重复…

游戏引擎学习第160天

回顾和今天的计划 我们没有使用任何游戏引擎和库&#xff0c;完全靠我们自己&#xff0c;使用的是老式的编程方式。 我们已经构建了很多内容&#xff0c;游戏引擎开发也慢慢接近尾声。现在我们已经接近完成了所有为支持游戏开发所需要的工作&#xff0c;接下来将逐步过渡到游戏…

从零搭建微服务项目Pro(第2-2章——JSR303自定义文件校验+整合至微服务公共模块)

前言&#xff1a; JSR 303&#xff0c;即 Bean Validation&#xff0c;是 Java EE 6 中的一项子规范&#xff0c;旨在为 Java Bean 提供一种标准化的数据验证机制。它通过注解的方式&#xff0c;允许开发者在 Java 类的字段或方法上直接定义验证规则&#xff0c;从而将验证逻辑…

如何用URDF文件构建机械手模型并与MoveIt集成

机械手URDF文件的编写 我们用urdf文件来描述我们的机械手的外观以及物理性能。这里为了简便&#xff0c;就只用了基本的圆柱、立方体了。追求美观的朋友&#xff0c;还可以用dae文件来描述机械手的外形。 import re def remove_comments(text):pattern r<!--(.*?)-->…

【训练细节解读】文本智能混合分块(Mixtures of Text Chunking,MoC)引领RAG进入多粒度感知智能分块阶段

喜欢本文可以在主页订阅专栏哟 核心创新&#xff1a;双重评估指标与混合分块架构&#xff1a; 第一章&#xff1a;检索增强生成&#xff08;RAG&#xff09;技术演进与分块挑战 1.1 RAG架构的核心演变 检索增强生成&#xff08;Retrieval-Augmented Generation&#xff09…

招聘信息|基于SprinBoot+vue的招聘信息管理系统(源码+数据库+文档)

招聘信息管理系统 目录 基于SprinBootvue的招聘信息管理系统 一、前言 二、系统设计 三、系统功能设计 5.1系统功能模块 5.2管理员功能模块 5.3企业后台管理模块 5.4用户后台管理模块 四、数据库设计 五、核心代码 六、论文参考 七、最新计算机毕设选题推荐 八、…

HCIA-AI人工智能笔记1:大模型技术演进与发展历程

一、大模型发展的技术演进图谱 timelinetitle 大模型发展关键里程碑1958 : 感知机模型诞生&#xff08;Frank Rosenblatt&#xff09;1986 : BP反向传播算法&#xff08;Rumelhart&#xff09;2012 : AlexNet开启深度学习时代2017 : Transformer架构提出&#xff08;《Attenti…

在微信小程序或前端开发中,picker 和 select 都是用户交互中用于选择的组件,但它们在功能、设计和使用场景上有一定的区别

在微信小程序或前端开发中&#xff0c;picker 和 select 都是用户交互中用于选择的组件&#xff0c;但它们在功能、设计和使用场景上有一定的区别。 1. picker 的特点 描述&#xff1a; picker 是微信小程序中的原生组件&#xff0c;通常用于选择单项或多项值&#xff0c;如时…

向量数据库对比以及Chroma操作

一、向量数据库与传统类型数据库 向量数据库&#xff08;Vector Storage Engine&#xff09;与传统类型的数据库如关系型数据库&#xff08;MySQL&#xff09;、文档型数据库&#xff08;MongoDB&#xff09;、键值存储&#xff08;Redis&#xff09;、全文搜索引擎&#xff0…

Python Matplotlib面试题精选及参考答案

绘制函数 y2x5 在区间 [1,10] 的折线图&#xff0c;设置标题和坐标轴标签 要绘制函数 y 2x 5 在区间 [1, 10] 的折线图&#xff0c;并设置标题和坐标轴标签&#xff0c;可借助 Python 的 matplotlib 库来实现。以下是详细的实现步骤与代码示例。 首先&#xff0c;要导入 mat…

正点原子[第三期]Arm(iMX6U)Linux移植学习笔记-5.1 uboot顶层Makefile分析-VSCode工程创建

前言&#xff1a; 本文是根据哔哩哔哩网站上“Arm(iMX6U)Linux系统移植和根文件系统构键篇”视频的学习笔记&#xff0c;在这里会记录下正点原子 I.MX6ULL 开发板的配套视频教程所作的实验和学习笔记内容。本文大量引用了正点原子教学视频和链接中的内容。 引用&#xff1a; …