Spring Boot 实现日志链路追踪,无需引入组件vs引入sleuth实现,让日志定位更方便!

本文介绍了如何在SpringBoot应用中使用无需引入额外组件的方式实现日志链路追踪,以及与SpringCloudSleuth的Sleuth组件进行比较,突出了两者在功能实现和便捷性上的差异。

Spring Boot 实现日志链路追踪,无需引入组件vs引入sleuth实现,让日志定位更方便!

前言

在生产环境中,由于节点众多日志打印超级快基本上无法得到有效的日志,靠搜索关键字
查日志能解决一些,但是不能完全精准的拿到想要的,也不能完全呈现整个链路相关的日
志。
那么为了解决这个问题,需要给同一次业务调用链上的日志串起来。

实现后的效果图

在这里插入图片描述

正文

无需引入组件方式实现

  1. 首选先创建一个登录拦截器 在这里插入图片描述

  2. 引入pom依赖 这里我使用的是slf4j

<dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>log4j-over-slf4j</artifactId>
</dependency>

  1. 整合log4j2.xml,打印日志
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN">
    <Properties>
        <Property name="PID">????</Property>
        <Property name="LOG_EXCEPTION_CONVERSION_WORD">%throwable</Property>
        <Property name="LOG_LEVEL_PATTERN">%5p</Property>
        <Property name="LOG_DATEFORMAT_PATTERN">yyyy-MM-dd HH:mm:ss.SSS</Property>
        <property name="APP_NAME">fb-budget-service</property>
        <Property name="LOG_SLEUTH_PATTERN">[${APP_NAME},[%traceId],%X{X-B3-TraceId},%X{X-B3-SpanId}]</Property>
        <Property name="CONSOLE_LOG_PATTERN">%clr{%d{${LOG_DATEFORMAT_PATTERN}}}{faint}%clr{${LOG_LEVEL_PATTERN}} ${LOG_SLEUTH_PATTERN} %clr{${sys:PID}}{magenta} %clr{---}{faint} %clr{[%15.15t]}{faint} %clr{%-40.40c{1.}}{cyan} %clr{:}{faint} %m%n${sys:LOG_EXCEPTION_CONVERSION_WORD}</Property>
        <Property name="FILE_LOG_PATTERN">%d{${LOG_DATEFORMAT_PATTERN}} ${LOG_LEVEL_PATTERN} ${LOG_SLEUTH_PATTERN} ${sys:PID} --- [%t] %-40.40c{1.} : %m%n${sys:LOG_EXCEPTION_CONVERSION_WORD}</Property>
    </Properties>
    <Appenders>
        <Console name="Console" target="SYSTEM_OUT" follow="true">
            <PatternLayout pattern="${sys:CONSOLE_LOG_PATTERN}" />
        </Console>
        <!--<Console name="Console" target="SYSTEM_OUT">
            <PatternLayout pattern="%X{X-B3-TraceId} %d{yyyy-MM-dd HH:mm:ss.SSS} %5p -&#45;&#45;[%15.15t] %-40.40logger{39} : %m%n"/>
        </Console>-->
    </Appenders>
    <Loggers>
        <Logger name="org.apache.catalina.startup.DigesterFactory" level="error" />
        <Logger name="org.apache.catalina.util.LifecycleBase" level="error" />
        <Logger name="org.apache.coyote.http11.Http11NioProtocol" level="warn" />
        <logger name="org.apache.sshd.common.util.SecurityUtils" level="warn"/>
        <Logger name="org.apache.tomcat.util.net.NioSelectorPool" level="warn" />
        <Logger name="org.eclipse.jetty.util.component.AbstractLifeCycle" level="error" />
        <Logger name="org.hibernate.validator.internal.util.Version" level="warn" />
        <logger name="org.springframework.boot.actuate.endpoint.jmx" level="warn"/>
        <Root level="info">
            <AppenderRef ref="Console" />
        </Root>
    </Loggers>
