Log4Net结构详解
当我们在描述为系统做日志这个动作的时候,实际上描述了3个点;做日志,其实就是在规定,在什么地方 用什么日志记录器 以什么样的格式做日志。把三个最重要的点抽取出来,即什么地方,日志记录器,什么格式。在Log4net中,就使用了三个最重要的组件来描述这三个要素,即Logger:日志记录器、Appender:什么地方、 Layout:什么格式。 下面我们就分别来看看这三个对象在Log4net中起的重要作用和一些基本的规则用法。
1、Logger
之前我们在创建Logger的时候,都是使用LogManager.GetLogger("seven");方法来得到一个类绑定的日志记录器的。实际上,当我们说把一个日志记录器绑定在一个类上,这种说法是不准确的,正确的说,我们仅仅是使用给定的类的全限定名为Logger取了一个名字。这里请大家注意一下,Logger都是有名字的,假如我们除开rootLogger不谈,我们可以把Logger想象成一张表里面的数据,Logger对应的名字就是其主键,当两个Logger的名字相同,这两个Logger就是同一个Logger实例。我们可以简单的通过一个实例来验证:
static void Main(string[] args) { log4net.Config.BasicConfigurator.Configure(); ILog log = LogManager.GetLogger("seven"); ILog log2 = LogManager.GetLogger("seven"); log.Debug(log == log2); Console.ReadKey(); }
控制台打印:30 [9] DEBUG seven<null> - true;说明Logger自身维护着每一个名字的Logger实例的引用,保证相同名字的Logger在不同地方获取到的实例是一致的,这样就允许我们在统一的代码中配置不同Logger的特性。
另外,Logger的层次结构,也是靠Logger的名字来区分的,比如:名称为DoNet的Logger就是DoNet.Seven的父Logger;DoNet.Seven是DoNet.Seven.Song的父Logger;Logger的体系结构和命名空间的结构划分类似,使用.来区分;所以我们前面才说,使用类的全限定名是最简单,也是最符合logger的体系结构的命名方式。当然,你也可能使用任何的能想到的方式去处理Logger的命名;这也是可以的。
看到这里,可能有的人会提出疑问,那既然Logger的层次结构是按照Logger的名字来创建的,那在创建Logger的时候,是否必须按照其结构来顺序创建Logger?比如:
ILog log = LogManager.GetLogger("DoNet");
ILog log = LogManager.GetLogger("DoNet.Seven");
ILog log = LogManager.GetLogger("DoNet.Seven.Song");
是否必须要按照这个顺序创建呢?不需要。在做前面的示例的时候,我们说到,大家可能认为当我们通过ILog log = LogManager.GetLogger("DoNet.Seven.Song");的时候,Log4J其实为我们创建了DoNet;DoNet.Seven和DoNet.Seven.Song这三个Logger;其实不然,如果是这样的话,Log4net可能会产生非常多不必要的Logger。所以,真正的方式应该是,当我通过ILog log = LogManager.GetLogger("DoNet.Seven.Song");的时候,Log4net仅仅为我们创建了DoNet.Seven.Song这个Logger;而当我们再次使用ILog log = LogManager.GetLogger("DoNet.Seven");的时候,Log4net才会为我们创建DoNet.Seven这个Logger,并且Log4net会自动的去寻找这个Logger的上下级关系,并自动的把这个新创建的Logger添加到已有的Logger结构体系中。
在Logger中,非常重要的一个组件,就是Logger的日志级别,即Level。关于Level的划分,使用,我们在前面的例子中已经大概了解到了,下面来看看完整的Level的定义和其在Logger体系中的继承方式,这是一个很简单的概念。
首先,在Log4net中,为我们默认的定义了7种不同的Level级别,即all<debug<info<warn<error<fatal<off;而这7中Level级别又刚好对应着Level类中的7个默认实例:Level.ALL;Level.DEBUG;Level.INFO;Level.WARN;Level.ERROR;Level.FATAL和Level.OFF。我们在之前也只看到了其中的debug到fatal这5种;这五种日志级别正好对应着Logger中的五个方法,即:
void Debug(object message);
void Info(object message);
void Warn(object message);
void Error(object message);
void Fatal(object message);
当然也对应着一些扩展方法,方便使用,这里不列举了。Log4Net学习【一】我们已经介绍了Logger的基本使用,以及Level的使用和限制。其实,在真正使用Log4net的时候,我们一般都不需要使用代码的方式去配置Level,这些配置更多的时候是使用配置文件来完成的,这个后面会详细介绍。但是,不管使用什么样的配置文件,最终也会解释成这样的配置代码,所以,理解了这些代码,再去使用配置文件,会更加清楚到底配置文件在干什么
2、Appender
使用Logger的日志记录方法,仅仅是发出了日志记录的事件,具体日志要记录到什么地方,需要Appender的支持。在Log4net中,Appender定义了日志输出的目的地。在上面所有的示例当中,我们日志输出的目的地都是控制台,在Log4j中,还有非常多的Appender可供选择,可以将日志输出到文件,网络,数据库等等,这个后面再介绍。说到这里,可能有人就已经会思考,既然Logger对象的info()等方法仅仅是发出了日志记录的事件,还需要指定输出目的地;那么我们之前的示例代码也并没有为任何一个Logger设置Appender啊?其实这很好理解,我们回顾一下之前的Level,按道理,也应该为每一个Logger指定对应的日志输出级别,但是我们也并没有这样做,正是因为Logger本身存在一个完整的体系结构,而Level能够在这个结构中自下而上的继承。同理,Appender也具有这种继承的特性.
log4net.Config.BasicConfigurator.Configure(); ILog log = LogManager.GetLogger("DoNet.Seven"); log.Logger.Repository.Threshold = Level.Info; #region 给log添加一个Append IBasicRepositoryConfigurator configurableRepository = log.Logger.Repository as IBasicRepositoryConfigurator; SimpleLayout layout=new SimpleLayout(); FileAppender appender = new FileAppender(layout, "test.log"); configurableRepository.Configure(appender); #endregion ILog log2 = LogManager.GetLogger("DoNet.Seven.Song"); log.Debug("debug"); log.Info("Info"); log.Error("Error"); log2.Debug("Debug2"); log2.Info("Info2"); log2.Error("Error2"); Console.ReadKey();
结果如下:
这里文件中的输出明显已经和控制台的输入不一致了,在这里说明了两个问题:
1、在Log4Net中,这个特性叫做Appender的追加性。默认情况下,所有的Logger都自动具有追加性,通过一个表来说明
Logger | addAppender | 起作用的Appender |
Root | A1 | A1 |
DoNet | A2 | A2,A1 |
DoNet.Seven | null | A2,A1 |
DoNet.Seven.Song | A3 | A3,A2,A1 |
但是,在某些情况下,这样做反而会引起日志输出的混乱。有些时候,我们并不希望Logger具有追加性。比如在上面这张表中,我们想让DoNet.Seven.Song只需要继承A2和自己的A3Appender,而不想使用root上面的A1 Appender,又该怎么做呢?
其实很简单,在Logger上,都有一个additivity属性,如果设置additivity为false,则该logger的子类停止追加该logger之上的Appender;如果设置为true,则具有追加性。修改一下上表:
Logger | addAppender | 起作用的Appender | additivity |
Root | A1 | A1 | true |
DoNet | A2 | A2 | false |
DoNet.Seven | null | A2 | true |
DoNet.Seven.Song | A3 | A3,A2 | true |
3、 Layout
Logger规定了输出什么日志,Appender规定了日志输出到哪里,当然,我们还会奢望,以什么样的方式输出日志。这就涉及到之前我们在观察Appender的时候创建ConsoleAppender和FileAppender都需要传入的Layout。在Log4net中,Layout对象提供了以什么样的方式格式化日志。这个对象是绑定在Appender之上的,一般在Appender创建的时候指定。
下面就简单看一个最常使用的Layout:PatternLayout。PatternLayout允许使用标准的输出格式来指定格式化日志消息的样式。举个简单的例子,可能之前大家看到的使用BasicConfigurator配置的输出的日志样式和我们使用SimpleLayout输出的样式不一样,这些Layout的输出样式都是默认的,我们也可以手动置顶一些format,正如下面这样:
log4net.Config.BasicConfigurator.Configure(); ILog log = LogManager.GetLogger("DoNet.Seven"); log.Logger.Repository.Threshold = Level.Info; #region 给log添加一个Append IBasicRepositoryConfigurator configurableRepository = log.Logger.Repository as IBasicRepositoryConfigurator; PatternLayout layout = new PatternLayout("%d[%t]%-5p %c [%x] - %m%n"); layout.ConversionPattern = PatternLayout.DefaultConversionPattern; FileAppender appender = new FileAppender(layout, "test.log"); configurableRepository.Configure(appender); #endregion ILog log2 = LogManager.GetLogger("DoNet.Seven.Song"); log.Debug("debug"); log.Info("Info"); log.Error("Error"); log2.Debug("Debug2"); log2.Info("Info2"); log2.Error("Error2");
文件中的样式是我们通过 PatternLayout layout = new PatternLayout("%d[%t]%-5p %c [%x] - %m%n");
这个Layout控制的,那么我们在创建Layout的时候传递了一个字符串,是什么意思呢?在这个模式中,有很多以%开头的参数,每一个特定的参数代表着一种日志的内容,这就是log4net.Layout.PatternLayout中的转换模式(ConversionPattern)
%m(message):输出的日志消息,如ILog.Debug(…)输出的一条消息
%n(new line):换行
%d(datetime):输出当前语句运行的时刻
%r(run time):输出程序从运行到执行到当前语句时消耗的毫秒数
%t(thread id):当前语句所在的线程ID
%p(priority): 日志的当前优先级别,即DEBUG、INFO、WARN…等
%c(class):当前日志对象的名称
%L:输出语句所在的行号
%F:输出语句所在的文件名
%-数字:表示该项的最小长度,如果不够,则用空格填充
例如,转换模式为%r [%t]%-5p %c - %m%n 的 PatternLayout 将生成类似于以下内容的输出:
176 [main] INFO org.foo.Bar - Located nearest gas station.
4、组件的使用
前面简单的了解了Log4J中最重要的3个组件,下面我们来看看Log4j是怎么使用这3个组件完成当我们调用logger.debug()方法能在控制台上打印出日志信息的。
第一步,继承数体系上的门槛检查:首先当调用info()方法后,Log4J会立刻使用该Logger所在的体系结构中设置的门槛去检查当前日志的级别。如果级别不够,立刻作废当前日志请求。
第二步,Level级别检查:使用当前Logger上设置的或者继承的Level级别来检查当前的日志级别。如果当前日志级别不够,立刻作废当前日志请求。
第三步,创建LoggingEvent对象:当日志审核通过,Log4J就会创建一个LoggingEvent对象(即日志事件对象)。在该对象中,会保存和本次日志相关的所有参数信息,包括日志内容,日志时间等。
第四步,执行Appender:当创建完成LoggingEvent对象时候,会该对象交给当前logger上起作用的所有的Appender对象,并调用这些对象的doAppend方法来处理日志消息。
第五步,格式化日志消息:接下来,会使用每一个Appender绑定的Layout对象(如果有)来格式化日志消息。Layout对象会把LoggingEvent格式化成最终准备输出的String。
第六步,输出日志消息:当得到最终要输出的String对象之后,appender会把字符输出到最终的目标上,比如控制台或者文件。
三个主要组件的组成结构:
日志执行流程图: