夕辞

Apache log4j介绍 | 夕辞夕辞

Apache log4j介绍

在所有JAVA开发人员的职业生涯中,日志绝对是不可或缺的一部分,但是对于很多人来说(其实就是作者),要么是公司有统一日志组件配置,要么就是搜索网上的配置方法,其实对日志本身技术以及如何生效并没有深究,上半年在公司从log4j升级到log4j2中遇到问题,发现没有像解决其他问题那样得心应手了,因此下定决心来研究下。

首先还是从最基本的,从Apache的log4j开始了,原文链接:http://logging.apache.org/log4j/1.2/manual.html

PS:翻译有增减


产品终结

 

2015年8月5日,日志服务项目管理委员会宣布Log4j 1.x版本已经终结,有关公告的完整内容请参见Apache Blog。Log4j 1.x相关版本的使用者建议升级到Apache Log4j 2。


摘要

 

本文档介绍了log4j API独特的功能和设计原理。Log4j是一个基于许多开发者的开源项目。它允许开发人员以任意细粒度来决定不同日志的输出。它支持使用外部配置文件完全配置运行时参数。最重要的是,log4j有一个温和的学习曲线。当心:从用户反馈来看,它也令人上瘾。


介绍

 

几乎所有的大型应用都包含自己的日志和跟踪API。按照这条规则,E.U SEMPER项目决定更便携自己的跟踪API。在1996年初,经过无数次的优化提升和大量的工作,API已经发展成为log4j,一个JAVA的很受欢迎的日志记录包。该软件包是又Apache软件许可证分发,通过开源计划认证的完整开源学科正。最新的log4j版本,包括完整源代码,类文件和文档可以在http://logging.apache.org/log4j/找到。顺便说下,log4j已经移植到C、C++、C#、Perl、Python、Ruby和Eiffel语言中。

 

在代码中插入日志语句是一个低级别技术的调试方法,但是它可能也是唯一的方式,因为调试器不可能总是好用的(线上环境更不可能断点停止)。对于多线程应用程序和分布式应用程序通常就是这样。

 

经验表明,日志打印在开发周期中是很重要的环节。它提供了几个优势。它提供了运行一个程序精确的上下文,一旦插入代码,输出日志的过程就不需要人为干预了。此外,输出的日志还可以保存在持久介质中,以便后续进行分析。除了在开发周期中使用,一个足够详细丰富的日志记录还可以作为审计工具使用(QPS、访问记录等)。

 

正如Brian W. Kernighan和Rob Pike在《编程实践》这本真正的优秀的书中写的一样:

作为个人选择,我们倾向于不通过调试器跟踪对战或者一两个变量的值。一个原因是它很容易让人迷失在复杂数据结构和控制流程上,我们发现通过努力思考和在关键地方添加自建代码比通过一个程序更为有效。逐行点击鼠标执行语句比浏览明智的输出日志更花费时间,并且决定在哪里打印日志比逐行执行关键代码更节省时间,更是因为我们一般知道调试点在哪里。更重要的是,调试语句要与程序保持一致(很多情况下需要暂停执行),而调试会话打印日志纸质暂时的。

 

日志打印确实有它的缺点,它有可能拖慢应用的速度。如果太详细,可能导致屏幕闪动。为了减轻这些担忧。log4j被设计为可靠、快速、可扩展的特点。由于日志很少是应用主要的焦点,因此log4j API努力使其变得易于理解和使用。


Loggers,Appenders and Layouts(记录器、追加器、布局)

 

Log4j有三个主要组件:loggers(记录器)、appenders(追加器)和layouts(布局)。这三种组件一起工作,使开发人员能够根据日志类型和级别来记录日志,并在运行时控制这些日志的格式和存储位置。


Logger hierarchy(记录器层级)

 

