【日志追踪】(微服务应用和单体应用)-logback中的MDC机制

一.MDC介绍

  MDC(Mapped Diagnostic Contexts映射诊断上下文,该特征logback提供的一种方便在多线程条件下的记录日志的功能,

  某些应用程序采用多线程的方式来处理多个用户的请求。在一个用户的使用过程中,可能有多个不同的线程来进行处理。典型的例子是 Web 应用服务器。当用户访问某个页面时,应用服务器可能会创建一个新的线程来处理该请求,也可能从线程池中复用已有的线程。在一个用户的会话存续期间,可能有多个线程处理过该用户的请求。这使得比较难以区分不同用户所对应的日志。当需要追踪某个用户在系统中的相关日志记录时,就会变得很麻烦。
  MDC正是用于解决上述问题的,MDC 可以看成是一个与当前线程绑定的哈希表,可以往其中添加键值对。MDC 中包含的内容可以被同一线程中执行的代码所访问。当前线程的子线程会继承其父线程中的 MDC 的内容。当需要记录日志时,只需要从 MDC 中获取所需的信息即可。MDC 的内容则由程序在适当的时候保存进去。对于一个 Web 应用来说,通常是在请求被处理的最开始保存这些数据。

  通俗点来说:

    MDC为每一个请求创建一条独立的可识别的的日志,方便追踪和查看,特别是在分布式系统中,分布式日志追踪往往对于问题诊断是特别重要的;

 

二.MDC在单体应用中的案例

  环境:JDK8+Springboot2.x(已经默认集成了logback日志框架)

  过滤器或者拦截器中设置MDC日志,为了尽量每个请求尽量唯一,这里使用UUID作为ID

/**
 * @author: Gabriel
 * @date: 2020/1/28 21:14
 * @description 过滤器
 */
@Slf4j
@Component
@WebFilter(urlPatterns = "/**",filterName = "tlFilter")
public class ThreadLocalFilter implements Filter {

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
      log.info("过滤器初始化");
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        //添加MDC日志
        String logtrackId = UUID.randomUUID(true).toString();
        MDC.put("logTrackId",logtrackId);
        log.info("过滤器执行中");
        try {
            filterChain.doFilter(servletRequest, servletResponse);
        }finally{
            //移除MDC日志
            MDC.remove(logtrackId);
        }
        log.info("过滤器执行完成");
    }

    @Override
    public void destroy() {
        log.info("过滤器销毁");

    }
}

  logback.xml配置文件

  日志输出需要添加  %X{logTrackId} 其中logTrackId为MDC的key值

<?xml version="1.0" encoding="UTF-8"?>

<configuration scan="true" scanPeriod="60 seconds" debug="false">
    <!--定义日志文件的存储地址 勿在 LogBack 的配置中使用相对路径 注意:如果部署在在linux系统中 这里可以文件路径替换为linux文件路径即可-->
    <property name="LOG_HOME" value="src/main/resources/log.properties"/>
    <!--<property name="LOG_HOME" value="/Users/weibinbin/logs/carton"/>-->

    <!-- 控制台输出 -->
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
            <!--格式化输出:%d表示日期,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符-->
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%c-%L][%X{logTrackId}] %msg%n</pattern>
            <charset>UTF-8</charset> <!-- 此处设置字符集 -->
        </encoder>
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>INFO</level>
        </filter>
    </appender>

    <!-- 按照每天生成info日志文件 -->
    <appender name="infoLog" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
            <!--格式化输出:%d表示日期,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符-->
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%c-%L][%X{logTrackId}] %msg%n</pattern>
            <charset>UTF-8</charset> <!-- 此处设置字符集 -->
        </encoder>
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <!--日志文件输出的文件名-->
            <FileNamePattern>${LOG_HOME}/info/info.%d{yyyyMMdd}.%i.log</FileNamePattern>
            <timeBasedFileNamingAndTriggeringPolicy
                    class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                <!-- or whenever the file size reaches 64 MB -->
                <maxFileSize>500 MB</maxFileSize>
            </timeBasedFileNamingAndTriggeringPolicy>
            <!--日志文件保留天数-->
            <maxHistory>365</maxHistory>
        </rollingPolicy>
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>INFO</level>
        </filter>
        <prudent>false</prudent>
    </appender>

    <!-- 按照每天生成error日志文件 -->
    <appender name="errorLog" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
            <!--格式化输出:%d表示日期,%-5level:级别从左显示5个字符宽度 %msg:日志消息,%n是换行符-->
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%c-%L][%X{logTrackId}] %msg%n</pattern>
            <charset>UTF-8</charset> <!-- 此处设置字符集 -->
        </encoder>
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <!--日志文件输出的文件名-->
            <FileNamePattern>${LOG_HOME}/error/error.%d{yyyyMMdd}.%i.log</FileNamePattern>
            <timeBasedFileNamingAndTriggeringPolicy
                    class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                <!-- or whenever the file size reaches 64 MB -->
                <maxFileSize>500 MB</maxFileSize>
            </timeBasedFileNamingAndTriggeringPolicy>
            <!--日志文件保留天数-->
            <maxHistory>365</maxHistory>
        </rollingPolicy>
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>ERROR</level>
            <onMatch>ACCEPT</onMatch>
            <onMismatch>DENY</onMismatch>
        </filter>
        <prudent>false</prudent>
    </appender>

    <!-- 日志输出级别 -->
    <root level="INFO">
        <appender-ref ref="STDOUT"/>
        <appender-ref ref="infoLog"/>
        <appender-ref ref="errorLog"/>
    </root>

