通过slf4j/log4j的MDC/NDC 实现日志追踪

首先介绍下NDC和MDC的区别:

NDC和MDC

NDC(Nested Diagnostic Context)和MDC(Mapped Diagnostic Context)是log4j种非常有用的两个类,它们用于存储应用程序的上下文信息(context infomation),从而便于在log中使用这些上下文信息。

NDC采用了一个类似栈的机制来push和pop上下文信息,每一个线程都独立地储存上下文信息。比如说一个servlet就可以针对每一个request创建对应的NDC,储存客户端地址等等信息。

当使用的时候,我们要尽可能确保在进入一个context的时候,把相关的信息使用NDC.push(message);在离开这个context的时候使用NDC.pop()将信息删除。另外由于设计上的一些问题,还需要保证在当前thread结束的时候使用NDC.remove()清除内存,否则会产生内存泄漏的问题。

存储了上下文信息之后,我们就可以在log的时候将信息输出。在相应的PatternLayout中使用”%x”来输出存储的上下文信息,下面是一个PatternLayout的例子:

%r [%t] %-5p %c{2} %x - %m%n

使用NDC最重要的好处就是,当我们想输出一些上下文的信息的时候,不需要让logger去寻找这些信息,而只需要在适当的位置进行存储,然后再配置文件中修改PatternLayout。在最新的log4j 1.3版本中增加了一个org.apache.log4j.filters.NDCMatch

Filter,用来根据NDC中存储的信息接受或拒绝一条log信息。

        MDC和NDC非常相似,所不同的是MDC内部使用了类似map的机制来存储信息,上下文信息也是每个线程独立地储存,所不同的是信息都是以它们的key值存储在”map”中。相对应的方法,MDC.put(key, value); MDC.remove(key); MDC.get(key); 在配置PatternLayout的时候使用:%x{key}来输出对应的value。同样地,MDC也有一个org.apache.log4j.filters.MDCMatchFilter。这里需要注意的一点,MDC是线程独立的,但是一个子线程会自动获得一个父线程MDC的copy。

至于选择NDC还是MDC要看需要存储的上下文信息是堆栈式的还是key/value形式的。

NDC的实现是用hashtable来存储每个线程的stack信息,这个stack是每个线程可以设置当前线程的request的相关信息,然后当前线程在处理过程中只要在log4j配置打印出%x的信息,那么当前线程的整个stack信息就会在log4j打印日志的时候也会都打印出来,这样可以很好的跟踪当前request的用户行为功能。

MDC的实现是使用threadlocal来保存每个线程的Hashtable的类似map的信息,其他功能类似。




在分布式系统或者较为复杂的系统中,我们希望可以看到一个客户请求的处理过程所涉及到的所有子系统\模块的处理日志。

     由于slf4j/log4j基本是日志记录的标准组件,所以slf4j/log4j成为了我的重点研究对象。

     slf4j/log4j支持MDC,可以实现同一请求的日志追踪功能。

     基本思路是:

     实现自定义Filter,在接受到http请求时,计算eventID并存储在MDC中。如果涉及分布式多系统,那么向其他子系统发送请求时,需要携带此eventID。

源代码:https://github.com/athinboy/studyjava.git

其中:

response.setHeader("eventsign", eventsign);很有用,返回的response中,将有httpheader:eventsign:000010x1489108079237

log4j日志格式配置为:
log4j.appender.stdout.layout.ConversionPattern=%d %logger-%5p - %X{eventid} - %m%n
 
在web.xml中配置自定义filter:
<!--start  log4jfilter-->
    <filter>
        <filter-name>log4jfilter</filter-name>
        <filter-class>org.fgq.study.log4j.Log4jFilter</filter-class>
        <init-param>
            <param-name>sign</param-name>
            <param-value>000010x</param-value>
        </init-param>
     </filter>
    <filter-mapping>
        <filter-name>log4jfilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>  

    <!--end  log4jfilter-->

doFilterInternal方法:
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        String eventsign;
        if (request.getHeader("eventsign") == null || request.getHeader("eventsign").length() == 0) {
            eventsign = this.sign + String.valueOf(new Date().getTime()); //计算eventID
            logger.error("set eventid:" + eventsign);
        } else {
            eventsign = request.getHeader("eventsign");//从请求端获取eventsign
            logger.error("get eventid from request:" + eventsign);
        }
        MDC.put("eventid",  eventsign);
        response.setHeader("eventsign", eventsign);
        filterChain.doFilter(request, response);
    }

测试结果:

1、

Request URL:http://localhost:8084/spmvc/index.html
Request Method:GET
Status Code:304 Not Modified
Remote Address:[::1]:8084

Response Headers
Date:Fri, 10 Mar 2017 03:20:01 GMT
ETag:W/"660-1489115626000"
eventsign:000010x1489116001756

服务器端日志:
2017-03-10 11:20:01,756 org.fgq.study.log4j.Log4jFilter.doFilterInternal(Log4jFilter.java:59)ogger-ERROR - - set eventid:000010x1489116001756