任何日志记录API相较于System.out.println的一个最基本的优势在于它能够禁止某些日志打印,同事允许其他日志不受限制的打印。此功能假设记录的空间,即所有有可能打印日志记录语句的空间(也就是一个运行时程序,包含它的依赖),是根据某些开发人员选择的标准进行分类的。此观察角度曾导致我们将类别作为包的中心概念。但是,由于log4j 1.2版本,Logger类已经替换了Category类。对于熟悉log4j早期版本的用户,Logger类可以被认为是Category类的别名。

 

Loggers是以实体命名。Looger名称区分大小写,并且它们遵循层级命名规则:

层级命名:

当A looger的名字是B logger的名字后面加上点然后加上名字(参考JAVA类的包名规则)时,我们可以认为B是A的祖先,也就是集成关系。如果两个logger之间没有层级在两者之间时,我们认为他们是父级别和子级别的关系(JAVA的父类和子类,但是概念是在包上)

 

举个例子,一个名为“com.fool”的logger是名为“com.fool.Bar”的父级。同样,java是java.util的父类,也是java.util.Vector的祖先。这个命名方案是大部分开发人员熟悉的。

 

Root logger处于记录器的顶部,它有两个特别的地方:

1.它总是存在;

2.它不能通过名称来获取。

 

调用静态方法Logger.getRootLogger可以获取它,且所有其他logger都是用静态方法Logger.getLogger实例化和获取。该方法以所需logger的名称作为参数,一些Logger基本的方法在下面列出:

package org.apache.log4j;

public class Logger {

    // Creation & retrieval methods:
    public static Logger getRootLogger();
    public static Logger getLogger(String name);

    // printing methods:
    public void trace(Object message);
    public void debug(Object message);
    public void info(Object message);
    public void warn(Object message);
    public void error(Object message);
    public void fatal(Object message);

    // generic printing method:
    public void log(Level l, Object message);
}

 

Logger是可能被分配级别,下面是一组可能的级别:TRACE、DEBUG、INFO、WARN、ERROR、FATAL,这些是定义在org.apache.log4j.Level类中的。虽然我们不鼓励你这样做,你可以通过对Level类进行分类来定义你自己的日志级别。稍后会解释一个更好的方法。

 

如果一个给定的logger没有分配级别,那么它将从离他最接近的祖先中集成一个指定级别的logger,更正式来说是:

给定logger C集成的日志级别,等于logger层次结构中从C开始向root logger开始算起,第一个非空的日志级别。

 

为了确保所有的logggers最终都能继承一个级别,root logger总是会被分配一个级别。

 

以下是具有各种分配级别值的四个表格,以及他们根据上述从规则生成的继承级别。

 

例子1:

Logger name 分配级别 遗传级别
root Proot Proot
X none Proot
X.Y none Proot
X.Y.Z none Proot

在例子1中,只有root logger分配了级别,是Proot,被其他loggers继承

 

例子2:

Logger name 分配级别 遗传级别
root Proot Proot
X Px Px
X.Y Pxy Pxy
X.Y.Z Pxyz Pxyz

在例子2中,所有的logggers都有分配的级别,不需要继承级别

 

例子3:

Logger name 分配级别 遗传级别
root Proot Proot
X Px Px
X.Y none Px
X.Y.Z Pxyz Pxyz

在例子3中,root、X、X.Y.Z都分配了级别,而X.Y从它的父级X继承了Px

 

例子4:

Logger name 分配级别 遗传级别
root Proot Proot
X Px Px
X.Y none Px
X.Y.Z none Px

在例子4中,root和X分别分配了Proot、Px,X.Y和X.Y.Z从离它们最近的父级获取分配的级别

 

日志请求是通过调用logger实例的方法之一进行的,这些打印的方法是debug、info、warn、error、fatal和log。

 

根据定义,打印方法决定日志请求的级别。例如,如果c是一个logger实例,那么c.info(“”)是一个info级别的日志请求。

 

如果日志请求的级别高于或者等于它的实例的级别,那么日志请求是可用的。否则,这个日志请求是禁用的。没有被分配级别的logger将从层级中集成一个级别。这个规则总结如下:

