MDC实现日志跟踪

使用MDC标注日志上下文

背景

最近在研究ELK,想通过ELK来统一管理日志,并简单分析系统的一些功能,比如:机构下的交易量,交易成功/失败的比例,单位时间内某种交易的笔数,访问系统前50IP……,但是苦于无法建立统一的分析标准,无法实施,想法是把一些业务参数打印到日志中,进行分析统计。

简介

  MDC(Mapped Diagnostic Context,映射调试上下文)是 log4j 和 logback 提供的一种方便在多线程条件下记录日志的功能。某些应用程序采用多线程的方式来处理多个用户的请求。在一个用户的使用过程中,可能有多个不同的线程来进行处理。典型的例子是 Web 应用服务器。当用户访问某个页面时,应用服务器可能会创建一个新的线程来处理该请求,也可能从线程池中复用已有的线程。在一个用户的会话存续期间,可能有多个线程处理过该用户的请求。这使得比较难以区分不同用户所对应的日志。当需要追踪某个用户在系统中的相关日志记录时,就会变得很麻烦。

  一种解决的办法是采用自定义的日志格式,把用户的信息采用某种方式编码在日志记录中。这种方式的问题在于要求在每个使用日志记录器的类中,都可以访问到用户相关的信息。这样才可能在记录日志时使用。这样的条件通常是比较难以满足的。MDC 的作用是解决这个问题。

  MDC 可以看成是一个与当前线程绑定的哈希表,可以往其中添加键值对。MDC 中包含的内容可以被同一线程中执行的代码所访问。当前线程的子线程会继承其父线程中的 MDC 的内容。当需要记录日志时,只需要从 MDC 中获取所需的信息即可。MDC 的内容则由程序在适当的时候保存进去。对于一个 Web 应用来说,通常是在请求被处理的最开始保存这些数据。

好处

  • 如果你的系统已经上线,突然有一天老板说我们增加一些用户数据到日志里分析一下。如果没有MDC我猜此时此刻你应该处于雪崩状态。MDC恰到好处的让你能够实现在日志上突如其来的一些需求
  • 如果你是个代码洁癖,封装了公司LOG的操作,并且将处理线程跟踪日志号也封装了进去,但只有使用了你封装日志工具的部分才能打印跟踪日志号,其他部分(比如hibernate、mybatis、httpclient等等)日志都不会体现跟踪号。当然我们可以通过linux命令来绕过这些困扰。
  • 使代码简洁、日志风格统一

使用

  • spirng: import org.slf4j.MDC;
  • springboot: import org.apache.log4j.MDC;

实现

  • 过滤器(Filter)

  • 遇到问题:如何在过滤器中读取controller的返回值(因为直接Response没有提供直接拿到返回值的方法。所以要通过代理来取得返回值)

@WebFilter(urlPatterns = "/*")
@Order(1)
public class MyFilter implements Filter {

    private static Logger logger = LoggerFactory.getLogger(MyFilter.class);

    private static ObjectMapper mapper = new ObjectMapper();

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {

    }

    @SuppressWarnings("unchecked")
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {

        HttpServletRequest req = (HttpServletRequest) request;

        String body = IOUtils.toString(request.getInputStream());
        Map<String, Object> reqMap = mapper.readValue(body, Map.class);

        // 可以在此处解析请求报文,然后将业务参数放入日志打印
        MDC.put("service_name", "my_service1");
        MDC.put("trcode", "123456");
        MDC.put("chid", "999999");

        ResponseWrapper wrapperResponse = new ResponseWrapper((HttpServletResponse) response);// 转换成代理类

        ParameterRequestWrapper wrapRequest = new ParameterRequestWrapper(req, reqMap);
        chain.doFilter(wrapRequest, wrapperResponse);

        byte[] content = wrapperResponse.getContent();// 获取返回值
        String str = new String(content, "UTF-8");

        Map<String, Object> rspMap = JSONUtil.jsonToObj(str, Map.class);
        Map<String, String> rspHeadMap = (Map<String, String>) rspMap.get("rspHead");
        MDC.put("rspcode", rspHeadMap.get("rspcode"));

        MDC.clear();// 清除数据
    }

    /*
     * (non-Javadoc)
     * 
     * @see javax.servlet.Filter#destroy()
     */
    @Override
    public void destroy() {

    }

}
/**
 * 返回值输出代理类
 */
public class ResponseWrapper extends HttpServletResponseWrapper {

    private ByteArrayOutputStream buffer;

    private ServletOutputStream out;

    public ResponseWrapper(HttpServletResponse httpServletResponse) {
        super(httpServletResponse);
        buffer = new ByteArrayOutputStream();
        out = new WrapperOutputStream(buffer);
    }

    @Override
    public ServletOutputStream getOutputStream() throws IOException {
        return out;
    }

    @Override
    public void flushBuffer() throws IOException {
        if (out != null) {
            out.flush();
        }
    }

    public byte[] getContent() throws IOException {
        flushBuffer();
        return buffer.toByteArray();
    }

