Logback的架构
Logback的基本体系结构足够通用,可以在不同情况下应用。目前,logback分为三个模块:logback-core,logback-classic和logback-access。
core模块为其他两个模块奠定了基础。classic模块扩展了core。classic模块对应于log4j的一个显著改进的版本。logback -classic本机实现SLF4J API,因此您可以轻松地在logback和其他日志系统(如log4j或java.util)之间来回切换。日志记录(JUL)在JDK 1.4中引入。名为access的第三个模块与Servlet容器集成,提供HTTP-access日志功能。一个单独的文档包含访问模块文档。
在本文的其余部分中,我们将编写“logback”来引用logback-classic模块。
Logger, Appenders 和 Layouts
Logback建立在三个主要的类上:Logger、Appender和Layout。这三种类型的组件协同工作,使开发人员能够根据消息类型和级别记录消息,并在运行时控制这些消息的格式和报告位置。
Logger类是logback-classic模块的一部分。另一方面,Appender和Layout接口是logback-core的一部分。作为一个通用模块,logback-core没有日志记录器的概念。
Logger context
与一般日志记录API相比,任何日志记录API的第一个也是最重要的优势 System.out.println在于它能够禁用某些日志语句,同时允许其他语句不受阻碍地进行打印。此功能假定根据某些开发人员选择的标准对日志记录空间(即所有可能的日志记录语句的空间)进行了分类。在logback-classic中,此分类是记录器的固有部分。每个单独的记录器都附加到一个LoggerContext,后者负责制造记录器并将它们排列成树状层次结构。
日志记录器是命名的实体。他们的名字是大小写敏感的,他们遵循分层命名规则:
命名的层次结构
如果一个记录器的名称后跟一个点,则该记录器是另一个记录器的祖先,该后跟点的名称是该子记录器名称的前缀。如果记录器与子记录器之间没有祖先,则称该记录器为子记录器的父项。
例如,名为“com.foo”的日志程序是名为“com.foo.Bar”的日志记录器的父进程。类似地,“java”是“java.util”的父类以及“java.util.Vector”的祖先。大多数开发人员应该熟悉这种命名方案。
根记录器驻留在记录器层次结构的顶部。它的特殊之处在于,它一开始就是每个等级制度的一部分。像每个日志记录器一样,可以通过它的名称检索它,如下所示:
Logger rootLogger = LoggerFactory.getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME);
还使用org.slf4j.LoggerFactory 类中的类静态getLogger
方法检索所有其他记录器 。此方法将所需记录器的名称作为参数。 下面列出了Logger接口中的一些基本方法。
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);
}
有效级别又名级别继承
可以为记录器分配级别。在类中定义了可能的级别集合(TRACE,DEBUG,INFO,WARN和ERROR) ch.qos.logback.classic.Level。请注意,在logback中,Level该类是最终的,不能进行子类化,因为以Marker对象的形式存在一种更为灵活的方法。
如果没有为给定的记录器分配一个级别,那么它将从其最接近的祖先那里继承一个分配的级别。更正式地:
给定日志记录器L的有效级别等于其层次结构中的第一个非空级别,从L本身开始,在层次结构中向上直到根日志记录器。
为了确保所有记录器最终都可以继承级别,根记录器始终具有分配的级别。默认情况下,此级别是DEBUG。
以下是四个示例,这些示例具有各种分配的级别值以及根据级别继承规则得出的有效(继承)级别。
示例1
Logger name | Assigned level | Effective level |
---|---|---|
root | DEBUG | DEBUG |
X | none | DEBUG |
X.Y | none | DEBUG |
X.Y.Z | none | DEBUG |
在上面的示例1中,仅为根记录器分配了一个级别。该级别值,DEBUG由其他记录器继承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中,所有记录器都有一个分配的级别值。级别继承不起作用。
示例3
Logger name | Assigned level | Effective level |
---|---|---|
root | DEBUG | DEBUG |
X | INFO | INFO |
X.Y | none | INFO |
X.Y.Z | ERROR | ERROR |
在上面示例3中,记录器root, X和X.Y.Z被分配了等级 DEBUG,INFO和ERROR 分别。Logger X.Y从其父级继承其级别值X。
示例4
Logger name | Assigned level | Effective level |
---|---|---|
root | DEBUG | DEBUG |
X | INFO | INFO |
X.Y | none | INFO |
X.Y.Z | none | INFO |
在上面的示例4中,分别为记录器root和 X和分配了级别DEBUG和 INFO。记录器X.Y并 X.Y.Z从其最近的父X级(已分配级别)继承其级别值。
打印方法和基本选择规则
根据定义,打印方法确定日志记录请求的级别。例如,如果L是logger实例,则该语句L.info("..")是INFO级别的日志记录语句。
如果日志记录请求的级别高于或等于记录器的有效级别,则认为该请求已启用。否则,该请求被称为已禁用。如前所述,没有分配级别的记录器将从其最近的祖先那里继承一个。该规则总结如下。
基本选择规则
如果p> = q,则 向具有有效级别q的记录器发出级别p的日志请求。
此规则是注销的核心。假定级别按以下顺序排序: TRACE < DEBUG < INFO < WARN < ERROR。
以更图形化的方式,这是选择规则的工作方式。在下表中,垂直标题显示记录请求的级别,由p表示,而水平标题显示记录器的有效级别,由q表示。行(级别请求)和列(有效级别)的交集是根据基本选择规则得出的布尔值。
下面是一个基本选择规则的例子。
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 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 >= INFO.
barlogger.info("Located nearest gas station.");
// This request is disabled, because DEBUG < INFO.
barlogger.debug("Exiting gas station search");
Retrieving Loggers
LoggerFactory.getLogger
用相同名称 调用方法将始终返回对完全相同的记录器对象的引用。
例如,在
Logger x = LoggerFactory.getLogger("wombat");
Logger y = LoggerFactory.getLogger("wombat");
x和y指的是完全相同的logger对象。
因此,可以配置记录器,然后在代码中其他地方检索相同实例而无需传递引用。与亲生父母制(父母总是先于子女)的根本矛盾是,可以以任何顺序创建和配置logback记录器。特别是,“父”记录器将找到并链接到其后代,即使在其后代实例化之后也是如此。
通常在应用程序初始化时完成对logback环境的配置。首选方法是读取配置文件。稍后将讨论这种方法。
通过Logback,可以轻松按软件组件命名记录器。这可以通过实例化每个类中的记录器来实现,记录器名称等于该类的完全限定名称。这是定义记录器的有用且直接的方法。由于日志输出带有生成记录器的名称,因此这种命名策略使识别日志消息的来源变得容易。但是,这只是命名记录器的一种可能的策略,尽管很常见。Logback不限制可能的记录器集。作为开发人员,您可以随意命名记录器。
然而,以它们所在的类来命名日志记录器似乎是迄今为止所知的最佳通用策略。
Appenders and Layouts
根据日志记录器有选择地启用或禁用日志记录请求的功能只是其中的一部分。Logback允许将日志请求打印到多个目的地。在logback中,输出目的地称为Appender。目前,针对控制台、文件、远程套接字服务器、MySQL、PostgreSQL、Oracle和其他数据库、JMS和远程UNIX Syslog守护进程的附加程序已经存在。
一个Logger可以附加多个Appender
该addAppender
方法将附加器添加到给定的记录器。给定记录器的每个启用的日志记录请求都将转发给该记录器中的所有附加程序以及层次结构中较高的附加程序。换句话说,追加器是从记录器层次结构中继承而来的。例如,如果将控制台附加程序添加到根记录器,则所有已启用的记录请求将至少在控制台上打印。如果另外将文件追加器添加到记录器中,例如L,则对L和L的子级启用的日志记录请求将打印在文件上, 并且在控制台上。通过将记录器的可加性标志设置为false,可以覆盖此默认行为,以便不再增加附加器累积。
下面概述了控制追加程序可加性的规则。
Appender可加性
logger L的日志语句的输出将传递给L及其祖先中的所有追加器。这就是“附加加性”一词的含义。
但是,如果logger L的一个祖先(比如P)将可加性标记设置为false,那么L的输出将被定向到L中的所有追加器及其祖先(包括P,但不包括P的所有祖先中的追加器)。
默认情况下,记录器的可加性标记设置为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. |
通常,用户不仅希望自定义输出目标,还希望自定义输出格式。这是通过将布局与附加程序相关联来实现的。布局负责根据用户的需求格式化日志记录请求,而附加程序负责将格式化后的输出发送到其目的地。该 PatternLayout标准的logback分布的,部分,让用户根据类似于C语言的转换图案指定输出格式printf 的功能。
例如,带有转换模式“%-4relative [%thread]%-5level%logger {32}-%msg%n”的PatternLayout将输出类似于:
176 [main] DEBUG manual.architecture.HelloWorld2 - Hello world.
第一个字段是自程序启动以来经过的毫秒数。第二个字段是发出日志请求的线程。第三个字段是日志请求的级别。第四个字段是与日志请求关联的记录器的名称。“-”之后的文本是请求的消息。
参数化的日志
考虑到logback-classic中的日志记录器实现了SLF4J的日志记录器接口,某些打印方法允许使用多个参数。这些打印方法变体主要是为了提高性能,同时最小化对代码可读性的影响。
对于某些Logger而言logger,
logger.debug("Entry number: " + i + " is " + String.valueOf(entry[i]));
会产生构造message参数(即将整数i和entry[i] 字符串都转换为字符串,并连接中间字符串)的开销。这与是否将记录消息无关。
避免参数构造成本的一种可能方法是,将log语句包含在测试中。这是一个例子。
if(logger.isDebugEnabled()) {
logger.debug("Entry number: " + i + " is " + String.valueOf(entry[i]));
}
这样,如果为禁用了调试,则不会产生参数构造的开销logger。另一方面,如果为记录器启用了DEBUG级别,则将产生两次评估:一次进入debugEnabled一次 的成本,以评估是否启用了记录器debug。实际上,此开销微不足道,因为评估记录器所花费的时间不到实际记录请求所花费的时间的1%。
更好的选择
存在一种基于消息格式的便捷替代方法。假设entry是一个对象,您可以编写:
Object entry = new SomeObject();
logger.debug("The entry is {}.", entry);
只有在评估是否记录日志之后,并且只有在决定是肯定的情况下,记录器实现才会格式化消息,并用字符串值“”替换“ {}”对entry。换句话说,禁用log语句时,此格式不会产生参数构造的开销。
以下两行将产生完全相同的输出。但是,在禁用日志记录语句的情况下,第二个变体将比第一个变体好至少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组件之后,我们现在可以描述用户调用记录器的打印方法时logback框架所采取的步骤。现在让我们分析当用户调用info()名为com.wombat的记录器的方法时,logback采取的步骤 。
1.获得过滤器链决策
如果存在,TurboFilter则调用链。涡轮过滤器可以基于诸如信息设定的上下文范围的阈值,或过滤掉某些事件Marker, Level,Logger,消息,或 Throwable与每个日志记录请求相关联。如果过滤器链的答复为 FilterReply.DENY,那么将删除日志记录请求。如果是FilterReply.NEUTRAL,那么我们继续下一步,即步骤2。如果答复是 FilterReply.ACCEPT,则跳过下一步,直接跳到步骤3。
2.应用基本选择规则
在此步骤中,logback将记录器的有效级别与请求级别进行比较。如果根据此测试禁用了日志记录请求,则logback将丢弃该请求而无需进一步处理。否则,将继续进行下一步。
3.创建一个LoggingEvent对象
如果请求在先前的过滤器中仍然有效,则logback将创建一个ch.qos.logback.classic.LoggingEvent
对象,其中包含请求的所有相关参数,例如请求的记录器,请求级别,消息本身,可能与请求一起传递的异常,当前时间,当前线程,有关发出日志记录请求的类的各种数据以及MDC
。请注意,其中的某些字段是仅在实际需要时才延迟进行初始化的。将MDC
被用来装饰用额外的上下文信息的日志记录请求。MDC将在下一章中讨论。
4.调用附加器
创建LoggingEvent对象后,logback将调用doAppend()所有适用的appender 的方法,即从logger上下文继承的appender。
logback分发附带的所有追加程序都扩展了AppenderBase抽象类,该 抽象类doAppend在确保线程安全的同步块中实现了该 方法。如果存在任何此类过滤器,该doAppend()方法 AppenderBase还将调用附加到附加程序的自定义过滤器。可以动态附加到任何附加程序的自定义过滤器将在单独的章节中介绍。
5.格式化输出
被调用的附加程序负责格式化日志记录事件。但是,一些(但不是全部)附加程序将格式化日志记录事件的任务委托给布局。布局会格式化LoggingEvent实例,并以字符串形式返回结果。请注意,某些附加程序(例如) SocketAppender不会将日志记录事件转换为字符串,而是将其序列化。因此,它们没有布局,也不需要布局。
6.发出 LoggingEvent
日志记录事件完全格式化后,每个附加程序会将其发送到其目的地。
这是一个序列UML图,以显示一切工作原理。您可能要单击该图像以显示其较大版本。
性能
经常提到的反对日志记录的论点之一是其计算成本。这是一个合理的问题,因为即使大小适中的应用程序也可以生成数千个日志请求。我们的大部分开发工作都花在了衡量和调整logback的性能上。与这些工作无关,用户仍应注意以下性能问题。
1.完全关闭日志记录时的日志记录性能
您可以通过将root记录程序的级别设置Level.OFF为最高级别来完全关闭日志记录。当完全关闭日志记录时,日志请求的成本包括方法调用和整数比较。在3.2Ghz的Pentium D机器上,此成本通常约为20纳秒。
但是,任何方法调用都涉及参数构造的“隐藏”成本。例如,对于一些logger x 编写,
x.debug("Entry number: " + i + "is " + entry[i]);
无论是否记录消息,都会花费构造消息参数的成本,即将整数i和 entry[i]字符串都转换为字符串,并连接中间字符串。
参数构造的成本可能很高,并且取决于所涉及参数的大小。为了避免参数构造的开销,您可以利用SLF4J的参数化日志记录:
x.debug("Entry number: {} is {}", i, entry[i]);
此变体不会产生参数构造的成本。与以前对该debug()方法的调用相比 ,它将大大提高速度。仅当将日志记录请求发送到附加的附加程序时,消息才会被格式化。此外,格式化消息的组件已得到高度优化。
尽管上述将日志语句置于紧密的循环中(即非常频繁调用的代码)是一种失败的提议,但很可能会导致性能下降。即使关闭日志记录,紧密循环的日志记录也会使您的应用程序变慢,并且即使打开了日志记录,也会生成大量的(因此无用的)输出。
2.决定在打开日志记录时是否记录日志的性能。
在logback中,无需遍历记录器层次结构。记录器在创建时便知道其有效级别(即,一旦考虑了级别继承,它的级别)。如果更改了父记录器的级别,则会联系所有子记录器以注意更改。因此,在基于有效级别接受或拒绝请求之前,记录器可以做出准即时的决定,而无需咨询其祖先。
3.实际日志记录(格式化和写入输出设备)
这是格式化日志输出并将其发送到目标目的地的成本。再次在这里,我们付出了巨大的努力来使布局(格式器)尽快执行。追加者也是如此。当记录到本地计算机上的文件时,实际记录的典型成本约为9到12微秒。登录到远程服务器上的数据库时,最多需要几毫秒的时间。
尽管功能丰富,但回送的首要设计目标之一是执行速度,这是仅次于可靠性的要求。一些logback组件已被重写多次,以提高性能。