Java中最简单的添加日志链路的方式之一

news/2025/2/11 21:27:41/

Java项目中添加日志链路功能的设计与实现


文章目录

  • Java项目中添加日志链路功能的设计与实现
  • 前言
  • 一、日志链路的概念与作用
  • 二、添加日志链路的设计思路
  • 三、如何支持多线程下的日志打印也附加上日志链路id
    • 1. 示例1:实现Runnable接口,无返回值
    • 2. 示例2:实现Callable接口,有返回值
    • 3.使用示例
  • 总结


前言

在Java项目的开发过程中,日志记录是不可或缺的一部分。它不仅可以帮助我们追踪程序的执行过程,还可以在出现问题时提供关键的调试信息,特别是当线上出现问题时,详细的日志可以帮助我们更快的定位到问题。然而,随着项目的不断壮大和复杂度的增加,单一的日志记录往往难以满足需求。因此,引入日志链路功能,将不同模块、不同层级的日志信息串联起来,形成完整的执行路径,对于提升项目的可维护性和调试效率具有重要意义。

本文主要是针对单体项目,以单项目为基础例子进行展开讲解,。


一、日志链路的概念与作用

日志链路是指将一次完整的业务请求或操作过程中产生的所有日志信息按照时间顺序和逻辑关系串联起来,形成一个完整的日志链。通过日志链路,我们可以清晰地看到业务请求从接收、处理到完成的整个过程,以及其中各个环节的详细执行情况。这对于分析性能瓶颈、定位故障原因以及优化系统性能等方面都具有重要作用。

二、添加日志链路的设计思路

在Java项目中添加日志链路功能,可以从以下几个方面进行设计:

  1. 定义一个全局唯一的标识(如UUID)作为日志链路的ID,用于标识一次完整的业务请求或操作。

简单示例代码:

public class TraceIdGenerator {private static final AtomicLong counter = new AtomicLong(0);/*** 生成一个随机的trace_id。* 这个trace_id由UUID和递增的计数器组成,确保在单一JVM实例中的唯一性。* 如果需要跨多个JVM实例的唯一性,请仅使用UUID部分。* 注意,仅仅使用UUID也并不能保证在极端情况下(例如全球范围内的大量并发请求)的绝对唯一性** @return 生成的trace_id*/public static String generateTraceId() {String uuidPart = UUID.randomUUID().toString().replace("-", ""); // 移除UUID中的横线,使其更简洁long counterPart = counter.incrementAndGet(); // 递增计数器// 拼接UUID和计数器部分,确保在单一JVM实例中的唯一性return uuidPart + "-" + counterPart;}public static void main(String[] args) {// 测试生成几个trace_idfor (int i = 0; i < 5; i++) {System.out.println(generateTraceId());}}
}
  1. 在业务请求的入口处生成该标识,并将其传递给后续的处理环节。此步骤可以统一使用过滤器或者拦截器都可以,正常现在每个项目在进
    入业务流程之前都会有一个解析用户信息的步骤,可以放在同一个步骤上。传递这个标识可以通过自定义日志记录器或使用现有的日志框架(如SLF4J、Logback等)的MDC(Mapped Diagnostic Context)功能来实现。

代码示例:MDC存放的逻辑你可以理解成map类型(底层是ThreadLocal),存放的数据是key、value形式。BaseConstants.TRACE_ID是定义的一个key,TraceIdGenerator.generateTraceId()是生成的链路id,放在MDC之后在线程的上下文就可以直接使用MDC.get(BaseConstants.TRACE_ID)去获取链路id。

public class LogFilter extends OncePerRequestFilter {private final Logger logger = LoggerFactory.getLogger(LogFilter.class);@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {filter(request,response, filterChain);}private void filter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {request.setCharacterEncoding(StandardCharsets.UTF_8.name());try {//将threadId和traceId放入mdc   BaseConstants.TRACE_ID = "TRACE_ID"MDC.put(BaseConstants.TRACE_ID, TraceIdGenerator.generateTraceId());MDC.put(BaseConstants.MDC_THREAD_ID, String.valueOf(Thread.currentThread().getId()));//在响应头中设置traceIdresponse.setHeader(BaseConstants.TRACE_ID,MDC.get(BaseConstants.TRACE_ID));logger.debug("=== begin request processing ===");RequestWrapper requestWrapper = new RequestWrapper(request);chain.doFilter(requestWrapper, response);} finally {logger.debug("=== finish request processing ===");//移除 threadId和traceIdMDC.remove(BaseConstants.TRACE_ID);MDC.remove(BaseConstants.MDC_THREAD_ID);}}
//过滤器需要注册,且需要配置相关的规则
@Configuration
public class WebConfig {@Beanpublic FilterRegistrationBean<LogFilter> platformFilter() {FilterRegistrationBean<LogFilter> filterRegBean = new FilterRegistrationBean<>();//设置过滤器filterRegBean.setFilter(new LogFilter());//设置过滤路径:所有请求路径filterRegBean.addUrlPatterns("/*");//设置过滤顺序:数字越小,会在越前面过滤filterRegBean.setOrder(-9999);return filterRegBean;}
}
  1. 在各个处理环节中,将日志链路的ID作为日志记录的一部分,确保所有与该请求相关的日志都能被正确关联。这里其实就是配置日志文件的输出格式,带上日志链路id即可。可以看到配置的输出中有TRACE_ID,这是因为在第二个步骤中,存放在MDC中的日志链路id的key是TRACE_ID。
logging.file.name=log-demo1.log
logging.level.root=info
logging.level.com.jzs.log=debug
logging.level.com.jzs.log.console=debug
logging.pattern.console=[%d{MM-dd HH:mm:ss.SSS}][%thread][%level][LOG-DEMO1] %cyan([%logger{50}.%F:%L]):[%X{TRACE_ID}] %msg%n
logging.pattern.file=[%d{MM-dd HH:mm:ss.SSS}][%thread][%level][LOG-DEMO1] %cyan([%logger{50}.%F:%L]):[%X{TRACE_ID}] %msg%n
  1. 最后,我们需要一种机制来收集具有相同日志链路ID的日志信息,并按照时间顺序和逻辑关系进行排序和展示。这可以通过日志收集系统(如ELK Stack、ClickHouse+可视化界面等)来实现。这些系统通常支持按照特定字段(如本例中的traceId)对日志进行过滤和排序,从而方便地展示完整的日志链路。

三、如何支持多线程下的日志打印也附加上日志链路id

主要就在于重写类方法上,涉及的是Runnable和Callable这两个接口,下面给出示例和使用方式。

1. 示例1:实现Runnable接口,无返回值

public abstract class MyRunnable implements Runnable {private final Logger log = LoggerFactory.getLogger(MyRunnable.class);private String traceId;public MyRunnable() {traceId = MDC.get(BaseConstants.TRACE_ID);}public MyRunnable(String traceId) {this.traceId = traceId;}@Overridepublic void run() {try {MDC.put(BaseConstants.TRACE_ID, traceId);log.info("MyRunnable::run");doRun();} finally {MDC.remove(BaseConstants.TRACE_ID);}}public abstract void doRun();}

2. 示例2:实现Callable接口,有返回值

public abstract class MyCallable implements Callable {private final Logger log = LoggerFactory.getLogger(MyCallable.class);private String traceId;public MyCallable() {traceId = MDC.get(BaseConstants.TRACE_ID);}public MyCallable(String traceId) {this.traceId = traceId;}@Overridepublic Object call() throws Exception {try {MDC.put(BaseConstants.TRACE_ID, traceId);log.info("TraceCallable::call");return doCall();} finally {MDC.remove(BaseConstants.TRACE_ID);}}public abstract Object doCall();}

3.使用示例

@GetMapping("/test")
public Object test() throws Exception {ExecutorService executorService = Executors.newSingleThreadExecutor();log.info("进入test");new Thread(()->{//预期无打印traceIdlog.info("Thread::start");}).start();new Thread(new MyRunnable() {@Overridepublic void doRun() {}}).start();new Thread(new MyRunnable() {@Overridepublic void doRun() {log.info("MyRunnable::doRun::2");}}).start();executorService.submit(new MyRunnable() {@Overridepublic void doRun() {log.info("MyRunnable::doRun::3");}});Future future = executorService.submit(new MyCallable() {@Overridepublic Object doCall() {log.info("MyCallable::doCall");return "11111";}});log.info("MyCallable::call::{}",future.get());/*//没有通过子线程去创建会导致traceId丢失new MyRunnable(){@Overridepublic void doRun() {log.info("MyRunnable::doRun::");}}.run();String s = MDC.get(BaseConstants.TRACE_ID);MyCallable myCallable = new MyCallable() {@Overridepublic Object doCall() {log.info("MyCallable::doCall");return "11111";}};myCallable.call();
*/return ""+ MDC.get(BaseConstants.TRACE_ID);
}

打印如下:
在这里插入图片描述


总结

通过以上步骤,我们可以在Java项目中实现日志链路功能,提升项目的可维护性和调试效率。然而,日志链路只是日志管理的一个方面,我们还需要关注日志的级别控制、格式化、异步处理等方面,以构建一个完善的日志管理体系。当前的只是一个很简单的例子,从基础到深层次,慢慢的熟悉才能一步步的成长。

基于单体项目的日志链路,其实分布式也是可以套用的。像常见的调用下游系统或者分布式项目中的其他项目的业务接口,只需要在请求业务接口之前,将traceId放在请求头中或者是一个约定俗成的对象字段中,那么下游接口就可以获取到这个链路id并做处理,这样子当发现问题时,需要下游帮忙排查时只需要提供这个链路id,就可以更快的进行请求和错误定位。当然,分布式链路追踪肯定不止这么简单,但是原理都是一样的,只是功能更加完善,使用也比较方便。


http://www.ppmy.cn/news/1391493.html

相关文章

【ubuntu20.04+tensorflow-gpu1.14配置】

ubuntu20.04tensorflow-gpu1.14配置 目录0. 版本注意事项说明1. 个人目录下载后配置系统环境变量2. anaconda配置所有环境&#xff08;过程简便&#xff0c;但容易出现不兼容问题&#xff09;3. 验证tensorflow-gpu4. 一些细节 目录 总结出两种方法 个人目录 下载cuda和cudnn…

【openCV】手写算式识别

OpenCV 机器学习库提供了一系列 SVM 函数和类来实现 SVM 模型的训练和预测&#xff0c;方便用户实现自己的 SVM 模型&#xff0c;并应用于分类问题。本文主要介绍使用 openCV 实现手写算式识别的工作原理与实现过程。 目录 1 SVM 模型 1.1 SVM 模型介绍 1.2 SVM 模型原理 2…

语言设置和 时间设置 wifi设置和ip设置 这些设置属于什么设置?

这些设置通常归类于不同的设置类别&#xff0c;具体如下&#xff1a; 语言设置&#xff08;Language Settings&#xff09;和时间设置&#xff08;Time Settings&#xff09;&#xff1a; 这些设置属于系统设置&#xff08;System Settings&#xff09;或设备设置&#xff08;D…

StarRocks学习笔记

介绍场景建表明细模型聚合模型更新模型主键模型 介绍 StarRocks是一款经过业界检验、现代化&#xff0c;面向多种数据分析场景的、兼容MySQL协议的、高性能分布式关系型分析数据库。 StarRocks充分吸收关系型 OLAP 数据库和分布式存储系统在大数据时代的优秀研究成果&#xff…

JAVA安全(偏基础)

SQL注入 SQLI(SQL Injection)&#xff0c; SQL注入是因为程序未能正确对用户的输入进行检查&#xff0c;将用户的输入以拼接的方式带入SQL语句&#xff0c;导致了SQL注入的产生。攻击者可通过SQL注入直接获取数据库信息&#xff0c;造成信息泄漏。 JDBC JDBC有两个方法获取s…

全国产飞腾+FPGA架构,支持B码+12网口+多串电力通讯管理机解决方案

行业痛点: 中国的电力网络已经成为当今世界覆盖范围最广、结构最为复杂的人造科技系统。随着国家和各部委颁布了一系列法律法规&#xff0c;如国家颁布的《中华人民共和国网络安全法》、工信部颁布的《工业控制系统信息安全防护指南》、发改委颁布的14号令《电力监控系统安全防…

React 系列 之 React Hooks(一) JSX本质、理解Hooks

借鉴自极客时间《React Hooks 核心原理与实战》 JSX语法的本质 可以认为JSX是一种语法糖&#xff0c;允许将html和js代码进行结合。 JSX文件会通过babel编译成js文件 下面有一段JSX代码&#xff0c;实现了一个Counter组件 import React from "react";export defau…

Vue.js前端开发零基础教学(三)

目录 2.6 计算属性 2.7侦听器 2.8 样式绑定 2.8.1 绑定class属性 2.8.2 绑定style属性 2.9 阶段案例——学习计划表 2.6 计算属性 概念&#xff1a;Vue提供了计算属性来描述依赖响应式数据的复杂逻辑。 计算属性可以实时监听数据的变化&#xff0c;返回一个计算…