MDC的一个举例:

  1. import org.apache.log4j.MDC;  
  2.   
  3. public class ThreadTest extends Thread {  
  4.     private int i ;  
  5.       
  6.     public ThreadTest(){  
  7.     }  
  8.       
  9.     public ThreadTest(int i){  
  10.         this.i = i;  
  11.     }  
  12.       
  13.     public void run(){  
  14.         System.out.println(++i);  
  15.         MDC.put("username", i);  
  16.       
  17.         for (int j = 0; j < 100; j++) {  
  18.             System.out.println("aaa" + i);  
  19.             if(j==10){  
  20.                 try {  
  21.                     this.sleep(1000);  
  22.                 } catch (InterruptedException e) {  
  23.                     e.printStackTrace();  
  24.                 }  
  25.             }  
  26.         }  
  27.         System.out.println("run: " + i + "     "  + MDC.get("username"));  
  28.     }  
  29.       
  30.     public static void main(String args[]) throws InterruptedException{  
  31.         ThreadTest t1 = new ThreadTest(1);  
  32.         t1.start();  
  33.         ThreadTest t2 = new ThreadTest(2);  
  34.         t2.start();  
  35.     }  
  36. }  

运行结果如下:

  1. 2  
  2. 3  
  3. aaa3  
  4. aaa3  
  5. aaa2  
  6. aaa3  
  7. aaa2  
  8. aaa3  
  9. aaa2  
  10. aaa3  
  11. aaa2  
  12. aaa3  
  13. aaa2  
  14. aaa3  
  15. aaa2  
  16. aaa2  
  17. aaa2  
  18. aaa2  
  19. aaa2  
  20. run: 2     2  
  21. aaa3  
  22. aaa3  
  23. aaa3  
  24. run: 3     3  
 从结果中可以看出:进程t1与t2在MDC中的值是没有相互影响的,确保了多进程下进程之间在MDC存放的值是没有相互的影响的或者说是无关的(进程t1在MDC中的username的键值为2;进程t2在MDC中的username的键值为3)。

分析:

  MDC类put方法:

查看文本打印
  1. public static void put(String key, Object o)  
  2.   {  
  3.     mdc.put0(key, o);  
  4.   }  
  5.   
  6.   private void put0(String key, Object o)  
  7.   {  
  8.     if (this.java1) {  
  9.       return;  
  10.     }  
  11.     Hashtable ht = (Hashtable)((ThreadLocalMap)this.tlm).get();  
  12.     if (ht == null) {  
  13.       ht = new Hashtable(7);  
  14.       ((ThreadLocalMap)this.tlm).set(ht);  
  15.     }  
  16.     ht.put(key, o);  
  17.   }  

结合类java.lang.ThreadLocal<T>及Thread类可以知道,MDC中的put方法其实就是讲键值对放入一个Hashtable对象中,然后赋值给当前线程的ThreadLocal.ThreadLocalMap对象,即threadLocals,这保证了各个线程的在MDC键值对的独立性。

下边为java.lang.ThreadLocal<T>的部分代码:

查看文本打印
  1. public class ThreadLocal<T> {  
  2.       
  3.     public void set(T value) {  
  4.         Thread t = Thread.currentThread();  
  5.         ThreadLocalMap map = getMap(t);  
  6.         if (map != null)  
  7.             map.set(this, value);  
  8.         else  
  9.             createMap(t, value);  
  10.     }  
  11.   
  12.   
  13.     public T get() {  
  14.         Thread t = Thread.currentThread();  
  15.         ThreadLocalMap map = getMap(t);  
  16.         if (map != null) {  
  17.             ThreadLocalMap.Entry e = map.getEntry(this);  
  18.             if (e != null)  
  19.                 return (T)e.value;  
  20.         }  
  21.         return setInitialValue();  
  22.     }  
  23.   
  24.     ThreadLocalMap getMap(Thread t) {  
  25.         return t.threadLocals;  
  26.     }  
  27.       
  28. }  

Thread类的部分代码:

  1. public class Thread implements Runnable {  
  2.     ThreadLocal.ThreadLocalMap threadLocals = null;  
  3.   
  4.     ......................  
  5.     .........................  
  6.   
  7.     static class ThreadLocalMap { //ThreadLocalMap为Thread类的内部类  
  8.   
  9.     }  
  10. }  

一:MDC介绍

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

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

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

