MDC实现日志打印请求唯一标识

在每次请求中打出的每条日志中添加统一的请求唯一标识。通过搜索日志唯一标识,这样就可以非常高效精准排查问题。

例如:

2018-12-21 10:21:26.329 [http-nio-8080-exec-2] [my-server-logtag] [] INFO  org.springframework.web.servlet.DispatcherServlet - FrameworkServlet 'dispatcherServlet': initialization completed in 18 ms 
2018-12-21 10:21:26.363 [http-nio-8080-exec-2] [my-server-logtag] [aed35d43-3fa2-46e1-bb72-ce9f50c3fe4d] ERROR com.code3.modules.controller.MyController - 日志1 
2018-12-21 10:21:26.367 [http-nio-8080-exec-2] [my-server-logtag] [aed35d43-3fa2-46e1-bb72-ce9f50c3fe4d] ERROR com.code3.advice.GlobalExceptionHandler - 系统异常,{} 
com.code3.base.BusinessException: 异常信息1
    at com.code3.modules.controller.MyController.logtag(MyController.java:18)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at 

通过日志标识:aed35d43-3fa2-46e1-bb72-ce9f50c3fe4d,就可以精准定位

一、简单的解决思路

想要跟踪请求,第一个想到的就是当请求来时生成一个traceId放在ThreadLocal里,然后打印时去取就行了。但在不改动原有输出语句的前提下自然需要日志框架的支持了,搜索的一番发现主流日志框架都提供了MDC功能。

MDC 介绍

MDC(Mapped Diagnostic Context,映射调试上下文)是 log4j 和 logback 提供的一种方便在多线程条件下记录日志的功能。

MDC 可以看成是一个与当前线程绑定的Map,可以往其中添加键值对。MDC 中包含的内容可以被同一线程中执行的代码所访问。当前线程的子线程会继承其父线程中的 MDC 的内容。当需要记录日志时,只需要从 MDC 中获取所需的信息即可。

MDC 的内容则由程序在适当的时候保存进去。对于一个 Web 应用来说,通常是在请求被处理的最开始保存这些数据。

二、拦截器实现唯一标识

1、自定义日志标识生成拦截器

LogInterceptor.java

package com.interceptors.logtag.interceptor;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;

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

/**
 * <p>日志拦截器</p>
 *
 * @author zetting
 * @date 2018/8/27 11:18
 */
@Component
public class LogInterceptor extends HandlerInterceptorAdapter {
    private Logger log = LoggerFactory.getLogger(this.getClass());
    /**
     * 日志跟踪标识
     */
    private static final String TRACE_ID = "TRACE_ID";

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        String traceId = UUID.randomUUID().toString();
        if (StringUtils.isEmpty(MDC.get(TRACE_ID))) {
            MDC.put(TRACE_ID, traceId);
        }
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) {
        MDC.remove(TRACE_ID);
    }
}

2、使用日志标识生成拦截器

  1. 在WebMvc配置加上LogInterceptor
package com.interceptors.logtag.configuration;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import com.ruoyi.framework.interceptor.LogInterceptor;

/**
 * MVC配置
 *
 * @author zet
 * @date 2018-12-21 22:30
 */
@Configuration
public class WebMvcConfiguration implements WebMvcConfigurer {
    @Autowired
    private LogInterceptor logInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(logInterceptor);
    }
}
  1. 在logback文件加上[%X{TRACE_ID}]
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <jmxConfigurator/>

    <property name="app-name" value="my-server" />
    <property name="filename" value="server" />
    <property name="module-name" value="logtag" />

    <appender name="consoleAppender" class="ch.qos.logback.core.ConsoleAppender">
        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
            <!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符-->
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [${app-name}-${module-name}] [%X{TRACE_ID}] %highlight(%-5level) %cyan(%logger{50}) - %highlight(%msg) %n</pattern>
        </encoder>
    </appender>

    <appender name="fileAppender" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>/data/log-center/${app-name}/${module-name}/${filename}.log</file>
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>/data/log-center/${app-name}/${module-name}/${filename}.%d{yyyy-MM-dd}.log</fileNamePattern>
            <maxHistory>30</maxHistory>
        </rollingPolicy>
        <encoder>
            <pattern>[%date{ISO8601}] [%level] %logger{80} [%thread] [${app-name}-${module-name}][%X{TRACE_ID}] ${app-name}-${module-name} ==> %msg%n</pattern>
        </encoder>
    </appender>

    <root level="INFO" additivity="false">
        <appender-ref ref="fileAppender"/>
        <appender-ref ref="consoleAppender"/>
    </root>
</configuration>

3、演示

img

打出的日志标识.jpg

4、附

gitee源码:https://gitee.com/zetting/my-gather/tree/master/springboot-interceptors-logtag

原创:https://www.jianshu.com/p/b74ade8bbb57

三、ServletComponentScan注解实现唯一标识

SpringBootApplication使用@ServletComponentScan注解作用

  • Servlet可以直接通过@WebServlet注解自动注册
  • Filter可以直接通过@WebFilter注解自动注册
  • Listener可以直接通过@WebListener 注解自动注册

1、自定义LogbackFilter

import java.io.IOException;
import java.util.UUID;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.annotation.WebFilter;

import org.slf4j.MDC;

@WebFilter(filterName = "logbackFilter", urlPatterns = "/*")
public class LogbackFilter implements Filter {

	public void init(FilterConfig filterConfig) throws ServletException {
	}

