Logback手册 Chapter 2: Architecture(LogBack的结构)
英文地址点击打开链接,大家可以对比英语原文。
为了应用不同的环境,logback的基础结构合乎常规。
logback分为三个模块:logback-core,logback-classic以及logback-access。
1>核心模块(core)为其他两个模块提供基础。
2>classic模块继承自core。classic模块明显当于log4j的增强版。classic原生的实现了SLF4J API,因此你可以很容易的在LogBack和其他的日志系统比如log4j或java.util.logging转换。
3>第三个模块access结合Servlet容器来提供HTTP-access日志功能,这个会有专门的文章来介绍。
在下文里,用“lockback”指代logback-classic。
Logger,Appender,和Layouts
Logback建立在三个重要的类之上:Logger,Appender和Layout。这个三个类同心协力地让发者能根据信息的类型和级别来打印日志,并能控制日志的输出格式和输出目的地。
一方面,“Logger”类属于logback-classic模块。而另一方面,“Appender”和“Layout”接口属于logback-core模块。作为通用的模块,logback-core并没有定义日志类。
Logger.context
对于任何比普通的System.out.println高级的日志API,其首要且最重要的的优点是在屏蔽不同日志输出的不妨碍其他的日志输出。
这种能力是根据开发者的选择,对不同的日志隔离。在logback-classic,这个隔离是logger固有的。LoggerContext用来像树的层次一样构建Logger,从而使每一个单独的
Logger都绑定到一个LoggerContext上。
Logger是被命了名的实体(entity),他们的命名区分小写并遵循如下命名规则:
命名规则:
一个Logger如果点号后面的名字是另一个子孙Logger的前缀,这个Logger就是另一个Logger的的父级。
举个栗子:
"com.foo"Logger是"com.foo.Bar“Logger的父级。类似的,"java"是的 "java.util" 父级,是"java.util.Vector"的祖先级。这种命名方案大多数开发者非常的熟悉。
根Logger位于Logger的最高层,是每一个Logger书的顶层。通每一个logger一样,它可以像下面这样通过名字来获得。
Logger rootLogger=LoggerFactory.getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME);
所有其他的loggers同样用org.slf4j.LoggerFactory类的静态方法getLogger来获取,这个方法的参数是logger的名字。
Logger接口里定义的一些简单接口如下:
package org.slf4j;
public interfaceLogger {
// Printing methods:
public void trace(String message);
public void debug(String message);
public void info(String message);
public void warn(String message);
public void error(String message);
}
Log的级别
Logger可以设置级别。可选的级别定义在在ch.qos.logback.classic.Level,有TRACE, DEBUG, INFO, WARN 和 ERROR等级别。
注意,在logback,Level类是final的,不可继承,它以标记类的形式,提供灵活性。
如果一个Logger没有分配一个level,它会继承离它最近的祖先所分配的等级。
更正式的表述:
对于一个Logger L,其等级是它所在继承树里第一个不为空的level,从它本身开始到根Logger结束.
为了保证所有的Logger最终都绑定一个level,根Logger(rootLogger)一定绑定一个level,默认是”DEBUG“
下面是根据level的继承规则,不同的level绑定后,最终生效的level
例子1
Logger name |
Assigned level |
Effective level |
root |
DEBUG |
DEBUG |
X |
none |
DEBUG |
X.Y |
none |
DEBUG |
X.Y.Z |
none |
DEBUG |
例子1可以看出,只有root分配了一个level。其level值为DEBUG,被其他logger X, X.Y 和X.Y.Z继承。
例子2
Logger name |
Assigned level |
Effective level |
root |
ERROR |
ERROR |
X |
INFO |
INFO |
X.Y |
DEBUG |
DEBUG |
X.Y.Z |
WARN |
WARN |
例子2里,所有的Logger都分配了一个level,则未出现继承level的情况。
例子3
Logger name |
Assigned level |
Effective level |
root |
DEBUG |
DEBUG |
X |
INFO |
INFO |
X.Y |
none |
INFO |
X.Y.Z |
ERROR |
ERROR |
Logger root,x,以及x.y.z 被分别分配了级别为DEBUG,INFO和ERROR。Logger x.y和x.y.z从让他们最近的分配
了level的祖先x那里继承了level。
例子4
Logger name |
Assigned level |
Effective level |
root |
DEBUG |
DEBUG |
X |
INFO |
INFO |
X.Y |
none |
INFO |
X.Y.Z |
none |
INFO |
在例子4中,Logger root和x分别分配了level 为DEBUG和INFO。Logger x.y.z从离他们最近的分配了level的x那里继承了level。
打印方法以及选择打印的规则
按照定义,打印方法决定了打印请求的级别。比如说,如果L为一个Logger实例,则L.info("")便是一个INFO级别的打印。
如果日志打印请求的级别高于Logger设置的生效级别,则打印请求有效。反之则不。如前所述,一个没有分配level的Logger将继承离它最近的设置了level的祖先的level。此规则总结如下:
打印选择规则
对于一个生效level为Q的Logger,处理一个level为P的日志打印请求,如果P>=Q,则打印生效。
这条规则是logback的核心,它规定的级别顺序如下:
TRACE<DEBUG<INFO<WARN<ERROR
用一个更图像化的方式,来说明打印选择的规则是如何起作用的,在下表,列标题是日志打印请求的级别,设为P,行标题为Logger的生效级别,设为Q。其它表格内容为对应的选择打印的结果。
level of request p |
effective level q |
|||||
TRACE |
DEBUG |
INFO |
WARN |
ERROR |
OFF |
|
TRACE |
YES |
NO |
NO |
NO |
NO |
NO |
DEBUG |
YES |
YES |
NO |
NO |
NO |
NO |
INFO |
YES |
YES |
YES |
NO |
NO |
NO |
WARN |
YES |
YES |
YES |
YES |
NO |
NO |
ERROR |
YES |
YES |
YES |
YES |
YES |
NO |
下面是一个展示打印选择规则的例子
import ch.qos.logback.classic.Level; import org.slf4j.Logger; import org.slf4j.LoggerFactory; .... // get a logger instance named"com.foo". Let us further assume that the // logger is of type ch.qos.logback.classic.Logger so that we can // set its level ch.qos.logback.classic.Logger logger = (ch.qos.logback.classic.Logger)LoggerFactory.getLogger("com.foo"); //set its Level to INFO. The setLevel()method requires a logback logger logger.setLevel(Level. INFO); Logger barlogger = LoggerFactory.getLogger("com.foo.Bar"); // This request is enabled, because WARN>= INFO logger.warn("Low fuel level."); // This request is disabled, because DEBUG< INFO. logger.debug("Starting search fornearest gas station."); // The logger instance barlogger, named"com.foo.Bar", // will inherit its level from the loggernamed // "com.foo" Thus, the followingrequest is enabled // because INFO >= INFO. barlogger.info("Located nearest gasstation."); // This request is disabled, because DEBUG< INFO. barlogger.debug("Exiting gas stationsearch");
运行结果为:
17:33:51.028 [main] WARN com.foo - Low fuel level.
17:33:51.030 [main] INFO com.foo.Bar - Located nearest gas station.
获得 Logger
以同样的名称触发LoggerFactory.getLogger方法永远会返回同一个Logger实例。
例如,在
Logger x =LoggerFactory.getLogger("wombat"); Logger y =LoggerFactory.getLogger("wombat");
x和y实际上是同一个Logger实例。
因此,如果在代码的其它地方没有传递引用,创建的Logger也可能是同一个实例。生物学遗传的规律是父代总是先于他们的子孙出现,但logback与之相反。logback可以在以任何顺序被创建。
特别的,一个“父级”Logger即使后于子孙被创建,也将会发现并关联它自己的子孙。
logback的配置在程序开始的时候初始化。首选的方法是读取配置文件,这个方式我会简单说明。
logback使得命名Logger简单化。可以在每个类里实例化Logger的时候,通过将每个类的全名作为Logger的名字来完成。
这个方法来定义Logger有效、直接。输出日志的时候,这种命名将标识一个日志的来源变得简。然而,这是唯一可行的,虽然常见,直接的命名策略。
logback没用限制Logger的产生组合。作为一个开发者,你可以随心所欲的去命名Logger。不过,根据类的位置来命名Logger是目前为止已知的最好的惯用策略了。
Appenders and Layouts
让Logger有选择的使日志打印请求能否放行的能力只是冰山一角。Logback允许日志请求打印到多个目的地。
在logback里,一个日志的输出目的地称为Appender。
目前,Appender可以是控制台, 文件,远程socket服务器,MySQL, PostgreSQL, Oracle和其他数据库以及JMS,和远程的UNIX Syslog进程(UNIX Syslog daemons)
一个Logger可以绑定多个Appender。addAppender方法可以添加一个Appender到一个Logger。
每一个放行的打印日志请求会指向所在Logger所有的Appender,同时Appender的层级比较高。换言之,Appender的继承附属于logger的的层级。
例如,如果一个控制台Appender添加到得了root Logger(根Logger),那么所有放行的日志打印请求将至少会打印在控制台上。如果另外一个文件Appender添加到了一个Logger,我们设它为L,随之L和L的子孙允许的日志打印请求将会打印到文件和控制台。
也可以通过设置additivity flag为false来覆盖它的默认设置让Appender的添加不再是全部累加。
控制Appender增加的规则总结如下:
Appender增加一个日志L的输出将会到达所有L以及L的祖先的Appender,我们称之为"Appender的积累性"
然而,如果L的一个祖先,我们设为P,将additivity flag 设置为false,这样L的输出将会到达所有L以及L祖先的Appender包括P但是除了P的祖先的Appender。
additivity flag默认为true。
例子如下面的表
Logger Name |
Attached Appenders |
Additivity Flag |
Output Targets |
Comment |
root |
A1 |
not applicable |
A1 |
Since the root logger stands at the top of the logger hierarchy, the additivity flag does not apply to it. |
x |
A-x1, A-x2 |
true |
A1, A-x1, A-x2 |
Appenders of "x" and of root. |
x.y |
none |
true |
A1, A-x1, A-x2 |
Appenders of "x" and of root. |
x.y.z |
A-xyz1 |
true |
A1, A-x1, A-x2, A-xyz1 |
Appenders of "x.y.z", "x" and of root. |
security |
A-sec |
false |
A-sec |
No appender accumulation since the additivity flag is set to false. Only appender A-sec will be used. |
security.access |
none |
true |
A-sec |
Only appenders of "security" because the additivity flag in "security" is set to false. |
与往往不同的是,我们不仅是想制定日志的输出目的地,而且还想制定它的格式。这就需要将一个layout绑定一个Logger来完成。layout负责根据用户的意愿来格式化日志打印请求的输出。而Appender则负责将格式化的输出送到目的地。
PatternLayout作为logback的标准组成,使得用户根据匹配( Pattern )指定输出格式,就像C语言的printf方法。
比如说,PatternLayout具有这样的匹配
"%-4relative [%thread] %-5level%logger{32} - %msg%n"
将会输出如下结果:
176 [main] DEBUG manual.architecture.HelloWorld2 - Hello world.
第一个字段是从程序开始的的毫秒。
第二个字段是日志发出打印请求的线程。
第三个字段是日志打印请求的级别。
第四个字段是日志请求相关的Logger名。
'-'后面是的日志打印的信息
参数化的日志输出
logback给定的Logger继承自SLF4J's Logger interface,某个打印方法可以有不止一个参数。
这些打印方法的变体主要是在兼顾代码可读性的同时提高性能。
对于某个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])); }
采用这个方法,如果debugging对于Logger无效,则构建参数的消耗不会产生。
换句话说,如果Logger允许了DEBUG这个级别,你将会产生判断Logger是否放行DEBUG级别的消耗,有两次,一次在debugEnabled,一次在debug。
实际上,这个开销微不足道,评估一个Logger用的时间消耗不到打印一条日志请求的1%。
更好的选择
对于日志格式有一个方便的替代。假设entry是一个对象,你可以这样写:
Object entry = new SomeObject(); logger.debug("The entry is {}.",entry);
只有日志能否打印 被评估且被允许后,Logger才会构建信息并用entry的字符串值替换“{}”。
换句话说,这种方式在日志不被允许的时候不会产生构建参数的消耗。
下面两行产生相同的输出。但是如果日志状态为无效,第二个将会比第一个至少高效30个点。
logger.debug("The new entry is"+entry+"."); logger.debug("The new entry is{}.", entry);
两个参数同样可以,你可以这样写
logger.debug("The new entry is {}. Itreplaces {}.", entry, oldEntry);
如果要传递三个火三个以上的参数,同样可以传递一个变量Object[],就像这样:
Object[] paramArray = {newVal, below, above}; logger.debug("Value {} was insertedbetween {} and {}.", paramArray);
底层一瞥
在介绍完logback必要的组件之后,我们现在要看看logback处理用户使用Logger的打印方法时的详细步骤。
我们现在分析一下,当我们调用一个名为“com.wombat”的Logger的info()方法时,logback的走了几步。
1>得到过滤链(filter chain)的判断结果
如果有决策过滤链的话,将触发TurboFilter。Turbo过滤器可以设置一个context-wide阈值,或者过滤掉建立在信息之上的事件,比如Marker,Level,Logger,
message。或者与每个日志请求相关联的Throwable。如果过滤链的的处理结果是FilterReply.DENY,那么日志打印请求被拒绝。如果是FilterReply.NEUTRAL,则进行下一步,比如第二步。一旦结果是FilterReply.ACCEPT,则直接放行到第三步。
2>使用选择输出规则
在这一步,logback比较Logger的level和日志打印请求的level。如果日志打印请求被禁行,logback将会直接丢掉日志打印请求。否则将会到下一步。
3>创建一个LoggingEvent对象
如果打印请求通过了上面两个步骤,logback将会创建一个包含所有这个请求的相关参数的ch.qos.logback.classic.LoggingEvent 对象,比如请求的Logger,请求的级别,和信息本身,伴随着请求的异常,当前时间,当前线程,说明这个请求和MDC的类的各种的数据。需要注意的是,除非是他们真正在被需要的时候,否则一些字段初始化的时候比较“懒”。
MDC用来以额外的上下文信息来修饰日志请求,我们将在下章讨论。
4>激活Appender
在建立完一个LoggingEvent对象后,logback将会触发所有有效的appender的doAppend()方法,这就是,Appender从Logger上下文继承而来的。
所有的继承了AppenderBase类且实现了doAppend方法的的Appender以在同步块中来保证线程安全像船一般运载logback的分发。
AppenderBase的doAppend方法也会触发定制的绑在Appender上的存在的过滤器filter。可以动态绑定到热河Appender上的定制的过滤器filter,将会在一个单独章节来讲。
5>格式化输出
被激活的Appender负责格式化日志事件。但是有一些(并非全部)Appender委托layout来格式化日志事件。layout格式化日志实例并返回一个字符串结果。
需要注意的是一些Appender,比如说SocketAppender,没有将日志时间转化为一个字符串,而是将其初始化。因此他们不需要layout
6>发送日志事件
被格式化后,日志事件会被每一个Appender分发到它的目的地。
下面的UML视图展示了每个环节的工作。
性能
一个经常被提到并用来反对日志打印的说法是它的计算消耗。这是个合理的出发点,因为即使中等的程序也可能产生数以千计的日志事件。
我们大多数的努力用在了测试和调整logback的性能。在这些努力之后,用户仍需要注意下面的性能要点。
1>完全关闭日志
你可以设置根日志root logger等级为Level.OFF来完全关闭日志。当日志完全关闭,一个日志请求的消耗包括方法的调用和一个整数判断。在3.2GHz的奔腾 D机器上这个消耗大约是20纳秒。
但是任何方法调用,隐含着“隐秘”的构建参数的消耗。比如某个logger x的输出 ,
x.debug("Entry number: " + i +"is " + entry[i]);
导致了构建消息参数的消耗。换言之,不管这条日志生效与否,都要将i和entry[i]转化为一个字符串,并和中间的字符串连接起来。
根据参数的数量,构建参数的消耗可以很高。为了避免构造参数的消耗,你可以利用SLF4的参数化日志打印:
x.debug("Entry number: {} is {}",i, entry[i]);
这个写法不会引发构造参数的消耗。与前面的debug()方法的调用相比,它很大程度上更快。
消息只在打印请求送到绑定的Appender才会被格式化,此外,格式化消息的组件是非常的优化了的。
尽管上述日志在的一个“紧密的环”中,也就是非常频繁的调用代码,是个双输的策略,可能导致性能降低。
即使日志被关闭,频繁的调用日志也会降低你的程序的性能,如果日志是打开的,则会产生大量的(而且没用)的输出。
2>当日志被打开的时候,决定是否打印的性能。
在logback,没有必要遍历日志层级树。当一个Logger被创建的时候,它知道它的有效等级(也就是它的等级,当考虑到等级继承的时候)
当父级Logger的等级变化的时候,所有的有关的子级Logger会收到通知。
因此在接受或拒绝一个基于生效等级的请求时,Logger可以瞬间做出选择,而不必咨询它的祖先。
3>实际的日志打印(格式化和往目标设备写入)
这是格式化日志输出和发写入到输出目标的消耗。
在这儿,为了使layout(format格式化)尽可能的快做了一系列的努力。对于Appender也是如此。对于完整的日志输出,当打印到本地时,典型的消耗是9到12微秒。如果打是输出到数据库或者是远程服务器会再高几个微秒。
纵使功能丰富,logback最重要的设计目标是执行速度。第二个目标是可靠性,为了提高性能,一些logback的组件已经被重写过多次。