基本选择规则:

在一个级别q的logger(无论是分配还是继承)中,对于级别p的日志请求,如果p>=q则可用

 

这个规则是log4j的核心,它假设这些级别是有序的,对于标准级别:DEBUG < INFO < WARN < ERROR < FATAL。

 

下面是这个规则的一个例子:

// get a logger instance named "com.foo"
Logger  logger = Logger.getLogger("com.foo");

// Now set its level. Normally you do not need to set the
// level of a logger programmatically. This is usually done
// in configuration files.
logger.setLevel(Level.INFO);

Logger barlogger = Logger.getLogger("com.foo.Bar");

// This request is enabled, because WARN &amp;amp;amp;gt;= INFO.
logger.warn("Low fuel level.");

// This request is disabled, because DEBUG &amp;amp;amp;lt; INFO. logger.debug("Starting search for nearest gas station."); // The logger instance barlogger, named "com.foo.Bar", // will inherit its level from the logger named // "com.foo" Thus, the following request is enabled // because INFO &amp;amp;amp;gt;= INFO.
barlogger.info("Located nearest gas station.");

// This request is disabled, because DEBUG &amp;amp;amp;lt; INFO.
barlogger.debug("Exiting gas station search");

调用getLogger方法时如果名称相同,始终会返回完全相同的logger对象的引用。

 

例如,在:

Logger x = Logger.getLogger("wombat");
Logger y = Logger.getLogger("wombat");

x和y指的是完全相同的logger对象。

 

因此,是可以定义一个logger,然后在其他代码中获取同样的实例,而不用传递引用。和生物学中父级总是先与子级的矛盾不同,log4j loggers可以以任何顺序和配置创建。特别尽管一个父级是在子级后面实例化,但是仍然可以影响子级。

 

log4j的环境配置通常是在程序初始化时完成,首选方法是通过读取配置文件,这种做法将很快讨论。

 

Log4j使软件组件可以轻松的给logger命名,可以在每个类中静态实例化相关的logger,这样logger名就是该类的完全限定名(加上包名的类名),这是定义loggers最有效和直接的方法。由于日志是以logger的名字输出的,因此这个命名策略可以轻松识别日志的来源。但是,这只是一个可能最常见的对loggers命名进行分类的策略,log4j并不严格限制loggers的命名,开发者仍然可以根据需求自由的给loggers命名。

 

然后,类名命名loggers仍然是迄今为止已知的最佳策略。


Appenders and Layouts(追加器和布局)

 

基于loggers启用或禁用日志记录请求仅仅是功能的一部分,log4j也支持将日志输出多个位置。在log4j语言中,一个日志输出位置被称为appender,目前,有console, files, GUI components, remote socket servers, JMS, NT Event Loggers, and remote UNIX Syslog daemons这些appenders,而且也可以异步记录。

 

可以将多个appender绑定到一个logger。

 

addAppender方法可以给logger添加一个appender,每个logger的日志请求将会转发给所有该logger的appenders以及较高层次的appender。例如如果一个console appender添加到了root logger,那么所有可用的日志记录请求将至少会在控制台打印。如果将一个文件appender追加到一个名为C的logger上,那么C以及C的子级可用的日志请求将同时在文件和控制台中打印。通过将additivity设置为false可禁止appender向上追加(类似js的冒泡)。

 

appder追加规则可总结如下:

Appender追加性:

logger C的输出将同时转发C所有的父级和祖先的所有appender,但是,比如logger P是logger C的一个祖先,并且logger P设置additivity为false,那么logger C所有的输出将被转发给所有包括logger P在内所有的祖先的appender,但是不包括从logger P向上的祖先的appender。

loggers的additivity属性默认为true。

 

下面的表格是一个示例:

