以下内容翻译整理自logback
官方手册,地址:logback官方手册
logback 的架构
logback
的基本架构足够通用,可以应用于不同的环境。目前,logback
分为三个模块:logback-core
,logback-classic
和logback-access
。
core
模块是其它两个模块的基础,classic
模块继承core
模块,classic
模块相对log4j
版本有显著的改进,logback-classic
天生实现了SLF4J API
,所以你可以在logback
和其他日志框架之间自由切换,比如log4j
和JDK1.4引入的JUL(java.util.logging)
。access
模块集成了Servlet
容器,用来提供HTTP-access
日志功能,一个单独的文档包含访问模块文档。
在本文档的其余部分中,我们将引用logback-classic
模块来编写logback
。
Logger,Appenders 和 Layouts
Logback
基于三个主要类:Logger
,Appender
和Layout
,这三种类型的组件协同工作,使开发人员能够根据消息类型和级别记录消息,并在运行时控制这些消息的格式和报告位置。
Logger
类是logback-classic
模块的一部分,Appender
和Layout
接口是logback-core
模块的一部分。作为通用模块,logback-core
没有日志记录器的概念。
logger
是日志记录器,appender
是追加器,layout
是布局。
Logger 上下文
任何日志API
相对于普通的System.out.println
的最重要的优势是能够禁用某些日志语句,同时允许其他语句不受阻碍地打印。该功能假定的日志空间是根据一些开发人员选择的标准进行分类的。在logback-classic
中,这种分类是logger
的固有组成部分。每个logger
都附加到一个LoggerContext
,该上下文负责生成logger
,并将它们安排在类似层次结构的树中。
logger
是命名实体。它们的名字区分大小写,并遵循分层命名规则:
如果一个
logger
的名称后面跟着一个点,那么这个logger
就是另一个logger
的祖先。如果在其自身和后代logger
之间没有祖先,则该logger
被称为子logger
的父。
例如,名称为com.foo
的logger
是名称为com.foo.Bar
的logger
的父,类似地,java
是java.util
的父,是java.util.Vector
的祖先。开发人员都应该熟悉这种命名方案。
根logger
位于logger
层次结构的顶部。它的特殊之处在于,它一开始就是每个层次结构的一部分。与每个logger
一样,可以通过它的名称获取它,如下所示:
Logger rootLogger = LoggerFactory.getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME);
所有其他logger
也可以通过org.slf4j.LoggerFactory
类中的静态方法getLogger()
来获取。该方法需要传递日志记录器的名称作为参数。下面列出了Logger
接口中的一些基本方法。
package org.slf4j;
public interface Logger {
String ROOT_LOGGER_NAME = "ROOT";
// 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
类中,该类是final修饰的,不能被子类化。
如果一个给定的logger
没有被分配一个级别,那么它将从其最近的祖先那里继承一个级别。为了确保所有的logger
最终都能继承到一个级别,根logger
有一个默认级别DEBUG
。
下面是四个例子,根据级别继承规则,使用各种指定的级别值和产生的有效(继承)级别。
示例1
Logger name | 指定级别 | 有效级别 |
---|---|---|
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 | 指定级别 | 有效级别 |
---|---|---|
root | ERROR | ERROR |
X | INFO | INFO |
X.Y | DEBUG | DEBUG |
X.Y.Z | WARN | WARN |
示例2中,所有logger
都有一个指定的级别值,级别继承不起作用。
示例3
Logger name | 指定级别 | 有效级别 |
---|---|---|
root | DEBUG | DEBUG |
X | INFO | INFO |
X.Y | none | INFO |
X.Y.Z | ERROR | ERROR |
示例3中,日志记录器root
, X
和X.Y.Z
都有指定的级别,X.Y
没有指定级别,是从父日志记录器X
继承的级别。
示例4
Logger name | 指定级别 | 有效级别 |
---|---|---|
root | DEBUG | DEBUG |
X | INFO | INFO |
X.Y | none | INFO |
X.Y.Z | none | INFO |
示例4中,日志记录器root
和X
有指定的级别,X.Y
和X.Y.Z
没有指定级别,从最近的有指定级别的父级X
继承级别值。
打印方法和基本选择规则
根据定义,打印方法确定日志请求的级别。例如,如果L
是一个logger
实例,那么语句L. INFO(“..”)
就是一个级别INFO
的日志语句。
如果日志记录请求的级别高于或等于其日志记录程序的有效级别,则启用日志记录请求。否则,该请求将被禁用。如前所述,没有指定级别的日志记录器将从其最近的祖先那里继承一个级别。这条规则是logback
的核心。它规定各级的次序如下:
TRACE < DEBUG < INFO < WARN < ERROR
下面是一个基本选择规则的例子。
package com.wangbo.cto.logback;
import ch.qos.logback.classic.Level;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* @date 2019/9/13 22:48
* @auther wangbo
*/
public class LogLevelTest {
public static void main(String[] args) {
//获取一个名为“com.foo”的logger,为了能设置级别,转换为ch.qos.logback.classic.Logger类型
ch.qos.logback.classic.Logger logger = (ch.qos.logback.classic.Logger) LoggerFactory.getLogger("com.foo");
//设置级别
logger.setLevel(Level.INFO);
//继承最近的父com.foo的级别info
Logger barlogger = LoggerFactory.getLogger("com.foo.Bar");
//warn >= info,启用此请求
logger.warn("Low fuel level.");
//debug <= info,此请求已禁用
logger.debug("Starting search for nearest gas station.");
//info >= info,启用此请求
barlogger.info("Located nearest gas station.");
//debug <= info,此请求已禁用
barlogger.debug("Exiting gas station search");
}
}
运行结果
22:59:44.139 [main] WARN com.foo - Low fuel level.
22:59:44.141 [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
对象。
因此,可以配置一个日志程序,然后在代码的其他地方通过相同的名字获取到相同的实例,而不需要传递引用。与生物学意义上的父母(父母总是先于子女)相反,logback
日志记录器可以按任何顺序创建和配置。特别是,父logger
将发现并链接到它的后代,即使它是在它们之后实例化的。
通常在应用程序初始化时配置logback环境。首选的方法是读取配置文件。不久将讨论这种方法。
以日志记录器所在的类命名日志记录器似乎是迄今为止所知的最佳通用策略。
Appenders 和 Layouts
根据日志程序选择性地启用或禁用日志记录请求的功能只是一部分。Logback
允许将日志请求打印到多个目的地。在logback
中,输出目的地称为appender
。目前,针对控制台、文件、远程套接字服务器、MySQL、PostgreSQL、Oracle和其他数据库、JMS和远程UNIX Syslog守护进程存在附加程序。
一个logger
可以附加多个appender
。
addAppender
方法向给定的logger
添加一个appender
。对于给定的logger
,每个启用的日志请求都将被转发到该logger
中的所有appender
以及层次结构中更高的appender
。换句话说,appender
是附加地从日志程序层次结构继承的。例如,如果将控制台appender
添加到根logger
,那么所有启用的日志请求至少都将打印在控制台上。此外,如果向logger
(L)添加了一个文件appender
,然后,为 L 和 L 的子节点启用的日志记录请求将打印在文件里和控制台上。通过将logger
的additivity flag
设置为false
,可以覆盖此默认行为,使追加器积累不再是附加的。
下表是一个例子:
Logger Name | 附加的 Appenders | Additivity Flag | 输出目标 | 注释 |
---|---|---|---|---|
root | A1 | 不适用 | A1 | 由于根日志程序位于日志程序层次结构的顶部,所以不应用加法标志。 |
x | A-x1, A-x2 | true | A1, A-x1, A-x2 | 使用了 x 和 root 的追加器 |
x.y | none | true | A1, A-x1, A-x2 | 使用了 x 和 root 的追加器 |
x.y.z | A-xyz1 | true | A1, A-x1, A-x2, A-xyz1 | 使用了 x.y.z,x 和 root的追加器 |
security | A-sec | false | A-sec | 由于可加性标志设置为 false,所以没有追加器累加,只会使用一个追加器 A-sec |
security.access | none | true | A-sec | 因为 security 中的可加性标志设置为 false,所以只使用 security 的追加器 A-sec |
通常,用户不仅希望自定义输出目的地,还希望自定义输出格式。可以通过将layout
与appender
关联来实现。layout
负责根据用户的意愿格式化日志请求,appender
负责将格式化的输出发送到它的目的地。PatternLayout
是标准logback
分发版的一部分,允许用户根据类似于C
语言printf
函数的转换模式指定输出格式。
例如,PatternLayout
设置为%-4relative [%thread] %-5level %logger{32} - %msg%n
,将输出类似于下面格式的内容:
176 [main] DEBUG manual.architecture.HelloWorld2 - Hello world.
第一个字段是自程序启动以来经过的毫秒数。第二个字段是发出日志请求的线程。第三个字段是日志请求的级别。第四个字段是与日志请求关联的日志记录器的名称。'-'后面的文本是请求的消息。
参数化日志
考虑到logback-classic
中的logger
实现了SLF4J
的Logger
接口,某些打印方法允许多个参数。这些打印方法变体主要是为了提高性能,同时降低对代码可读性的影响。
普通写法
对于一些logger
,可以这样写:
logger.debug("Entry number: " + i + " is " + String.valueOf(entry[i]));
该参数将整数 i 和 entry[i] 转换为字符串,并连接中间的字符串。会导致构造消息参数的额外开销,但是这与是否记录消息没有关系。
避免参数构造额外开销的一种方法是用一个测试包围 log 语句。比如这样:
if(logger.isDebugEnabled()) {
logger.debug("Entry number: " + i + " is " + String.valueOf(entry[i]));
}
这样,如果logger
禁用了DEBUG
级别,就不会产生参数构造的开销。另一方面,如果logger
启用了DEBUG
级别,系统将承担两次评估日志记录器是否启用的成本,一次是在debugEnabled
,第二次是在debug
,在实践中,这种开销是微不足道的,因为评估一个日志记录器所需时间相对于实际记录一个请求所需的时间不到1%。
推荐写法
存在一种基于消息格式的替代方法。假设entry
是一个对象,可以这样写:
Object entry = new SomeObject();
logger.debug("The entry is {}.", 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
框架所采取的步骤。现在让我们分析用户调用名为com.wombat
的日志记录器的info()
方法时,logback
所采取的步骤。
1. 获得过滤器链决策
如果存在,则调用TurboFilter
链。Turbo 过滤器可以设置上下文范围的阈值,或者根据与每个日志请求关联的标记、级别、日志记录器、消息或可抛出性等信息过滤掉某些事件。如果过滤器链的响应是拒绝FilterReply.DENY
,则日志请求将被删除。如果是中性FilterReply.NEUTRAL
,然后我们继续下一步,即第2步。如果是接受FilterReply.ACCEPT
,我们跳过下一步,直接跳到步骤3。
2. 应用基本的选择规则
在此步骤中,logback
将日志记录器的有效级别与请求的级别进行比较。如果根据此测试禁用日志记录请求,那么logback
将删除该请求,而不进行进一步处理。否则,将继续下一步。
3. 创建一个 LoggingEvent 对象
如果请求通过了前面的过滤器,logback
将创建一个ch. qs .logback.classic.LoggingEvent
对象,该对象包含请求的所有相关参数,例如请求的日志记录器,请求级别,消息本身,可能随请求一起传递的异常、当前时间、当前线程、发出日志记录请求的类的各种数据以及 MDC。注意,其中一些字段是延迟初始化的,只有在实际需要时才会这样做。MDC 用于用附加的上下文信息装饰日志记录请求。MDC将在下一章中讨论。
4. 调用 appenders
创建 LoggingEvent
对象之后,logback
将调用所有适用的appender
的doAppend()
方法,即从日志程序上下文中继承的appender
。
logback
发行版附带的所有附加程序都扩展了AppenderBase
抽象类,该类在确保线程安全的同步块中实现doAppend
方法。如果存在附加的自定义过滤器,AppenderBase
的doAppend()
方法也能调用。可以动态附加到任何附加器的自定义过滤器将在单独的一章中介绍。
5. 格式化输出
被调用的附加程序负责格式化日志事件。然而,一些(但不是所有)附加程序将格式化日志事件的任务委托给了layout
,布局可以格式化LoggingEvent
实例并以字符串的形式返回结果。注意,有些附加程序,如SocketAppender
,不将日志事件转换为字符串,而是序列化它。因此,它们没有也不需要布局。
6. 发送 LoggingEvent
日志事件完全格式化后,由每个附加程序将其发送到目的地。下面是一个序列 UML 图,展示了所有事情是如何工作的。