二:日志聚合与分析(摘自:http://www.ibm.com/developerworks/cn/java/j-lo-practicelog/index.html) -- 此部分是为了方便自己查阅,从参考资料中摘出来放到这里的

在程序中正确的地方输出合适的日志消息,只是合理使用日志的第一步。日志记录的真正作用在于当有问题发生时,能够帮助开发人员很快的定位问题所在。不过一个实用的系统通常由很多个不同的部分组成。这其中包括所开发的程序本身,也包括所依赖的第三方应用程序。以一个典型的电子商务网站为例,除了程序本身,还包括所依赖的底层操作系统、应用服务器、数据库、HTTP 服务器和代理服务器和缓存等。当一个问题发生时,真正的原因可能来自程序本身,也可能来自所依赖的第三方程序。这就意味着开发人员可能需要检查不同服务器上不同应用程序的日志来确定真正的原因。

日志聚合的作用就在于可以把来自不同服务器上不同应用程序产生的日志聚合起来,存放在单一的服务器上,方便进行搜索和分析。在日志聚合方面,已经有不少成熟的开源软件可以很好的满足需求。本文中要介绍的是 logstash,一个流行的事件和日志管理开源软件。logstash 采用了一种简单的处理模式:输入 -> 过滤器 -> 输出。logstash 可以作为代理程序安装到每台需要收集日志的机器上。logstash 提供了非常多的插件来处理不同类型的数据输入。典型的包括控制台、文件和 syslog 等;对于输入的数据,可以使用过滤器来进行处理。典型的处理方式是把日志消息转换成结构化的字段;过滤之后的结果可以被输出到不同的目的地,比如 ElasticSearch、文件、电子邮件和数据库等。

Logstash 在使用起来很简单。从官方网站下载 jar 包并运行即可。在运行时需要指定一个配置文件。配置文件中定义了输入、过滤器和输出的相关配置。清单 9 给出了一个简单的 logstash 配置文件的示例。

清单 9. logstash 配置文件示例
  1. input {   
  2.  file {   
  3.    path => [ "/var/log/*.log""/var/log/messages""/var/log/syslog" ]   
  4.    type => 'syslog'  
  5.  }   
  6. }   
  7.   
  8. output {   
  9.  stdout {   
  10. debug => true   
  11. debug_format => "json"   
  12.  }   
  13. }  

清单 9 中定义了 logstash 收集日志时的输入(input)和输出(output)的相关配置。输入类型是文件(file)。每种类型输入都有相应的配置。对于文件来说,需要配置的是文件的路径。对每种类型的输入,都需要指定一个类型(type)。该类型用来区分来自不同输入的记录。代码中使用的输出是控制台。配置文件完成之后,通过“java -jar logstash-1.1.13-flatjar.jar agent -f logstash-simple.conf”就可以启动 logstash。

在日志分析中,比较重要的是结构化的信息。而日志信息通常只是一段文本,其中的不同字段表示不同的含义。不同的应用程序产生的日志的格式并不相同。在分析时需要关注的是其中包含的不同字段。比如 Apache 服务器会产生与用户访问请求相关的日志。在日志中包含了访问者的各种信息,包括 IP 地址、时间、HTTP 状态码、响应内容的长度和 User Agent 字符串等信息。在 logstash 收集到日志信息之后,可以根据一定的规则把日志信息中包含的数据提取出来并命名。logstash 提供了 grok 插件可以完成这样的功能。grok 基于正则表达式来工作,同时提供了非常多的常用类型数据的提取模式,如清单 10 所示。

清单 10. 使用 grok 提取日志记录中的内容

点击查看代码清单

在经过上面 grok 插件的提取之后,Apache 访问日志被转换成包含字段 client、method、request、status、bytes 和 useragent 的格式化数据。可以根据这些字段来进行搜索。这对于分析问题和进行统计都是很有帮助的。

当日志记录通过 logstash 进行收集和处理之后,通常会把这些日志记录保存到数据库中进行分析和处理。目前比较流行的方式是保存到 ElasticSearch 中,从而可以利用 ElasticSearch 提供的索引和搜索能力来分析日志。已经有不少的开源软件在 ElasticSearch 基础之上开发出相应的日志管理功能,可以很方便的进行搜索和分析。本文中介绍的是 Graylog2。

Graylog2 由服务器和 Web 界面两部分组成。服务器负责接收日志记录并保存到 ElasticSearch 之中。Web 界面则可以查看和搜索日志,并提供其他的辅助功能。logstash 提供了插件 gelf,可以把 logstash 收集和处理过的日志记录发送到 Graylog2 的服务器。这样就可以利用 Graylog2 的 Web 界面来进行查询和分析。只需要把清单 9 中的 logstash 的配置文件中的 output 部分改成清单 11 中所示即可。

清单 11. 配置 logstash 输出到 Graylog2
  1. output {   
  2.  gelf {   
  3.    host => '127.0.0.1'  
  4.  }   
  5. }  

在安装 Graylog2 时需要注意,一定要安装与 Graylog2 的版本相对应的版本的 ElasticSearch,否则会出现日志记录无法保存到 ElasticSearch 的问题。本文中使用的是 Graylog2 服务器 0.11.0 版本和 ElasticSearch 0.20.4 版本。

除了 Graylog2 之外,另外一个开源软件 Kibana 也比较流行。Kibana 可以看成是 logstash 和 ElasticSearch 的 Web 界面。Kibana 提供了更加丰富的功能来显示和分析日志记录。与代码清单中的 logstash 的配置相似,只需要把输出改为 elasticsearch 就可以了。Kibana 可以自动读取 ElasticSearch 中包含的日志记录并显示。


猜你喜欢

转载自blog.csdn.net/quliuwuyiz/article/details/79560404