Logger name 添加的Appenders Additivity属性 输出目标 备注
root A1 root,无该属性 A1  root logger是匿名的,但是可以通过Logger.getRootLogger()方法访问,root没有默认的appender
x A-x1, A-x2 true A1, A-x1, A-x2  Appenders包括“x”和root
x.y none true A1, A-x1, A-x2  Appenders包括“x”和root
x.y.z A-xyz1 true A1, A-x1, A-x2, A-xyz1  Appenders包括“x.y.z”、“x”和root
security A-sec false A-sec  additivity设置false后没有累加的appender
security.access none true A-sec  只有“security”的appender集成,因为“security”的additivity设置为false

 

通常情况下,用户不仅要自定义输出位置,还要自定义输出日志格式,可以通过在appender上关联一个layout来做到。Layout负责按照用户的设置格式化日志请求,而appender关心的是将格式化后的日志输出到相应的位置。

 

PatternLayout是标准log4j分发的一部分,它允许用户根据类似于于C语言printf函数转换功能来格式化日志输出。

 

例如,PatternLayou配置“%r [%t] %-5p %c – %m%n”将会输出类似于下面的日志:

176 [main] INFO  org.foo.Bar – Located nearest gas station.

 

第一个字段是从程序启动依赖经过的毫秒数,第二个字段是发出日志请求的线程,第三个字段是日志语句的级别,第四个字段日期请求的logger的名字,“-”后面的文本就是日志请求的消息。

 

同样重要的是,log4j将根据用户指定的条件呈现日志内容。例如,如果你经常需要记录当前项目中使用的Oranges对象,那么可以注册一个OrangeRenderer,每次需要记录Orange对象时它都会被调用。

 

对象渲染也遵循类层级结构。例如,假设橘子是水果,如果你注册一个水果Renderer,所有包含橘子在内的水果都会由水果Renderer渲染,除非自定义里橘子Renderer。

 

对象Rednerers必须实现ObjectRenderer接口。


Configuration(配置)

 

在应用程序代码中插入日志请求需要大量的计划和努力,观察表明大概4%的代码专用于日志记录。因此,甚至中等大小的应用也将在其代码中嵌入数千个日志记录语句,给定它们的数量,需要手动修改就能管理他们是必不可少的。

 

Log4j环境是可完全配置的,但是,使用配置文件来配置log4j更为灵活。目前,配置文件可用XML或者JAVA属性(kv,例如properteis)。

 

下面借鉴虚拟程序MyApp来帮你了解如何完成此操作:

import com.foo.Bar;
import org.apache.log4j.BasicConfigurator;
// Import log4j classes.
import org.apache.log4j.Logger;


public class MyApp {
    // Define a static logger variable so that it references the
    // Logger instance named "MyApp".
    static Logger logger = Logger.getLogger(MyApp.class);

    public static void main(String[] args) {
        // Set up a simple configuration that logs on the console.
        BasicConfigurator.configure();

        logger.info("Entering application.");

        Bar bar = new Bar();
        bar.doIt();
        logger.info("Exiting application.");
    }
}

MyApp以引入log4j相关类开始,然后定义了一个静态的名为MyApp类完全限定名的logger变量。
MyApp用到的Bar类在com.foo包中定义:

package com.foo;

import org.apache.log4j.Logger;

public class Bar {
    static Logger logger = Logger.getLogger(Bar.class);

    public void doIt() {
        logger.debug("Did it again!");
    }
}

 

调用BasicConfigurator.configure方法会创建一个很简单的log4j配置,这种方法是硬连线到root logger一个ConsoleAppender,输出会被格式化为一个配置为“%-4r [%t] %-5p %c %x – %m%n”的PatternLayout。

 

请注意,默认情况下,root logger被分配级别Level.DEBUG,MyApp输出是:

0 [main] INFO MyApp – Entering application.
36 [main] DEBUG com.foo.Bar – Did it again!
51 [main] INFO MyApp – Exiting application.

 

下图描绘了MyApp调用BasicConfigurator.configure方法的对象图:

 

