基于SpringBoot实现让日志像诗一样有韵律
时间:2025-11-05 16:01:38 出处:IT科技类资讯阅读(143)
前言
在传统系统中,基于如果能够提供日志输出,实现诗样基本上已经能够满足需求的让日。但一旦将系统拆分成两套及以上的志像系统,再加上负载均衡等,有韵调用链路就变得复杂起来。基于
特别是实现诗样进一步向微服务方向演化,如果没有日志的让日合理规划、链路追踪,志像那么排查日志将变得异常困难。有韵
比如系统A、基于B、实现诗样C,让日调用链路为A -> B -> C,志像如果每套服务都是有韵双活,则调用路径有2的三次方种可能性。如果系统更多,服务更多,调用链路则会成指数增长。
因此,无论是几个简单的内部服务调用,还是复杂的微服务系统,都需要通过一个机制来实现日志的链路追踪。网站模板让你系统的日志输出,像诗一样有形式美,又有和谐的韵律。
日志追踪其实已经有很多现成的框架了,比如Sleuth、Zipkin等组件。但这不是我们要讲的重点,本文重点基于Spring Boot、LogBack来手写实现一个简单的日志调用链路追踪功能。基于此实现模式,大家可以更细粒度的去实现。
Spring Boot中集成Logback
Spring Boot本身就内置了日志功能,这里使用logback日志框架,并对输出结果进行格式化。先来看一下SpringBoot对Logback的内置集成,依赖关系如下。当项目中引入了:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency>spring-boot-starter-web中间接引入了:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency>spring-boot-starter又引入了logging的starter:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-logging</artifactId> </dependency>在logging中真正引入了所需的logback包:
<dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> </dependency> <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-to-slf4j</artifactId> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>jul-to-slf4j</artifactId> </dependency>因此,我们使用时,只需将logback-spring.xml配置文件放在resources目录下即可。理论上配置文件命名为logback.xml也是支持的,但Spring Boot官网推荐使用的b2b供应网名称为:logback-spring.xml。
然后,在logback-spring.xml中进行日志输出的配置即可。这里不贴全部代码了,只贴出来相关日志输出格式部分,以控制台输出为例:
在value属性的表达式中,我们新增了自定义的变量值requestId,通过“[%X{requestId}]”的形式来展示。
这个requestId便是我们用来追踪日志的唯一标识。如果一个请求,从头到尾都使用了同一个requestId便可以把整个请求链路串联起来。如果系统还基于EKL等日志搜集工具进行统一收集,就可以更方便的查看整个日志的调用链路了。
那么,这个requestId变量是如何来的,又存储在何处呢?要了解这个,我们要先来了解一下日志框架提供的MDC功能。
什么是MDC?
MDC(Mapped Diagnostic Contexts) 是一个线程安全的存放诊断日志的容器。源码库MDC是slf4j提供的适配其他具体日志实现包的工具类,目前只有logback和log4j支持此功能。
MDC是线程独立、线程安全的,通常无论是HTTP还是RPC请求,都是在各自独立的线程中完成的,这与MDC的机制可以很好地契合。
在使用MDC功能时,我们主要使用是put方法,该方法间接的调用了MDCAdapter接口的put方法。
看一下接口MDCAdapter其中一个实现类BasicMDCAdapter中的代码来:
public class BasicMDCAdapter implements MDCAdapter { private InheritableThreadLocal<Map<String, String>> inheritableThreadLocal = new InheritableThreadLocal<Map<String, String>>() { @Override protected Map<String, String> childValue(Map<String, String> parentValue) { if (parentValue == null) { return null; } return new HashMap<String, String>(parentValue); } }; public void put(String key, String val) { if (key == null) { throw new IllegalArgumentException("key cannot be null"); } Map<String, String> map = inheritableThreadLocal.get(); if (map == null) { map = new HashMap<String, String>(); inheritableThreadLocal.set(map); } map.put(key, val); } // ... }通过源码可以看出内部持有一个InheritableThreadLocal的实例,该实例中通过HashMap来保存context数据。
此外,MDC提供了put/get/clear等几个核心接口,用于操作ThreadLocal中存储的数据。而在logback.xml中,可在layout中通过声明“%X{requestId}”这种形式来获得MDC中存储的数据,并进行打印此信息。
基于MDC的这些特性,因此它经常被用来做日志链路跟踪、动态配置用户自定义信息(比如requestId、sessionId等)等场景。
实战使用
上面了解了一些基础的原理知识,下面我们就来看看如何基于日志框架的MDC功能实现日志的追踪。
工具类准备
首先定义一些工具类,这个强烈建议大家将一些操作通过工具类的形式进行实现,这是写出优雅代码的一部分,也避免后期修改时每个地方都需要改。
TraceID(我们定义参数名为requestId)的生成类,这里采用UUID进行生成,当然可根据你的场景和需要,通过其他方式进行生成。
public class TraceIdUtils { /** * 生成traceId * * @return TraceId 基于UUID */ public static String getTraceId() { return UUID.randomUUID().toString().replace("-", ""); } }对Context内容的操作工具类TraceIdContext:
public class TraceIdContext { public static final String TRACE_ID_KEY = "requestId"; public static void setTraceId(String traceId) { if (StringLocalUtil.isNotEmpty(traceId)) { MDC.put(TRACE_ID_KEY, traceId); } } public static String getTraceId() { String traceId = MDC.get(TRACE_ID_KEY); return traceId == null ? "" : traceId; } public static void removeTraceId() { MDC.remove(TRACE_ID_KEY); } public static void clearTraceId() { MDC.clear(); } }通过工具类,方便所有服务统一使用,比如requestId可以统一定义,避免每处都不一样。这里不仅提供了set方法,还提供了移除和清理的方法。
需要注意的是,MDC.clear()方法的使用。如果所有的线程都是通过new Thread方法建立的,线程消亡之后,存储的数据也随之消亡,这倒没什么。但如果采用的是线程池的情况时,线程是可以被重复利用的,如果之前线程的MDC内容没有清除掉,再次从线程池中获取这个线程,会取出之前的数据(脏数据),会导致一些不可预期的错误,所以当前线程结束后一定要清掉。
Filter拦截
既然我们要实现日志链路的追踪,最直观的思路就是在访问的源头生成一个请求ID,然后一路传下去,直到这个请求完成。这里以Http为例,通过Filter来拦截请求,并将数据通过Http的Header来存储和传递数据。涉及到系统之间调用时,调用方设置requestId到Header中,被调用方从Header中取即可。
Filter的定义:
public class TraceIdRequestLoggingFilter extends AbstractRequestLoggingFilter { @Override protected void beforeRequest(HttpServletRequest request, String message) { String requestId = request.getHeader(TraceIdContext.TRACE_ID_KEY); if (StringLocalUtil.isNotEmpty(requestId)) { TraceIdContext.setTraceId(requestId); } else { TraceIdContext.setTraceId(TraceIdUtils.getTraceId()); } } @Override protected void afterRequest(HttpServletRequest request, String message) { TraceIdContext.removeTraceId(); } }在beforeRequest方法中,从Header中获取requestId,如果获取不到则视为“源头”,生成一个requestId,设置到MDC当中。当这个请求完成时,将设置的requestId移除,防止上面说到的线程池问题。系统中每个服务都可以通过上述方式实现,整个请求链路就串起来了。
当然,上面定义的Filter是需要进行初始化的,在Spring Boot中实例化方法如下:
@Configuration public class TraceIdConfig { @Bean public TraceIdRequestLoggingFilter traceIdRequestLoggingFilter() { return new TraceIdRequestLoggingFilter(); } }针对普通的系统调用,上述方式基本上已经能满足了,实践中可根据自己的需要在此基础上进行扩展。这里使用的是Filter,也可以通过拦截器、Spring的AOP等方式进行实现。
微服务中的Feign
如果你的系统是基于Spring Cloud中的Feign组件进行调用,则可通过实现RequestInterceptor拦截器来达到添加requestId效果。具体实现如下:
@Configuration public class FeignConfig implements RequestInterceptor { @Override public void apply(RequestTemplate requestTemplate) { requestTemplate.header(TraceIdContext.TRACE_ID_KEY, TraceIdContext.getTraceId()); } }结果验证
当完成上述操作之后,对一个Controller进行请求,会打印如下的日志:
2021-04-13 10:58:31.092 cloud-sevice-consumer-demo [http-nio-7199-exec-1] INFO [ef76526ca96242bc8e646cdef3ab31e6] c.b.demo.controller.CityController - getCity 2021-04-13 10:58:31.185 cloud-sevice-consumer-demo [http-nio-7199-exec-1] WARN [ef76526ca96242bc8e646cdef3ab31e6] o.s.c.o.l.FeignBlockingLoadBalancerClient -可以看到requestID已经被成功添加。当我们排查日志时,只需找到请求的关键信息,然后根据关键信息日志中的requestId值就可以把整个日志串联起来。
小结
最后,我们来回顾一下日志追踪的整个过程:当请求到达第一个服务器,服务检查requestId是否存在,如果不存在,则创建一个,放入MDC当中;服务调用其他服务时,再通过Header将requestId进行传递;而每个服务的logback配置requestId的输出。从而达到从头到尾将日志串联的效果。
在学习本文,如果你只学到了日志追踪,那是一种损失,因为文中还涉及到了SpringBoot对logback的集成、MDC的底层实现及坑、过滤器的使用、Feign的请求拦截器等。如果感兴趣,每个都可以发散一下,学习到更多的知识点。
猜你喜欢
- 电脑触控板拆除教程(轻松拆卸电脑触控板,维修无忧)
- 五分钟技术趣谈 | OCR在游戏加速效果上的应用
- 用 Eleventy 建立一个静态网站
- 消费者实现逻辑-kafka知识体系(四)
- 美图T8快充体验报告(一款强劲续航力的旗舰手机)
- Gulp打包支持Await/Async语法
- 一文看懂HarmonyOS服务卡片运行原理和开发方法
- 盘点LinkedList集合和LinkedList中定义的方法
- 大家好,本教程将学习如何在 ubuntu 15.04 上面安装 puppet,它可以用来管理你的服务器基础环境。puppet 是由 puppet 实验室Puppet Labs开发并维护的一款开源的配置管理软件,它能够帮我们自动化供给、配置和管理服务器的基础环境。不管我们管理的是几个服务器还是数以千计的计算机组成的业务报表体系,puppet 都能够使管理员从繁琐的手动配置调整中解放出来,腾出时间和精力去提系统的升整体效率。它能够确保所有自动化流程作业的一致性、可靠性以及稳定性。它让管理员和开发者更紧密的联系在一起,使开发者更容易产出付出设计良好、简洁清晰的代码。puppet 提供了配置管理和数据中心自动化的两个解决方案。这两个解决方案分别是 puppet 开源版 和 puppet 企业版。puppet 开源版以 Apache 2.0 许可证发布,它是一个非常灵活、可定制的解决方案,设置初衷是帮助管理员去完成那些重复性操作工作。pupprt 企业版是一个全平台复杂 IT 环境下的成熟解决方案,它除了拥有开源版本所有优势以外还有移动端 apps、只有商业版才有的加强支持,以及模块化和集成管理等。Puppet 使用 SSL 证书来认证主控服务器与代理节点之间的通信。本教程将要介绍如何在运行 ubuntu 15.04 的主控服务器和代理节点上面安装开源版的 puppet。在这里,我们用一台服务器做主控服务器master,管理和控制剩余的当作 puppet 代理节点agent node的服务器,这些代理节点将依据主控服务器来进行配置。在 ubuntu 15.04 只需要简单的几步就能安装配置好 puppet,用它来管理我们的服务器基础环境非常的方便。(LCTT 译注:puppet 采用 C/S 架构,所以必须有至少有一台作为服务器,其他作为客户端处理)复制代码代码如下:注意,puppet 主控服务器必使用 8140 端口来运行,所以请务必保证开启8140端口。2. 用 NTP 更新时间复制代码代码如下:17 Jun 00:17:08 ntpdate[882]: adjust time server 66.175.209.17 offset -0.001938 sec 复制代码代码如下:复制代码代码如下:下载完成,我们来安装它:复制代码代码如下:使用 apt 包管理命令更新一下本地的软件源:复制代码代码如下:现在我们就可以安装 puppetmaster-passenger 了复制代码代码如下:提示: 在安装的时候可能会报错:复制代码代码如下:现在我们已经安装好了 puppet 主控服务器。因为我们使用的是配合 apache 的 passenger,由 apache 来控制 puppet 主控服务器,当 apache 运行时 puppet 主控服务器才运行。在开始之前,我们需要通过停止 apache 服务来让 puppet 主控服务器停止运行。复制代码代码如下:复制代码代码如下:在新创建的文件里面添加以下内容:复制代码代码如下:这样在以后的系统软件升级中, puppet 主控服务器将不会跟随系统软件一起升级。5. 配置 Puppet 主控服务器复制代码代码如下:现在来配置该证书,在创建 puppet 主控服务器证书时,我们需要包括代理节点与主控服务器沟通所用的每个 DNS 名称。使用文本编辑器来修改服务器的配置文件 puppet.conf:复制代码代码如下:输出的结果像下面这样复制代码代码如下:在这我们需要注释掉 templatedir 这行使它失效。然后在文件的 [main] 小节的结尾添加下面的信息。复制代码代码如下:编辑完成后保存退出。使用下面的命令来生成一个新的证书。复制代码代码如下:至此,证书已经生成。一旦我们看到 Notice: Starting Puppet master version 3.8.1,就表明证书就已经制作好了。我们按下 CTRL-C 回到 shell 命令行。查看新生成证书的信息,可以使用下面的命令。复制代码代码如下:复制代码代码如下:在刚打开的文件里面添加下面这几行:复制代码代码如下:以上这几行的意思是给代理节点部署 apache web 服务。7. 运行 puppet 主控服务复制代码代码如下:我们 puppet 主控服务器已经运行,不过它还不能管理任何代理节点。现在我们给 puppet 主控服务器添加代理节点.提示: 假如报错Job for apache2.service failed. see systemctl status apache2.service and journalctl -xe for details.复制代码代码如下:使用 apt 包管理命令更新一下本地的软件源:复制代码代码如下:通过远程仓库安装:复制代码代码如下:Puppet 代理默认是不启动的。这里我们需要使用文本编辑器修改 /etc/default/puppet 文件,使它正常工作:复制代码代码如下:更改 START 的值改成 yes 。复制代码代码如下:最后保存并退出。9. 使用 Apt 工具锁定代理软件的版本复制代码代码如下:在新建的文件里面加入如下内容复制代码代码如下:这样 puppet 就不会随着系统软件升级而随意升级了。10. 配置 puppet 代理节点复制代码代码如下:它看起来和服务器的配置文件完全一样。同样注释掉 templatedir 这行。不同的是在这里我们需要删除掉所有关于[master] 的部分。假定主控服务器可以通过名字“puppet-master”访问,我们的客户端应该可以和它相互连接通信。假如不行的话,我们需要使用完整的主机域名 puppetmaster.example.com复制代码代码如下:在文件的结尾增加上面3行,增加之后文件内容像下面这样:复制代码代码如下:最后保存并退出。使用下面的命令来启动客户端软件:复制代码代码如下:假如一切顺利的话,我们不会看到命令行有任何输出。 第一次运行的时候,代理节点会生成一个 ssl 证书并且给服务器发送一个请求,经过签名确认后,两台机器就可以互相通信了。提示: 假如这是你添加的第一个代理节点,建议你在添加其他节点前先给这个证书签名。一旦能够通过并正常运行,回过头来再添加其他代理节点。11. 在主控服务器上对证书请求进行签名复制代码代码如下:因为只设置了一台代理节点服务器,所以我们将只看到一个请求。看起来类似如上,代理节点的完整域名即其主机名。注意有没有“+”号在前面,代表这个证书有没有被签名。使用带有主机名的 puppet cert sign 这个命令来签署这个签名请求,如下:复制代码代码如下:主控服务器现在可以通讯和控制它签名过的代理节点了。假如想签署所有的当前请求,可以使用 -all 选项,如下所示:复制代码代码如下:复制代码代码如下:假如我们想查看所有的签署和未签署的请求,使用下面这条命令:复制代码代码如下:复制代码代码如下:这里向我们展示了主清单如何立即影响到了一个单一的服务器。假如我们打算运行的 puppet 清单与主清单没有什么关联,我们可以简单使用 puppet apply 带上相应的清单文件的路径即可。它仅将清单应用到我们运行该清单的代理节点上。复制代码代码如下:复制代码代码如下:添加下面的内容进去复制代码代码如下:这里的配置显示我们将在名为 puppetnode 和 puppetnode1 的2个指定的节点上面安装 apache 服务。这里可以添加其他我们需要安装部署的具体节点进去。15. 配置清单模块复制代码代码如下:警告: 千万不要在一个已经部署 apache 环境的机器上面使用这个模块,否则它将清空你没有被 puppet 管理的 apache 配置。现在用文本编辑器来修改 site.pp :复制代码代码如下:添加下面的内容进去,在 puppetnode 上面安装 apache 服务。复制代码代码如下:保存退出。然后重新运行该清单来为我们的代理节点部署 apache 配置。总结现在我们已经成功的在 ubuntu 15.04 上面部署并运行 puppet 来管理代理节点服务器的基础运行环境。我们学习了 puppet 是如何工作的,编写清单文件,节点与主机间使用 ssl 证书认证的认证过程。使用 puppet 开源软件配置管理工具在众多的代理节点上来控制、管理和配置重复性任务是非常容易的。
