Mapped Diagnostic Context (MDC)

logback的其中一设计目的就是方便监控和调试复杂的分布式应用。在现实生活中,分布式应用需要同时处理很多客户端请求。一种典型的多线程实现,不同的线程处理不同的客户端。为了区分不同客户端产生的日志输出,一种可行但不推荐的方法是为每一个客户端生成一个logger实例。这种做法会随着客户端的增多可能导致资源殆尽。
一种更机智的技巧是为每一个客户端独立标记 log request(Looging Diagnostic Message)。Logback 利用了这种技术的一种变体叫 Mapped Diagnostic Contexts (MDC)。
为了达到独立标记每一个requst,使用者会将 上下文的一些信息放到 MDC,下面我们看看MDC这个类一些显著的方法,也可以通过 MDC javadocs查看所有方法

package org .slf4j ;

public  class MDC  {
  //Put a context value as identified by  key
  //into the current thread's context map.
  public static void put(String key, String val);

  //Get the context identified by the  key  parameter.
  public static String get(String key);

  //Remove the context identified by the  key  parameter.
  public static void remove(String key);

  //Clear all entries in the MDC.
  public static void clear();
}

可以看到MDC类中只包含 静态 方法。这让开发人员可以在其他一些logback 组件中重新引用MDC中的信息。 MDC 是线程独立的。不过子线程会自动继承父线程的MDC。典型的应用:在开始处理客户端请求的时候,开发人员可以将一些相关的上下文信息插入到mdc中,例如:客户端ip地址,请求参数等。

下面看个简单的例子:

package chapters .mdc ;

import org .slf4j . Logger ;
import org .slf4j . LoggerFactory ;
import org .slf4j .MDC ;

import ch .qos .logback .classic . PatternLayout ;
import ch .qos .logback .core . ConsoleAppender ;

public  class  SimpleMDC  {
  static  public  void main ( String [] args )  throws  Exception  {

    // You can put values in the MDC at any time. Before anything else
    // we put the first name
    MDC .put ( "first" ,  "Dorothy" );

    [ 片段  ]
   
    Logger logger  =  LoggerFactory .getLogger ( SimpleMDC . class );
    // We now put the last name
    MDC .put ( "last" ,  "Parker" );

    // The most beautiful two words in the English language according
    // to Dorothy Parker:
    logger .info ( "Check enclosed." );
    logger .debug ( "The most beautiful two words in English." );

    MDC .put ( "first" ,  "Richard" );
    MDC .put ( "last" ,  "Nixon" );
    logger .info ( "I am not a crook." );
    logger .info ( "Attributed to the former US president. 17 Nov 1973." );
  }

  [ 片段  ]

}

配置:
<appender  name = "CONSOLE"  class = "ch.qos.logback.core.ConsoleAppender" > 
  <layout>
    <Pattern> %X{first} %X{last} - %m%n </Pattern>
  </layout> 
</appender>

输出结果:
Dorothy Parker - Check enclosed.
Dorothy Parker - The most beautiful two words in English.
Richard Nixon - I am not a crook.
Richard Nixon - Attributed to the former US president. 17 Nov 1973.

可以看出,向MDC中插入重复的key值,则旧的value会被覆盖。



Advanced Use
即使,MDC中的方法都是静态的,但是MDC是由线程独立管理的,MDC中的put(), get()仅仅影响当前的线程,以及它的子线程。因此开发人员在编码时不用担心线程安全以及同步的问题。
下面的例子中,采用了远程方法调用的方式(RMI),客户端会调用服务端的 factor()方法。例子很长,不必在意其中的算法
package chapters .mdc ;

import java .rmi . RemoteException ;
import java .rmi .registry . LocateRegistry ;
import java .rmi .registry . Registry ;
import java .rmi .server . UnicastRemoteObject ;
import java .util . Vector ;

import org .slf4j . Logger ;
import org .slf4j . LoggerFactory ;
import org .slf4j .MDC ;

import ch .qos .logback .classic . LoggerContext ;
import ch .qos .logback .classic .joran . JoranConfigurator ;
import ch .qos .logback .core .joran .spi . JoranException ;


/**
 * A simple NumberCruncher implementation that logs its progress when
 * factoring numbers. The purpose of the whole exercise is to show the
 * use of mapped diagnostic contexts in order to distinguish the log
 * output from different client requests.
 * */