</configuration>

  测试 

  

 

三.MDC在分布式系统中的应用

  这里以SpringCloud微服务框架来说明 日志请求链路

  1.网关服务(以Zuul为例)在预处理过滤器中,在请求进行路由之前设置MDC 日志ID,传递到下游的应用服务

/**
 * @Author: Gabriel
 * @Date: 2019/6/30 12:37
 * @Version 1.0
 * @Discription Zuul API网关过滤器
 */
@Slf4j
@Component
public class AccessFilter extends ZuulFilter {

    /**
     *
     过滤器的4中类型
         pre:请求在被路由之前执行
         routing:在路由请求时调用
         post:在routing和errror过滤器之后调用
         error:处理请求时发生错误调用

     */
    @Override
    public String filterType() {
        return "pre";
    }

    /**
     * 过滤器的顺序
     *      数字越小,越被优先被执行
     * @return
     */
    @Override
    public int filterOrder() {
        return 0;
    }

    /**
     * 过滤器是否被执行
     *      返回true / false
     *      实际运用中我们可以利用该方法来指定过滤器的有效范围
     * @return
     */
    @Override
    public boolean shouldFilter() {
        return true;
    }

    /**
     * 过滤器执行的具体逻辑
     *      这里的例子是校验头信息中是否有accessToken 这个信息
     * @return
     * @throws ZuulException
     */
    @Override
    public Object run() throws ZuulException {
        String logTrackId = UUID.randomUUID().toString();

        MDC.put("logTrackId", logTrackId);

        //获取请求上下文对象
        RequestContext ctx = RequestContext.getCurrentContext();
        HttpServletRequest request = ctx.getRequest();

        //添加请求头
        ctx.addZuulRequestHeader("logTrackId", logTrackId);

        //打印日志
        log.info("send {} request to {}", request.getMethod(),request.getRequestURL().toString());
    
//       ...省略相关业务代码,主要演示日志链路
        return null;
    }
}    

  2.应用服务 拦截器中收到网关请求头传递过来的MDC 日志ID,进行PUT到本次请求关联的先成功中,请求执行完成后,销毁该日志ID

/**
 * @author: Gabriel
 * @date: 2020/2/9 1:02
 * @description 拦截器
 */
@Slf4j
@Component
public class URIInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 统一日志标记
        String logTrackId = request.getHeader("logTrackId");
        if (StringUtils.isEmpty(logTrackId)) {
            logTrackId = String.valueOf(System.currentTimeMillis());
        }
        MDC.put("logTrackId", logTrackId);

        request.setCharacterEncoding("UTF-8");
        request.setAttribute("interfaceStartTime", System.currentTimeMillis());

        response.setCharacterEncoding("UTF-8");
        response.setHeader("content-type", "text/html;charset=UTF-8");

        String uri = request.getRequestURI();
        String method = request.getMethod();
        String userAgent = request.getHeader("User-Agent");
        log.info(String.format("##########【%s】,%s,%s", uri, method, userAgent));

        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {

    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        String uri = request.getRequestURI();
        String method = request.getMethod();
        long interfaceStartTime = (Long) request.getAttribute("interfaceStartTime");
        long interfaceEndTime = System.currentTimeMillis();

        long times = interfaceEndTime - interfaceStartTime;
        if (times > 1000) {
            log.info(String.format("==========【%s】,%s,耗时:%s,请检查是否异常", uri, method, times));
        } else {
            log.info(String.format("==========【%s】,%s,耗时:%s", uri, method, times));
        }
        MDC.remove("logTrackId");
    }
}

  拦截器注册

/**
 * @author: Gabriel
 * @date: 2020/2/9 1:08
 * @description 拦截器注册
 */
@Configuration
@Order
public class MyWebMvcConfig implements WebMvcConfigurer {

    @Autowired
    private URIInterceptor uriInterceptor;

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

 

Gabriel
posted @ 2020-02-07 00:00  听风是雨  阅读(3253)  评论(0编辑  收藏  举报
/* 看板娘 */