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图,显示了一切是如何工作的。您可能想要单击图像以显示其更大的版本。

android 设置loglevel 属性_字符串

性能

反对日志记录的一个经常被引用的论据是它的计算成本。这是一个合理的担忧,因为即使是中等规模的应用程序也可能生成数千个日志请求。我们的大部分开发工作都花在度量和调整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功能丰富,但其最重要的设计目标之一是执行速度,这一需求仅次于可靠性。为了提高性能,一些日志回送组件已经被重写了好几次。