public  class  NumberCruncherServer  extends  UnicastRemoteObject
  implements  NumberCruncher  {

  private  static  final  long serialVersionUID  =  1L ;

  static  Logger logger  =  LoggerFactory .getLogger ( NumberCruncherServer . class );

  public  NumberCruncherServer ()  throws  RemoteException  {
  }

  public  int [] factor ( int number )  throws  RemoteException  {
    // The client's host is an important source of information.
    try  {
      MDC.put("client", NumberCruncherServer.getClientHost());
    }  catch  (java .rmi .server . ServerNotActiveException e )  {
      logger .warn ( "Caught unexpected ServerNotActiveException." , e );
    }

    // The information contained within the request is another source
    // of distinctive information. It might reveal the users name,
    // date of request, request ID etc. In servlet type environments,
    // useful information is contained in the HttpRequest or in the 
    // HttpSession.
    MDC.put("number", String.valueOf(number));

    logger .info ( "Beginning to factor." );

    if  (number  <=  0 )  {
      throw  new  IllegalArgumentException (number  +
        " is not a positive integer." );
    }  else  if  (number  ==  1 )  {
      return  new  int []  {  1  };
    }

    Vector < Integer > factors  =  new  Vector < Integer >();
    int n  = number ;

    for  ( int i  =  2 ;  (<= n )  &&  ((* i )  <= number ); i ++)  {
      // It is bad practice to place log requests within tight loops.
      // It is done here to show interleaved log output from
      // different requests. 
      logger .debug ( "Trying "  + i  +  " as a factor." );

      if  ((% i )  ==  0 )  {
        logger .info ( "Found factor "  + i );
        factors .addElement ( new  Integer (i ));

        do  {
          n  /= i ;
        }  while  ((% i )  ==  0 );
      }

      // Placing artificial delays in tight loops will also lead to
      // sub-optimal results. :-)
      delay ( 100 );
    }

    if  (!=  1 )  {
      logger .info ( "Found factor "  + n );
      factors .addElement ( new  Integer (n ));
    }

    int len  = factors .size ();

    int [] result  =  new  int [len ];

    for  ( int i  =  0 ; i  < len ; i ++)  {
      result [i ]  =  (( Integer ) factors .elementAt (i )).intValue ();
    }

    // clean up
    MDC.remove("client");
    MDC.remove("number");


    return result ;
  }

  static  void usage ( String msg )  {
    System .err .println (msg );
    System .err .println ( "Usage: java chapters.mdc.NumberCruncherServer configFile\n"  +
      "   where configFile is a logback configuration file." );
    System . exit ( 1 );
  }

  public  static  void delay ( int millis )  {
    try  {
      Thread .sleep (millis );
    }  catch  ( InterruptedException e )  {
    }
  }

  public  static  void main ( String [] args )  {
    if  (args .length  !=  1 )  {
      usage ( "Wrong number of arguments." );
    }

    String configFile  = args [ 0 ];

    if  (configFile .endsWith ( ".xml" ))  {
      try  {
        LoggerContext lc  =  ( LoggerContext )  LoggerFactory .getILoggerFactory ();
        JoranConfigurator configurator  =  new  JoranConfigurator ();
        configurator .setContext (lc );
        lc .reset ();
        configurator .doConfigure (args [ 0 ]);
      }  catch  ( JoranException je )  {
        je .printStackTrace ();
      }
    }

    NumberCruncherServer ncs ;

    try  {
      ncs  =  new  NumberCruncherServer ();
      logger .info ( "Creating registry." );

      Registry registry  =  LocateRegistry .createRegistry ( Registry .REGISTRY_PORT );
      registry .rebind ( "Factor" , ncs );
      logger .info ( "NumberCruncherServer bound and ready." );
    }  catch  ( Exception e )  {
      logger .error ( "Could not bind NumberCruncherServer." , e );

      return ;
    }
  }
}

配置:
<configuration>
  <appender  name = "CONSOLE"  class = "ch.qos.logback.core.ConsoleAppender" >
    <layout>
      <Pattern>%-4r [%thread] %-5level  C:%X{client} N:%X{number} - %msg%n </Pattern>
    </layout>       
  </appender>
 
  <root  level = "debug" >
    <appender-ref  ref = "CONSOLE" />
  </root> 
</configuration>

样例输出:
70984 [RMI TCP Connection(4)-192.168.1.6] INFO  C:orion N:129 - Beginning to factor.
70984 [RMI TCP Connection(4)-192.168.1.6] DEBUG C:orion N:129 - Trying 2 as a factor.
71093 [RMI TCP Connection(4)-192.168.1.6] DEBUG C:orion N:129 - Trying 3 as a factor.
71093 [RMI TCP Connection(4)-192.168.1.6] INFO  C:orion N:129 - Found factor 3
71187 [RMI TCP Connection(4)-192.168.1.6] DEBUG C:orion N:129 - Trying 4 as a factor.
71297 [RMI TCP Connection(4)-192.168.1.6] DEBUG C:orion N:129 - Trying 5 as a factor.
71390 [RMI TCP Connection(4)-192.168.1.6] DEBUG C:orion N:129 - Trying 6 as a factor.
71453 [RMI TCP Connection(5)-192.168.1.6] INFO  C:orion N:71 - Beginning to factor.
71453 [RMI TCP Connection(5)-192.168.1.6] DEBUG C:orion N:71 - Trying 2 as a factor.
71484 [RMI TCP Connection(4)-192.168.1.6] DEBUG C:orion N:129 - Trying 7 as a factor.
71547 [RMI TCP Connection(5)-192.168.1.6] DEBUG C:orion N:71 - Trying 3 as a factor.
71593 [RMI TCP Connection(4)-192.168.1.6] DEBUG C:orion N:129 - Trying 8 as a factor.
71656 [RMI TCP Connection(5)-192.168.1.6] DEBUG C:orion N:71 - Trying 4 as a factor.
71687 [RMI TCP Connection(4)-192.168.1.6] DEBUG C:orion N:129 - Trying 9 as a factor.
71750 [RMI TCP Connection(5)-192.168.1.6] DEBUG C:orion N:71 - Trying 5 as a factor.
71797 [RMI TCP Connection(4)-192.168.1.6] DEBUG C:orion N:129 - Trying 10 as a factor.
71859 [RMI TCP Connection(5)-192.168.1.6] DEBUG C:orion N:71 - Trying 6 as a factor.
71890 [RMI TCP Connection(4)-192.168.1.6] DEBUG C:orion N:129 - Trying 11 as a factor.
71953 [RMI TCP Connection(5)-192.168.1.6] DEBUG C:orion N:71 - Trying 7 as a factor.
72000 [RMI TCP Connection(4)-192.168.1.6] INFO  C:orion N:129 - Found factor 43
72062 [RMI TCP Connection(5)-192.168.1.6] DEBUG C:orion N:71 - Trying 8 as a factor.
72156 [RMI TCP Connection(5)-192.168.1.6] INFO  C:orion N:71 - Found factor 71

在这个例子中,它开始先将client 的 hostname put进了mdc中,key为client,然后把number的值也put进了mdc中,key为number。经过一系列计算后,将结果返回给客户端,在返回客户端前,将mdc中刚才的键值对remove掉。这样做是为了防止保留过期的数据,如果采用了线程池的话,这个过期的数据可能会被重用,导致不必要的错误。所以我们推荐无论何时,都应该保证put 跟 remove应该成对出现,并保证remove一定会被执行。
从样例的输出我们也印证了刚才的理论,MDC是基于线程独享实现的。


Automating access to the MDC
正如我们所见,MDC在处理多客户端的情况非常有效。为了允许MDC中的信息在任何时候都是正确有效的,我们需要在request被处理之前,就讲相关信息放入mdc,再在处理完后,remove掉。而这里我们就可以通过Servlet Filter实现。
下面看个简单的自己实现的例子:
package chapters .mdc ;

import java .io . IOException ;
import java .security . Principal ;

import javax .servlet . Filter ;
import javax .servlet . FilterChain ;
import javax .servlet . FilterConfig ;
import javax .servlet . ServletException ;
import javax .servlet . ServletRequest ;
import javax .servlet . ServletResponse ;
import javax .servlet .http . HttpServletRequest ;
import javax .servlet .http . HttpSession ;

import org .slf4j .MDC ;

public  class  UserServletFilter  implements  Filter  {

  private  final  String USER_KEY  =  "username" ;
 
  public  void destroy ()  {
  }

  public  void doFilter ( ServletRequest request ,  ServletResponse response ,
    FilterChain chain )  throws  IOException ,  ServletException  {

    boolean successfulRegistration  =  false ;

    HttpServletRequest req  =  ( HttpServletRequest ) request ;   
    Principal principal  = req .getUserPrincipal ();
    // Please note that we could have also used a cookie to 
    // retrieve the user name

    if  (principal  !=  null )  {
      String username  = principal .getName ();
      successfulRegistration  = registerUsername (username );
    } 

    try  {
      chain .doFilter (request , response );
    }  finally  {
      if  (successfulRegistration )  {
        MDC .remove (USER_KEY );
      }
    }
  }

  public  void init ( FilterConfig arg0 )  throws  ServletException  {
  }
 

  /**
   * Register the user in the MDC under USER_KEY.
   * 
   * @param username
   * @return true id the user can be successfully registered
   */

  private  boolean registerUsername ( String username )  {
    if  (username  !=  null  && username .trim ().length ()  >  0 )  {
      MDC .put (USER_KEY , username );
      return  true ;
    }
    return  false ;
  }
}


logback也提供了一个 MDCInsertingServletFilter ,将以下信息放入到了MDC中
MDC keyMDC value
req.remoteHostas returned by the getRemoteHost() method
req.xForwardedForvalue of the "X-Forwarded-For" header
req.methodas returned by getMethod() method
req.requestURIas returned by getRequestURI() method
req.requestURLas returned by getRequestURL() method
req.queryStringas returned by getQueryString() method
req.userAgentvalue of the "User-Agent" header

使用配置:需要保证filter在需要使用的到该MDC的其他filter之前。
<filter>
  <filter-name>MDCInsertingServletFilter </filter-name>
  <filter-class>
    ch.qos.logback.classic.helpers.MDCInsertingServletFilter
  </filter-class>
</filter>
<filter-mapping>
  <filter-name>MDCInsertingServletFilter </filter-name>
  <url-pattern>/* </url-pattern>
</filter-mapping> 







评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值