zoukankan      html  css  js  c++  java
  • 【日志追踪】(微服务应用和单体应用)-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
  • 相关阅读:
    安装wamp的方法及过程
    js原生获取className&多选一
    构造函数
    轮播图
    NaN
    ++与--运算练习
    if语句的练习
    switch语句的练习
    九九乘法表
    mac下git提交github代码
  • 原文地址:https://www.cnblogs.com/july-sunny/p/12271627.html
Copyright © 2011-2022 走看看