</Configuration>

  1. 自定义日志拦截器 LogInterceptor.java中增加 traceid

前端每次请求都会传的一个 32 位的随机 id 在请求头中和 tokenid 一样,在
LoginInterceptor 里 preHandle 里放到 MDC 里

@Slf4j
@Component
public class LoginInterceptor implements HandlerInterceptor {


    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object o) throws Exception {
        String traceId = request.getHeader("traceId");
        MDC.put("traceId",traceId);
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception {

    }

    @Override
    public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception {
    }
}

  1. MDC

MDC (Mapped Diagnostic Context,映射调试上下文)是 log4j logback 及 log4j2 提供的
一种方便在多线程条件下记录日志的功能。MDC 可以看成是一个与当前线程绑定的哈希
表,内部是基于 threadLoca 实现,可以往其中添加键值对,MDC 中包合的内容可以被同一
执行的代码所访问。当开启 isThreadContextMapinheritable 属性后,当前线程的子线程
会继承其父线程中的 MDC 的内容。当需要记录日志时,日志框架会自动从 MDC 中获取所
需的信息,MDC 的内容需要由程序在适当的时候保存进去

  1. WebMvcConfigurerAdapter.java 添加拦截器
@Configuration
public class FiscalMvcConfig extends WebMvcConfigurerAdapter {

    @Autowired
    private LoginInterceptor loginInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(loginInterceptor).
                addPathPatterns("/**")
        ;
    }

}

到这里其实已经完成了,但是还有一个问题,在开启新的线程后MDC在新的线程中获取不到traceId,所以我们需要针对子线程使用情形,做调整,思路:将父线程的trackId传递下去给子线程即可。

  1. ThreadPoolConfig.java 定义线程池,交给spring管理

/**
 * Description 继承ThreadPoolExecutor,多线程处理任务时传递日志trace_id
 */
public class ThreadPoolExecutorMdcUtil extends ThreadPoolExecutor {

    public ThreadPoolExecutorMdcUtil(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) {
        super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
    }

    public ThreadPoolExecutorMdcUtil(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory) {
        super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory);
    }

    public ThreadPoolExecutorMdcUtil(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, RejectedExecutionHandler handler) {
        super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, handler);
    }

    public ThreadPoolExecutorMdcUtil(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) {
        super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, handler);
    }

    @Override
    public void execute(Runnable task) {
        super.execute(wrap(task));
    }

    @Override
    public <T> Future<T> submit(Callable<T> task) {
        return super.submit(wrap(task));
    }

    @Override
    public Future<?> submit(Runnable task) {
        return super.submit(wrap(task));
    }

    private <T> Callable<T> wrap(final Callable<T> callable) {
        Map<String, String> context = MDC.getCopyOfContextMap();

        return () -> {
            if (context != null) {
                MDC.setContextMap(context);
            }
            try {
                return callable.call();
            } finally {
                MDC.clear();
            }
        };
    }

    private Runnable wrap(final Runnable runnable) {
        Map<String, String> context = MDC.getCopyOfContextMap();

        return () -> {
            if (context != null) {
                MDC.setContextMap(context);
            }

            try {
                runnable.run();
            } finally {
                MDC.clear();
            }
        };
    }
}

创建线程池时使用ThreadPoolExecutorMdcUtil创建即可,会把父线程的MDC的上下文传递给子线程,这样就可以实现多线程的日志链路追踪了。

当然也可以手动的传递例如

ThreadUtil.execute(() -> {
    try {
        MDC.put(ConstantConfig.TRACE_ID, traceID);
    } catch (Exception e){
        log.error("getFormerData2",e);
    } finally {
        downLatch.countDown();
    }
});

引入sleuth实现

  1. 引入pom依赖
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-sleuth</artifactId>
    <version>1.3.6.RELEASE</version>