    class WrapperOutputStream extends ServletOutputStream {
        private ByteArrayOutputStream bos;

        public WrapperOutputStream(ByteArrayOutputStream bos) {
            this.bos = bos;
        }

        @Override
        public void write(int b) throws IOException {
            bos.write(b);
        }

        @Override
        public boolean isReady() {

            return false;
        }

        @Override
        public void setWriteListener(WriteListener arg0) {

        }
    }

}
  • 拦截器(暂时未实现获取controller中的返回值)
/**
 * 拦截器
 * 
 * @author Fan.W
 * @since 1.8
 */
public class MDCInterceptor implements HandlerInterceptor {

    private static final Logger logger = LoggerFactory.getLogger(MDCInterceptor.class);

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
            throws Exception {

        MDC.put("user_name", "fan wei");
        MDC.put("user_id", "123456");
        return true;
    }

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

    }

    /**
    *这个方法的主要作用是用于清理资源的,当然这个方法也只能在当前这个Interceptor的preHandle方法的返回值为true时才会执行。
    */
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
            throws Exception {
        MDC.clear();// 清除
    }

}
  • 约束及注册
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:tx="http://www.springframework.org/schema/tx"
    xmlns:mvc="http://www.springframework.org/schema/mvc" xmlns:context="http://www.springframework.org/schema/context"
    xmlns:aop="http://www.springframework.org/schema/aop"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans 
        http://www.springframework.org/schema/beans/spring-beans.xsd 
        http://www.springframework.org/schema/tx 
        http://www.springframework.org/schema/tx/spring-tx.xsd 
        http://www.springframework.org/schema/mvc
         http://www.springframework.org/schema/mvc/spring-mvc.xsd
        http://www.springframework.org/schema/context 
        http://www.springframework.org/schema/context/spring-context.xsd
        http://www.springframework.org/schema/aop
        http://www.springframework.org/schema/aop/spring-aop.xsd"
    default-lazy-init="false">

    <!--拦截器 -->  
    <mvc:interceptors> 
        <bean class="com.seeker.interceptor.MDCInterceptor"/> 
    </mvc:interceptors>  

</beans>
  • spring aop实现(建议)

异常通知要先于controller中ExceptionHandler捕获异常!!!!!

@Aspect
@Component
public class MyInterceptor {

    private static Logger logger = LoggerFactory.getLogger(MyInterceptor.class);

    /**
     * 拦截类的入口--拦截所有controller类
     */
    @Pointcut("execution(public * com.seeker.controller..*.*(..)) ")
    public void pointCut() {
    }

    /**
     * 方法调用之前调用
     * 
     * @param joinPoint
     */
    @Before(value = "pointCut()")
    public void doBefore(JoinPoint joinPoint) {

    }

    /**
     * 环绕通知
     * 
     * @param pjp
     * @return
     * @throws Throwable
     */
    @SuppressWarnings({ "rawtypes", "unchecked" })
    @Around("pointCut()")
    public Object doBasicProfiling(ProceedingJoinPoint pjp) throws Throwable {

        String className = pjp.getTarget().getClass().getName(); // 拦截类
        String methodName = pjp.getSignature().getName() + "()";
        Object[] args = pjp.getArgs();// 获取请求参数,可以校验属性

        // 解析请求报文,使用MDC,打印日志,也可在前置通知中获取
        MDC.put("service_name", "my_service1");
        MDC.put("trcode", "123456");
        MDC.put("chid", "999999");

        // 此处返回的是拦截的方法的返回值,如果不执行此方法,则不会执行被拦截的方法,利用环绕通知可以很好的做权限管理
        Object obj = pjp.proceed();

        MDC.put("rspcode", "0000");
        return obj;
    }

    /**
     * 异常通知:pjp.proceed();跑出异常即捕获,先于@ExceptionHandler中捕获到
     * 
     * @param joinPoint
     * @param e
     */
    @AfterThrowing(pointcut = "pointCut()", throwing = "e")
    public void doAfterThrowing(JoinPoint joinPoint, Throwable e) {
        if (e instanceof BizException) {
            MDC.put("rspcode", ((BizException) e).messageCode);
        } else {
            MDC.put("rspcode", "9999");
        }
    }
}
  • 约束及开启aop
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:mvc="http://www.springframework.org/schema/mvc"
    xmlns:aop="http://www.springframework.org/schema/aop" 
    xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd  
        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
        http://www.springframework.org/schema/aop 
                        http://www.springframework.org/schema/aop/spring-aop.xsd">

        <!--通知spring使用cglib而不是jdk的来生成代理方法 AOP可以拦截到Controller  -->
    <aop:aspectj-autoproxy expose-proxy="true"></aop:aspectj-autoproxy>

</beans>
  • logback.xml配置

 <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder"> 
    <pattern>[%X{service_name}] %d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%thread] [%X{chid}] [%X{trcode}] [%X{rspcode}] - %m%n</pattern>   
</encoder> 

猜你喜欢

转载自blog.csdn.net/dwyane__wade/article/details/79978747