	@Override
	public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
			throws IOException, ServletException {
		UUID uuid = UUID.randomUUID();
		MDC.put("traceid", uuid.toString().replace("-", ""));
		try {
			filterChain.doFilter(servletRequest, servletResponse);
		} finally {
			MDC.remove("traceid");// 保证一次请求一个唯一标识
		}
	}

	@Override
	public void destroy() {
	}
}

2、logback.xml配置

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
   	<!-- 上下文名称 -->
	<contextName>ruoyi-admin</contextName>
    <!--日志输出格式:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符-->
	<property name="log.pattern" value="%-5level [%contextName] %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %logger - traceId:%X{traceid} - %msg%n" />
   	<include resource="org/springframework/boot/logging/logback/defaults.xml" />

   	<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
      	<encoder>
         	<pattern>${log.pattern}</pattern>
         	<charset>utf8</charset>
      	</encoder>
   	</appender>

   <root level="INFO">
      <appender-ref ref="CONSOLE" />
   </root>
</configuration>

3、演示

DEBUG [ruoyi-admin] 2020-04-14 14:34:02.402 [http-nio-80-exec-2] com.ruoyi.system.mapper.SysConfigMapper.selectConfigList_COUNT - traceId:a8309218ae64434d856f922a1abac3b6 - ==>  Preparing: SELECT count(0) FROM sys_config 
DEBUG [ruoyi-admin] 2020-04-14 14:34:02.404 [http-nio-80-exec-2] com.ruoyi.system.mapper.SysConfigMapper.selectConfigList_COUNT - traceId:a8309218ae64434d856f922a1abac3b6 - ==> Parameters: 
DEBUG [ruoyi-admin] 2020-04-14 14:34:02.440 [http-nio-80-exec-2] com.ruoyi.system.mapper.SysConfigMapper.selectConfigList_COUNT - traceId:a8309218ae64434d856f922a1abac3b6 - <==      Total: 1
DEBUG [ruoyi-admin] 2020-04-14 14:34:02.444 [http-nio-80-exec-2] com.ruoyi.system.mapper.SysConfigMapper.selectConfigList - traceId:a8309218ae64434d856f922a1abac3b6 - ==>  Preparing: SELECT config_id, config_name, config_key, config_value, config_type, create_by, create_time, update_by, update_time, remark FROM sys_config order by create_time desc LIMIT ? 
DEBUG [ruoyi-admin] 2020-04-14 14:34:02.445 [http-nio-80-exec-2] com.ruoyi.system.mapper.SysConfigMapper.selectConfigList - traceId:a8309218ae64434d856f922a1abac3b6 - ==> Parameters: 10(Integer)
DEBUG [ruoyi-admin] 2020-04-14 14:34:02.479 [http-nio-80-exec-2] com.ruoyi.system.mapper.SysConfigMapper.selectConfigList - traceId:a8309218ae64434d856f922a1abac3b6 - <==      Total: 4

4、tips

  1. 自定义 LogbackFilter 需要实现 Filter

    • springboot方式需要在 LogbackFilter 类加上@WebFilter(filterName = “logbackFilter”, urlPatterns = “/*”)注解
    • 启动类 ApplicationBootstrap 加上@ServletComponentScan注解
  2. %cyan(%X{wtraceid}) 自定义traceid的颜色

    日志颜色设置参考:

    https://www.cnblogs.com/sxdcgaq8080/p/7885340.html

    https://blog.csdn.net/qq_34313827/article/details/80394325

    注:这个部署线上环境时候切记要注释掉!

原创:https://www.cnblogs.com/xiao-xin-xin/p/10729280.html

封装参考:https://blog.csdn.net/hanchao5272/article/details/92107651

四、线程池实现唯一标识

前两种实现的问题:

  • 线程池中的线程会打印错误的traceId
  • 调用下游服务后会生成新的traceId,无法继续跟踪

MDC使用的InheritableThreadLocal只是在线程被创建时继承,但是线程池中的线程是复用的,后续请求使用已有的线程将打印出之前请求的traceId。这时候就需要对线程池进行一定的包装,在线程在执行时读取之前保存的MDC内容。不仅自身业务会用到线程池,spring项目也使用到了很多线程池,比如@Async异步调用,zookeeper线程池、kafka线程池等。不管是哪种线程池都大都支持传入指定的线程池实现。

详细参考:

https://blog.csdn.net/yangcheng33/article/details/80796129

https://github.com/TiantianUpup/springboot-log/tree/master/springboot-trace

https://www.jianshu.com/p/ae855779f30b

五、手写分布式链路追踪框架

分布式环境下,由于系统不再是单点,一个业务可能会有多个系统参与执行。这就涉及到请求在多个系统之间来回转发,如果没有一个全局的标识能够追踪单个业务调用,那么就会在发生业务故障时盲目查日志,造成问题定位困难。
如果我们能在系统间透明的传递一个全局唯一ID,将请求在分布式系统中的流转路径聚合并在使用中保存和传递该id,最后通过页面进行可视化的展示,让开发、运维等人员能够轻松的发现系统故障,那么对问题的快速定位,系统的快速恢复有着非凡的意义。

实现目标

  1. 基于HTTP同步调用,能实现TraceId的传递,SpanId的生成及传递,ParentSpanId的获取
  2. 应用层无感知,业务请求无需显示传递链路信息
  3. 支持Spring框架

本文我们就根据谷歌的dapper论文的模型,自己开发一个最简的分布式链路追踪框架。工程上传至github,可以自行下载源码研究。

详细参考:自己写分布式链路追踪框架1–TraceId/SpanId/ParentSpanId生成及传递

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值