</dependency>
  1. 整合log4j2.xml,打印日志
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN">
    <Properties>
        <Property name="PID">????</Property>
        <Property name="LOG_EXCEPTION_CONVERSION_WORD">%throwable</Property>
        <Property name="LOG_LEVEL_PATTERN">%5p</Property>
        <Property name="LOG_DATEFORMAT_PATTERN">yyyy-MM-dd HH:mm:ss.SSS</Property>
        <property name="APP_NAME">fb-budget-service</property>
        <Property name="LOG_SLEUTH_PATTERN">[${APP_NAME},[%traceId],%X{X-B3-TraceId},%X{X-B3-SpanId}]</Property>
        <Property name="CONSOLE_LOG_PATTERN">%clr{%d{${LOG_DATEFORMAT_PATTERN}}}{faint}%clr{${LOG_LEVEL_PATTERN}} ${LOG_SLEUTH_PATTERN} %clr{${sys:PID}}{magenta} %clr{---}{faint} %clr{[%15.15t]}{faint} %clr{%-40.40c{1.}}{cyan} %clr{:}{faint} %m%n${sys:LOG_EXCEPTION_CONVERSION_WORD}</Property>
        <Property name="FILE_LOG_PATTERN">%d{${LOG_DATEFORMAT_PATTERN}} ${LOG_LEVEL_PATTERN} ${LOG_SLEUTH_PATTERN} ${sys:PID} --- [%t] %-40.40c{1.} : %m%n${sys:LOG_EXCEPTION_CONVERSION_WORD}</Property>
    </Properties>
    <Appenders>
        <Console name="Console" target="SYSTEM_OUT" follow="true">
            <PatternLayout pattern="${sys:CONSOLE_LOG_PATTERN}" />
        </Console>
        <!--<Console name="Console" target="SYSTEM_OUT">
            <PatternLayout pattern="%X{X-B3-TraceId} %d{yyyy-MM-dd HH:mm:ss.SSS} %5p -&#45;&#45;[%15.15t] %-40.40logger{39} : %m%n"/>
        </Console>-->
    </Appenders>
    <Loggers>
        <Logger name="org.apache.catalina.startup.DigesterFactory" level="error" />
        <Logger name="org.apache.catalina.util.LifecycleBase" level="error" />
        <Logger name="org.apache.coyote.http11.Http11NioProtocol" level="warn" />
        <logger name="org.apache.sshd.common.util.SecurityUtils" level="warn"/>
        <Logger name="org.apache.tomcat.util.net.NioSelectorPool" level="warn" />
        <Logger name="org.eclipse.jetty.util.component.AbstractLifeCycle" level="error" />
        <Logger name="org.hibernate.validator.internal.util.Version" level="warn" />
        <logger name="org.springframework.boot.actuate.endpoint.jmx" level="warn"/>
        <Root level="info">
            <AppenderRef ref="Console" />
        </Root>
    </Loggers>
</Configuration>

  1. 这里需要注意sleuth封装的key名称是:X-B3-TraceId
  2. 这里需要注意sleuth封装的value改成: 只能包含这些字符{ 0123456789abcdef }也就是16进制,不能有大写字母
  3. 如果前端没有传X-B3-TraceId参数会自动生成一个
  4. 这里添加一个切面统一把X-B3-TraceId添加到接口返回值当中
@RestControllerAdvice
public class ResponseBodyFilter implements ResponseBodyAdvice<Object> {

    @Override
    public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
        return true;
    }

    @Override
    public Object beforeBodyWrite(Object o, MethodParameter methodParameter, MediaType mediaType, Class<? extends HttpMessageConverter<?>> aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {
        if (o instanceof Map) {
            ((Map<Object, Object>) o).putIfAbsent("env", System.getenv("HOSTNAME"));
            ((Map<Object, Object>) o).putIfAbsent(Span.TRACE_ID_NAME, BudgetUtils.getTraceId());
        }
        return o;
    }


}

到这里也就实现了,总体对比下来发现引入sleuth实现更加方便快捷

评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值