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