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
public static String get(String key);
//Remove the context identified by the
public static void remove(String key);
//Clear all entries in the MDC.
public static void clear();
}
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." );
}
}
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>
<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.
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 ; (i <= n ) && ((i * 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 ((n % i ) == 0 ) {
logger .info ( "Found factor " + i );
factors .addElement ( new Integer (i ));
do {
n /= i ;
} while ((n % i ) == 0 );
}
// Placing artificial delays in tight loops will also lead to
// sub-optimal results. :-)
delay ( 100 );
}
if (n != 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 ;
}
}
}
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 ; (i <= n ) && ((i * 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 ((n % i ) == 0 ) {
logger .info ( "Found factor " + i );
factors .addElement ( new Integer (i ));
do {
n /= i ;
} while ((n % i ) == 0 );
}
// Placing artificial delays in tight loops will also lead to
// sub-optimal results. :-)
delay ( 100 );
}
if (n != 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>
<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.
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 ;
}
}
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 key | MDC value |
---|---|
req.remoteHost | as returned by the getRemoteHost() method |
req.xForwardedFor | value of the "X-Forwarded-For" header |
req.method | as returned by getMethod() method |
req.requestURI | as returned by getRequestURI() method |
req.requestURL | as returned by getRequestURL() method |
req.queryString | as returned by getQueryString() method |
req.userAgent | value 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>
<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>