当前位置: 首页 > 科技观察

基于SpringBoot,日志有韵律如诗

时间:2023-03-16 10:50:49 科技观察

前言在传统系统中,如果能提供日志输出,基本可以满足需求。但是一旦系统拆分成两个或多个系统,再加上负载均衡等,调用环节就变得复杂了。尤其是在向微服务的进一步演进中,如果没有对日志进行合理的规划和链路跟踪,日志的排查将变得异常困难。例如A、B、C系统,调用链路为A->B->C,如果每组服务都是双活的,则调用路径有2种可能。如果系统多了,服务多了,调用链路就会呈指数增长。因此无论是几个简单的内部服务调用,还是复杂的微服务系统,都需要一种机制来实现日志链接跟踪。让您系统的日志输出具有诗歌般的形式之美和和谐的韵律。其实日志追踪的框架有很多现成的,比如Sleuth、Zipkin等组件。但这不是我们要讲的重点。本文重点介绍基于SpringBoot和LogBack手写实现一个简单的日志调用链接跟踪功能。基于这种实现方式,您可以更细粒度地实现它。SpringBoot中Logback的集成SpringBoot本身就内置了日志功能。这里使用logback日志框架对输出结果进行格式化。我们先来看看SpringBoot内置集成了Logback。依赖关系如下。项目引入时:org.springframework.bootspring-boot-starter-webspring-boot-starter-web是间接引入的:org.springframework.bootspring-boot-starterspring-boot-starter引入日志启动器:org.springframework.bootspring-boot-starter-logging其实在logging中引入了需要的logback包:ch.qos.logbacklogback-经典org.apache.logging.log4jlog4j-to-slf4jorg.slf4jjul-to-slf4jparentValue){if(parentValue==null){returnnull;}returnnewHashMap(parentValue);}};publicvoidput(Stringkey,Stringval){if(key==null){thrownewIllegalArgumentException("keycannotbenull");}Mapma??p=inheritableThreadLocal.get();if(map==null){map=newHashMap();inheritableThreadLocal.set(map);}map.put(key,val);}//...}从源码可以看出,内部持有一个InheritableThreadLocal的实例,上下文数据通过HashMap保存在这个实例中。此外,MDC还提供了put/get/clear等几个核心接口,用于操作存储在ThreadLocal中的数据。在logback.xml中,可以通过在布局中声明“%X{requestId}”来获取MDC中存储的数据,并可以打印这些信息。基于MDC的这些特点,常用于日志链接跟踪、动态配置自定义信息(如requestId、sessionId等)等场景。上面的实际使用已经了解了一些基本的原理知识,下面我们来看看如何基于日志框架的MDC功能实现日志跟踪。准备工具类首先要定义一些工具类。这里强烈建议大家以工具类的形式实现一些操作。这是编写优雅代码的一部分,也避免了在以后的修改中什么都改。TraceID的生成类(我们定义参数名称为requestId),这里是通过UUID生成的。当然也可以根据你的场景和需要,通过其他方式生成。publicclassTraceIdUtils{/***根据UUID生成traceId**@returnTraceId*/publicstaticStringgetTraceId(){returnUUID.randomUUID().toString().replace("-","");}}Context内容TraceIdContext的操作工具类:publicclassTraceIdContext{publicstaticfinalStringTRACE_ID_KEY="requestId";publicstaticvoidsetTraceId(StringtraceId){if(StringLocalUtil.isNotEmpty(traceId)){MDC.put(TRACE_ID_KEY,traceId);}}publicstaticStringgetTraceId(){StringtraceId=MDC.get(TRAYtrace_ID)=null?"":traceId;}publicstaticvoidremoveTraceId(){MDC.remove(TRACE_ID_KEY);}publicstaticvoidclearTraceId(){MDC.clear();}}通过工具类,方便统一使用所有服务。比如requestId可以统一定义,避免到处都不一样。这里不仅提供了set方法,还提供了remove和cleaning的方法。需要注意的是MDC.clear()方法的使用。如果所有的线程都是通过newThread方法创建的,那么线程死了之后,存储的数据也就死了,这没什么。但是如果使用线程池,线程是可以复用的。如果不清除上一个线程的MDC内容,再次从线程池中获取线程,会把之前的数据(脏数据)取出来,会导致一些意想不到的错误,所以必须在当前之后清除线程结束。过滤器拦截既然我们要跟踪日志链接,最直观的想法就是在访问的源头生成一个请求ID,然后一路传递,直到请求完成。这里以Http为例,通过Filter拦截请求,通过HttpHeader存储和传输数据。系统间调用时,调用方将requestId设置到Header中,被叫方从Header中取即可。Filter的定义:publicclassTraceIdRequestLoggingFilterextendsAbstractRequestLoggingFilter{@OverrideprotectedvoidbeforeRequest(HttpServletRequestrequest,Stringmessage){StringrequestId=request.getHeader(TraceIdContext.TRACE_ID_KEY);if(StringLocalUtil.isNotEmpty(requestId)){TraceIdContext.setTraceId(requestId);}else{TraceIdContext.setTraceId(TraceIdUtils.getTraceId());}}@OverrideprotectedvoidafterRequest(HttpServletRequestrequest,Stringmessage){TraceIdContext.removeTraceId();}}在beforeRequest方法中,从Header中获取requestId,如果没有则视为“源”,并且将生成一个requestId。将其设置为MDC。当请求完成后,将设置的requestId移除,防止上面提到的线程池问题。系统中的每一个服务都可以通过上面的方式来实现,整个请求链就打通了。当然上面定义的Filter是需要初始化的。SpringBoot中的实例化方法如下:@ConfigurationpublicclassTraceIdConfig{@BeanpublicTraceIdRequestLoggingFiltertraceIdRequestLoggingFilter(){returnnewTraceIdRequestLoggingFilter();}}对于普通的系统调用,上面的方法基本够用了。实践中可以根据自己的需要在此基础上进行扩展。这里使用了Filter,也可以通过拦截器、Spring的AOP等实现。微服务中的Feign如果你的系统是基于SpringCloud中的Feign组件调用的,你可以通过实现RequestInterceptor拦截器来达到添加requestId的效果。具体实现如下:@ConfigurationpublicclassFeignConfigimplementsRequestInterceptor{@Overridepublicvoidapply(RequestTemplaterequestTemplate){requestTemplate.header(TraceIdContext.TRACE_ID_KEY,TraceIdContext.getTraceId());}}以上操作完成后,向Controller发出请求会打印如下日志:2021-04-1310:58:31.092cloud-service-consumer-demo[http-nio-7199-exec-1]INFO[ef76526ca96242bc8e646cdef3ab31e6]c.b.demo.controller.CityController-getCity2021-04-1310:58:31.18evice5-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的请求拦截器等,如果你有兴趣的话,他们每个人都可以发散,学习更多的知识点。