作为附注,不得不提log4j中子loggers只链接到它们存在的祖先,具体来说,一个名字为com.foo.Bar的logger直接链接到root logger,而绕过了并未使用的com或com.foo的logger。这个特性显著提高了性能并且降低了log4j的内存占用。

 

MyApp类通过调用BasicConfigurator.configure方法,其他类只需要引入org.apache.log4j.Logger类,然后获取他们想用的loggers,然后退出。

 

上一个例子总是输出相同的日志信息,幸运的是,修改MyApp很容易,以达到运行时控制日志输出,这是一个有修改的版本:

import com.foo.Bar;

import org.apache.log4j.Logger;
import org.apache.log4j.PropertyConfigurator;


public class MyApp {
    static Logger logger = Logger.getLogger(MyApp.class.getName());

    public static void main(String[] args) {
        // BasicConfigurator replaced with PropertyConfigurator.
        PropertyConfigurator.configure(args[0]);

        logger.info("Entering application.");

        Bar bar = new Bar();
        bar.doIt();
        logger.info("Exiting application.");
    }
}

这个版本的MyApp使PropertyConfigurator解析配置文件从而配置logger。

下面是一个示例配置文件,它的结果和之前BasicConfigurator的示例相同。

# Set root logger level to DEBUG and its only appender to A1.
log4j.rootLogger=DEBUG, A1

# A1 is set to be a ConsoleAppender.
log4j.appender.A1=org.apache.log4j.ConsoleAppender

# A1 uses PatternLayout.
log4j.appender.A1.layout=org.apache.log4j.PatternLayout
log4j.appender.A1.layout.ConversionPattern=%-4r [%t] %-5p %c %x - %m%n

假设我们不需要查看属于com.fool任何组件的输出,以下配置文件展示了实现此目的的一个方法:

log4j.rootLogger=DEBUG, A1
log4j.appender.A1=org.apache.log4j.ConsoleAppender
log4j.appender.A1.layout=org.apache.log4j.PatternLayout

# Print the date in ISO 8601 format
log4j.appender.A1.layout.ConversionPattern=%d [%t] %-5p %c - %m%n

# Print only messages of level WARN or above in the package com.foo.
log4j.logger.com.foo=WARN

配置了此文件的MyApp输出的日志如下:

2000-09-07 14:07:41,508 [main] INFO MyApp – Entering application.
2000-09-07 14:07:41,529 [main] INFO MyApp – Exiting application.

由于名com.foo.Bar的logger并没有分配级别,它从com.fool继承配置为WARN的级别,Bar.doIt方法的日志级别为DEBUG,低于WARN,因此,doIt()方法的日志请求被拒绝。

 

下面是另一个使用多个appender的配置文件:

log4j.rootLogger=debug, stdout, R

log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout

# Pattern to output the caller's file name and line number.
log4j.appender.stdout.layout.ConversionPattern=%5p [%t] (%F:%L) - %m%n

log4j.appender.R=org.apache.log4j.RollingFileAppender
log4j.appender.R.File=example.log

log4j.appender.R.MaxFileSize=100KB
# Keep one backup file
log4j.appender.R.MaxBackupIndex=1

log4j.appender.R.layout=org.apache.log4j.PatternLayout
log4j.appender.R.layout.ConversionPattern=%p %t %c - %m%n

使用此配置然后调用MyApp将在控制台输出以下内容:

INFO [main] (MyApp2.java:12) – Entering application.
DEBUG [main] (Bar.java:8) – Doing it again!
INFO [main] (MyApp2.java:15) – Exiting application.

另外,由于root logger已经被分配了第二个appender,输出也将被指到example.log文件,当文件达到100K时,该文件将滚动,当发生滚动时,旧版本的example.log将被移动到example.log.1。

 

请注意,为了实现这些不同的记录行为,我们并不需要重新编译代码,我们可以轻松的登录一个UNIX系统Syslog守护程序,将所有com.foo输出重定向到NT事件记录器,或根据本地服务器策略将日志记录转发到远程log4j服务器,甚至第二个log4j服务器。


