第二章:logback架构
[TOC]
官方英文文档地址
logback架构
logback被设计的足够通用以承载不同情况下的使用。logback设计为三个模块:logback-core,logback-classic和logback-access。
logback-core实现了其他两个模块的基础需要。classic模块继承自core模块,显著改进了log4j。classic继承并实现了SLF4J接口。所以你可以很容易的从其他日志系统切换到logback。
接下来我们将通过例子使用logback-classic 模块。
Logger,Appenders和Layouts
logback中包含主要的三个组件(components):Logger
, Appender
and Layout
。这三个组件一起为开发者提供了通过多种类型、级别以及在运行时格式化输出日志的能力。
Logger类包含在 logback-classic 模块中,Appender和Layout类被包含在logback-core模块中。作为一个通用的模块,logback-core不包含任何关于logger的概念。
Logger context
绝大多数的基于System.out.println都提供了开关某些类的日志,而输出另一些类的日志的功能。这种设计主要对日志空间(可能输出的语句)通过条件语句进行分类。而在logback-classic的实现中,这种分类是从logger记录器内部固有的分类。每一个logger记录器都连接到LoggerContext,LoggerContext负责生成logger对象并将这些logger记录安排在一个梳妆层次结构中。
loggers被实体对象命名,区分大小写。并且它们遵循分层命名规则:
分层命名
一个logger的名称后跟一个点是后代logger的名称的前缀,则称该logger是另一个logger的祖先(ancestor)。
一个logger是另一个logger的祖先(ancestor),并且他们之家没有其他的祖先,则称该logger是另一个logger的parent,后一个是前一个的child.
例如,名为“com.foo”的logger是“com.foo.Bar”logger的parent。同样“java”logger是“java.util.Vector”的祖先(ancestor)。
root logger位于logger层级结构顶端。像其他logger一样,root logger可以通过其名称如下方式获取实例:
Logger rootLogger = LoggerFactory.getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME);
其他的所有logger都可以通过 LoggerFactory.getLogger(LOGGER_NAME)把需要获取的logger名字作为参数获取实例,logger接口(interface)的一些基本的方法如下:
package org.slf4j;
public interface Logger {
// 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);
}
有效级别(Effective level) 和 级别(level)继承
logger会被分配一个级别(level),可用的级别是TRACE, DEBUG, INFO, WARN and ERROR。level被定义在ch.qos.logback.classic.Level类中。
提示:在logback中,level被是final的不能细分子类的。更多灵活的方法被定义在Marker
中。
如果一个给定的logger没有指定level,它会使用最近的祖先被指定的级别。
为了保证所有的logger可以被指定一个级别,root logger通常总是会被指定一个级别,如果不指定,默认的是DEBUG级别。
下面是3个示例(原文4个例子,这里省略了一个):
示例1:
logger name | 指定级别(assigned level) | 生效级别(Effective level) |
---|---|---|
root | DEBUG | DEBUG |
X | none | DEBUG |
X.Y | none | DEBUG |
X.Y.Z | none | DEBUG |
示例2:
logger name | 指定级别(assigned level) | 生效级别(Effective level) |
---|---|---|
root | DEBUG | DEBUG |
X | INFO | INFO |
X.Y | DEBUG | DEBUG |
X.Y.Z | WARN | WARN |
示例3:
logger name | 指定级别(assigned level) | 生效级别(Effective level) |
---|---|---|
root | DEBUG | DEBUG |
X | INFO | INFO |
X.Y | none | NFO |
X.Y.Z | ERROR | ERROR |
输出(printing )方法和基本的选择规则
按照定义,输出方法顶一个一个日志需要输出的级别。例如L.info定义了L的输出级别是INFO.
一个logging 输出方法级别高于等于当前定义的级别,则会有效输出,否则无效。
这个级别的基本规则是:TRACE < DEBUG < INFO < WARN < ERROR
。
下面通过一个实例代码解释以上规则
import ch.qos.logback.classic.Level;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
// 获取一个 "com.foo" logger实例
Logger logger = LoggerFactory.getLogger("com.foo");
// 定义该logger的输出级别是INFO
logger.setLevel(Level. INFO);
Logger barlogger = LoggerFactory.getLogger("com.foo.Bar");
// barlogger是"com.foo"的子logger,所以继承了其输出级别INFO,
// 这里的输出级别是warn,WARN >= INFO 所以这里的输出有效
logger.warn("Low fuel level.");
// DEBUG < INFO. logger的输出级别是INFO,所以该输出无效
logger.debug("Starting search for nearest gas station.");
//
// barlogger从logger继承到INFO输出级别,INFO >= INFO. 该输出有效
barlogger.info("Located nearest gas station.");
// DEBUG < INFO ,该输出无效
barlogger.debug("Exiting gas station search");
Retrieving(检索) Loggers
通过传入相同的名字调用 LoggerFactory.getLogger()
方法总是会返回同一个对象实例。
Logback可以按照软件类的继承分层关系记录日志,通常使用类名,
开发这可以根据需要自定义记录器名称。但是按照类名记录仍旧是目前已知的最佳实践。
Appenders and Layouts
logback可以像上面举例的选择性开启或者关闭一部分logger输出信息,可以允许log输出到不同的目的地。
logback的术语中一个输出的目的地被称为一个appender.目前appender支持console, files, remote socket servers, MySQL, PostgreSQL, Oracle 和other databases, JMS, and remote UNIX Syslog daemons(守护进程)。
logbakc支持同时定义多个appender。
addAppender
方法可以向logger中添加appender。appender和输出level一样都是可以从树状分层结构中的ancestor logger继承的。举例:如果一个console appender被添加到root logger,那么所有的有效的logger输出都会至少输出到console。可以通过设置logger的additivity标志为false来关闭这种默认行为
Appender的可加性
logger L 的输出请求将输出到L logger及其ancestor logger的所有appender中。默认情况additivity标志为true,即开启这种累加性质。
举例
logger name | attached Appenders | Additivity | Output Targets | 备注 |
---|---|---|---|---|
root | A1 | (不适用)not applicable | A1 | 因为root logger是树状结构的根节点,因此设置additivity属性是无效的。 |
x | A-x1,A-x2 | true | A1,A-x1,A-x2 | root的appender和x的appender |
x.y | none | true | A1,A-x1,A-x2 | root的appender和x的appender |
security | A-a | false | A-a | additivity为false,所以只security的appender |
ssecurity.access | none | true | A-a | 因为security的additivity是false。所以只继承了sercurity的appender,不累加security的ancestor logger的appender |
上面讲了如何定义输出目的地,下面讲如何输出格式化,输出格式化是通过layout类和appder关联来实现的(appder配置中中定义对一个layout)。PatternLayout是logback标准输出的一部分,可以让用户使用类似c语言中类似printf一样的格式化输出。
规则"%-4relative [%thread] %-5level %logger{32} - %msg%n"会输出类似下例的字符串
176 [main] DEBUG manual.architecture.HelloWorld2 - Hello world.
第一个字段显示的是从应用启动开始的毫秒信息;第二个字段显示的是线程名;第三个字段是输出的level;第四个字段是logger的名字,“-” 后面的是日志输出的内容。
参数化的日志记录(Parameterized logging)
在classic组件中的logger实现了定义在SLF中的logger接口。某些输出方法能输出多个参数,这些输出方法主要是为了在对可读性影响最小的情况下提高性能。
如一些logger这样写
logger.debug("Entry number: " + i + " is " + String.valueOf(entry[i]));
不论日志是否最终不可用而被忽略还是可用而被处处都会引发字符串合并构造:i
和entry[i]
转成String类型,并将"Entry number: " + i + " is " + String.valueOf(entry[i])
连接成字符串。
避免这种情况的一种解决方案是通过flag开关来筛选,如下例:
if(logger.isDebugEnabled()) {
logger.debug("Entry number: " + i + " is " + String.valueOf(entry[i]));
}
这种方式并不好,debug开关被检验了两次,在实践中,这样的操作一般会浪费大约1%的时间。
更好的一种logger用法选择(alternative)
entry是一个object,你可以这样写:
Object entry = new SomeObject();
logger.debug("The entry is {}.", entry);
这种写法在输出级别不可用的情况下(级别被覆盖)不会导致强转和字符串拼接。
下面两种写法,如果debug级别的输出是不可用的,那么第二种写法的性能至少优于第一种写法的30倍。
logger.debug("The new entry is "+entry+".");
logger.debug("The new entry is {}.", entry);
这种方式你可以输出两个参数,例:
logger.debug("The new entry is {}. It replaces {}.", entry, oldEntry);
如果要输出更多的参数,那么用object[]数组的方式也可以输出
Object[] paramArray = {newVal, below, above};
logger.debug("Value {} was inserted between {} and {}.", paramArray);
内部实现一瞥
前面我们介绍logback的三组件,下面我们根据这些分析下名为com.wombat的logger的info函数的调用过程:
1.获取过滤链返回值
TurboFilter
链(chain)如果存在,则会被调用。Turbo 过滤器可以设置一个上下文过滤的范围,或者基于确定的事件(如 Marker
, Level
, Logger
,消息等)过滤掉一些信息。
如果过滤链返回 FilterReply.DENY
,则这个日志输出请求会被丢弃;如果返回FilterReply.NEUTRAL,那么会进行第二步;如果返回是 FilterReply.ACCEPT
,则跳过第二步,直接进入第三步。
2.应用基础选择规则
在这一步中,logback比较请求输出的输出级别和实际生效的输出级别,如果请求输出级别低于实际输出级别,那么丢弃本地输出请求,如果高于则继续到下一步。
3.创建一个LoggingEvent对象
如果进行到这一步,logback会创建ch.qos.logback.classic.LoggingEvent
对象,对象中包含所有的输出请求参数,例如logger信息、level信息、输出的消息体本身、异常、当前时间、当前线程、类的信息等。
注意,这里的一些字段实体是懒加载(initialized lazily)的,只有在它们真的被需要的时候才会加载。
4.调用appenders
在创建loggingEvent对象后,logback会调用doAppend()方法来加载从之前讲的appender累加规则中记录的所有appenders。
logback的所有发行版中都会实现继承AppenderBase
抽象类。该类继承了doappend()方法, 这个方法是被包含在synchronized 块中,以确保线程安全。这个方法会调用附加在appender上的自定义过滤器。
5.格式化输出
被调用的appender负责格式化日志输出。然而,一些(并非全部)appenders把这项任务委派给layout,最终layout格式化消息体并输出String。
提示,一些appender并不转化消息体为string,如SocketAppender,它序列化消息体,因此SocketAppender并不需要layout.
6.发送LoggingEvent
最终,消息会被发送到各自的目的地,这里有一张uml图:性能(performance)
经常有人反对记录日志的一个论据是日志的性能开销,这是一个合理的问题,即使在一个中等规模的应用中也会生成上千条的日志记录。我们的大部分开发工作不应该花费在调试logback性能上,但是用户应该了解一些基本的性能问题
1.完全关闭时的性能开销
你可以在root logger中设置level为level.OFF来关闭日志记录。这样每次logger输出的花销包括方法调用和证书比较。在3.2Ghz的奔腾D机器上这样的一次开销一般是20纳秒。
当然如上面讲的你应该使用第二种记录方式,否则会产生拼接字符串,转换变量等而外开开销
x.debug("Entry number: " + i + "is " + entry[i]);
x.debug("Entry number: {} is {}", i, entry[i]);
2.是否记录日志的这个决定的性能开销
在logback中不需要遍历logger树结构来查询其level,在ancestor logger更改level的时候会通知child logger更改其level,因此一个logger直接知道自身的level。
3.实际记录(格式化并写入输出设备)
logback做了一系列的优化来确保layouts(formatters)尽量快的格式化数据。对于appender同样如此。这里举例一个日志输出请求到本机的文件中一般会花费9到12纳秒,记录到远程的数据库需要花费几毫秒。