Default Initialization Procedure(默认初始化过程)

 

log4j不会对其环境作出任何假设,特别是不存在默认的log4j appenders。在某些明确定义前提下,Logger类的静态初始化方法将尝试自动配置log4j。JAVA语言保证在类加载到内存时一个类的静态初始化方法会且仅会被调用一次。重要的是要记住,不同的类加载器可以加载同一个类的不同副本,但是这些同一个类的副本我们认为与JVM完全没关系。

 

应用的确切启动取决于运行环境时默认的初始化显得尤为重要。例如,一个同样的应用可以作为独立程序、作为小应用程序,或者额作为Web服务器下的servlet使用。

 

精确的默认初始化算法定义如下:

1.将log4j.defaultInitOverride系统属性设置为任何其他值时,“false”将导致log4j跳过默认的初始化过程;
2.将资源变量设置为log4j.configuration系统属性的值。指定默认初始化文件的首选方法是通过log4j.configuration系统属性,如果未定义系统属性log4j.configuration,则资源变量会被设置为它的默认值“log4j.properties”;
3.尝试转换资源变量为URL;
4.如果资源变量无法转换为URL,例如由于MalformedURLException,则将通过调用org.apache.log4j.helpers.Loader.getResource(resource,Logger.class)方法返回的URL来搜索资源。请注意,字符串“log4j.properties”是不正确的URL。有关搜索位置的列表,请参阅Loader.getResource(java.lang.String)。
5.如果找不到URL,则中止默认初始化,否则根据URL配置log4j。PropertyConfigurator将用于解析URL以配置log4j,在URL以“.xml”结尾时将使用DOMConfigurator。你也可以选择指定自定义配置器。系统属性log4j.configuratorClass的值被视为自定义配置器的完全类名,且自定义的配置器必须实现Configurator接口。


Default Initialization under Tomcat(Tomcat下的默认初始化)

 

默认的log4j初始化在Web服务器环境中特别有用。在Tomcat3.x和4.x下,你应该将log4j.properties放在WEB-INF/classess目录下,Log4j会找到属性配置文件并自动初始化,这很容易并且很好使。

 

你还可以选择在启动Tomcat之前设置系统属性log4j.configuration,对于Tomcat3.x环境变量TOMCAT_OPTS用于设置命令行选项,对于Tomcat4.0,以设置环境变量CATALINA_OPTS来代替TOMCAT_OPTS。

 

示例1:

Unix shell命令【export TOMCAT_OPTS=”-Dlog4j.configuration=foobar.txt”】告诉log4j使用foobar.txt作为默认配置文件。这个文件应放置在你的web应用的WEB-INF/classes目录下,且这个文件将会通过PropertyConfigurator读取,每一个web应用都用不同观点配置文件,因为配置文件是和web应用关联的。

 

示例2:

Unix shell命令【export TOMCAT_OPTS=”-Dlog4j.debug -Dlog4j.configuration=foobar.xml”】告诉log4j输出内部调试信息并使用foobar.xml作为默认配置文件。该文件应放置在你的web应用的WEB-INF/classes目录下,且这个文件将会通过DOMConfigurator读取,每一个web应用都用不同观点配置文件,因为配置文件是和web应用关联的。

 

示例3:

Windows shell命令【set TOMCAT_OPTS=-Dlog4j.configuration=foobar.lcf -Dlog4j.configuratorClass=com.foo.BarConfigurator】告诉log4j使用foobar.lcf作为默认配置文件。这个文件应放置在你的web应用的WEB-INF/classes目录下,由于定义了log4j.configuratorClass系统属性,该文件将会通过自定义com.foo.BarConfigurator来读取,每一个web应用都用不同观点配置文件,因为配置文件是和web应用关联的。

 

示例4:

Windows shell命令【set TOMCAT_OPTS=-Dlog4j.configuration=file:/c:/foobar.lcf】告诉log4j使用c:\foobar.lcf作为默认配置文件,配置文件通过file:被完全指定。这样,同样的配置文件将会被所有web应用使用。

不同的web应用将通过它们各自的类加载器加载log4j类,这样,每个log4j环境将会独立的进行且没有任何互相同步。例如,FileAppenders在多个web应用中配置完全相同,都会尝试写入相同的文件。结果可能不太令人满意,所以你必须确保不同web应用的log4j配置不要使用相同的底层系统资源。

 

初始化Servlet:

也可以通过特殊的servlet初始化log4j,下面是一个例子:

package com.foo;

import org.apache.log4j.PropertyConfigurator;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.PrintWriter;
import java.io.IOException;

public class Log4jInit extends HttpServlet {

  public void init() {
    String prefix =  getServletContext().getRealPath("/");
    String file = getInitParameter("log4j-init-file");
    // if the log4j-init-file is not set, then no point in trying
    if(file != null) {
      PropertyConfigurator.configure(prefix+file);
    }
  }

  public
  void doGet(HttpServletRequest req, HttpServletResponse res) {
  }
}

在web应用的web.xml中定义以下servlet:

&lt;servlet&gt; 
  &lt;servlet-name&gt;log4j-init&lt;/servlet-name&gt;  
  &lt;servlet-class&gt;com.foo.Log4jInit&lt;/servlet-class&gt;  
  &lt;init-param&gt; 
    &lt;param-name&gt;log4j-init-file&lt;/param-name&gt;  
    &lt;param-value&gt;WEB-INF/classes/log4j.lcf&lt;/param-value&gt; 
  &lt;/init-param&gt;  
  &lt;load-on-startup&gt;1&lt;/load-on-startup&gt; 
&lt;/servlet&gt;

写一个初始化servlet是初始化log4j最灵活的方法,并没有约束servlet的init()方法可以放置的代码。


Nested Diagnostic Contexts(嵌套诊断上下文)

 

大多数真实的系统必须同时处理多个客户端,在这种典型系统的多线程实现中,不同的线程将处理不同的客户端。记录日志特别适用于跟踪和调试复杂的分布式应用程序。区别一个客户端与另一个客户端日志输出的通用方法是为每个客户端实例化一个新的单独的logger,这促进了loggers的扩展并且提高了管理日志输出的开销。

更轻便的技术是为同一个客户端发起的每个日志请求做唯一的标识,Neil Harrison在R. Martin, D. Riehle, 和 F. Buschmann (Addison-Wesley, 1997)编写的《程序模式的设计语言3》中“日志诊断消息模式”中描述了这种方法。

 

为了唯一标识每个请求,用户将上下文信息推送到NDC(嵌套身段上下文的缩写)。NDC类如下所示:

public class NDC {
    // Used when printing the diagnostic
    public static String get();

    // Remove the top of the context from the NDC.
    public static String pop();

    // Add diagnostic context for the current thread.
    public static void push(String message);

    // Remove the diagnostic context for this thread.
    public static void remove();
}

每个线程将NDC作为一堆上下文信息进行管理。请注意,org.apache.log4j.NDC类的所有方法都是静态的。假设NDC打印打开,每次发出日志请求时,当前线程的日志输出相应的log4j组件将包含整个NDC堆栈,这是在没有用户干预的情况下完成的,用户只需通过在代码中几个明确定义的点使用push和pop方法来将正确的信息放在NDC中。相比之下,每个客户端的logger将会对代码进行大量更改。

 

为了说明这一点,让我们看一个servlet向多个客户端传递内容的例子。servlet可以初始化在执行代码之前且最初请求的地方,上下文信息可以是客户端的主机名和请求固有的其他信息,通常是Cookie中包含的信息。因此,即时servlet同时服务于多个客户端,仍然可以区分由相同代码发起的属于同一个logger的日志,因为每个客户端请求有不同的NDC堆栈,与传统将新实例化的logger传递给客户请求中执行的所有代码的复杂性相反。

 

然而,一些复杂的应用程序(如虚拟主机web服务器)必须根据虚拟主机上下文和发出请求的软件组件有所不同,最近的log4j版本支持多层次树,此增强功能允许每个虚拟主机拥有自己的logger层级结构副本。


Performance(性能)

 

反对日志记录经常引用的论据之一是其计算成本,这是一个合理的问题,即使中等大小的应用也可以产生数千个日志请求,测量和调整日志记录性能花费了大量的经历。Log4j声称快速灵活:速度第一,灵活性第二。

 

用户应该注意以下性能问题:
1.当日志关闭时的记录性能。

当完全关闭日志记录或仅关闭一组级别日志时,日志请求的成本由方法调用加上一个整数比较组成,在233 MHz Pentium II机器上,该成本通常在5至50纳秒范围内。

但是,方法调用涉及参数构建的“隐藏”成本。

例如,对于一些cat logger,写成

logger.debug("Entry number: " + i + " is " + String.valueOf(entry[i]));

导致构造参数的成本,即将整数i和entry[i]转换为字符串,并且连接中间字符串,不管该消息是否被记录,这种参数构建的成本可能相当高,这取决于所涉及参数的大小。

为了避免参数构造成本可以这样写:

if(logger.isDebugEnabled() {
	logger.debug("Entry number: " + i + " is " + String.valueOf(entry[i]));
}

如果禁用调试功能,则不会引起参数构造的花费。另一方面,如果记录器启用了调试器,则会产生两倍的判断logger是否可用的成本:一次再debugEnabled中,一次再调试中。这是一个无关紧要的开销,因为判断一个logger需要约1%的实际记录的时间。

某些用户使用预处理或编译时技术来编译所有日志语句,这样就可以提高日志记录的性能。但是,由于生成的应用程序二进制文件不包含任何日志语句,因此该二进制文件无法打开日志记录。在我看来,这是一个不成比例的花费来唤起很小的性能表现。

 

2.打开日志记录时决定是否记录日志的性能。

这本质上是logger层级结构的表现。当日志记录打开时,log4j仍然需要将日志请求的级别与请求logger的级别进行比较。然而,logger可能没有分配的级别,他们可以从logger层次结构继承它们。因此,在继承级别之前,logger可能需要搜索其祖先。

已经有大量的努力使层级结构运行的尽可能快。例如,子loggers只链接到它们存在的祖先,在前面显示的BasicConfigurator示例中,名为com.foo.Bar的logger直接链接到root logger,从而规避了不存在的com或com.foo logger。这显著提高了运行速度,特别是在有空缺的层级上。

层级结构的典型代价通常比完全关闭日志日志慢3倍。

 

3.实际输出日志消息

这是格式化日志并将其发送到目的的花费,再次,我们认真努力使layouts(格式化程序)尽可能快执行,appender是同样如此。实际记录日志典型的成本约为100至300微秒。

有关实际数据,请参阅:org.apache.log4.performance.Logging

 

尽管log4j有很多功能,但其第一个设计目标是速度。一些log4j组件重写了很多次以提高性能。不过,贡献者经常会提出新的优化。你应该很高兴的知道,当配置SimpleLayout性能测试显示记录日志log4j已经和System.out.println一样快。


Conclusions(结论)

 

Log4j是一个用JAVA编写的通用的日志记录包。其特征之一就是logger中集成的概念。使用logger层级结构,可以控制任意力度输出哪些日志语句,这有助于减少日志输出量,并最大限度的降低记录成本。

 

log4j API的优点之一是其可管理性。一旦日志语句被插入到代码中,它们就可以用配置文件来控制。它们可以选择性的启用或禁用,并以用户选择的格式发送到多个不同目的位置。log4j软件包的设计使得日志语句可以保留在发版代码中而不会导致巨大的性能成本。

回到顶部