本文主要内容为 logback 官网内容的翻译
目前,logback 主要分为三个模块:
- logback-core:是其他两个模块的基础模块
- logback-classic:是对 core 模块的扩展,相当于 log4j 的改良版。classic 模块实现了 Slf4j 的 API 因此可以便于和其他日志框架直接切换
- logback-access:与Servlet容器集成,以提供http访问日志功能。
Logger, Appenders and Layouts
logback 基于三个主要类: Logger
、Appender
和 Layout
。这三种类的组件协同工作,使开发人员能够通过日志类型和级别来记录日志,并在运行时控制这些日志的格式和报告位置。
Logger
类定义在 classic 模块中,而 Appender
和 Layout
定义在 core 模块中。
1. Logger context
与普通System.out.println相比,任何日志API的第一个也是最重要的优势在于它能够禁用某些日志语句,同时允许其他日志语句不受阻碍地打印。该功能假定日志空间(即所有可能的日志语句的空间)根据开发人员选择的一些标准进行分类。在 logback-classic
中,这种分类是 loggers 的固有部分。每个logger 都连接到一个LoggerContext,它负责制造 logger,并将它们排列成树状的层次结构。
logger 是命名实体。它们的名字区分大小写,并且遵循分层命名规则:
命名的层次结构
如果一个 logger 的名称后面跟着一个点是其后代 logger 名称的前缀,则该 logger 被称为另一个 logger 的祖先。如果一个 logger 本身和后代记录器之间没有祖先,那么它就被称为子 logger 的父 logger 。
比较绕,看例子很容易懂
com.foo
是 com.foo.Bar
的父 logger,java
是 java.util.Vector
的祖先 logger。
即通过对比前缀,以 .
做分隔,多级为祖先,单级为父子。
根 logger 位于 logger 层次结构的顶部。它的特殊之处在于,它从一开始就是所有等级制度的一部分。像每个日志程序一样,它可以通过它的名字进行检索,如下所示:
Logger rootLogger = LoggerFactory.getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME);
改 logger 的名字为 ROOT
下面列出 org.slf4j.Logger 接口中的常用方法
public interface Logger {
final public String ROOT_LOGGER_NAME = "ROOT";
public String getName();
public void trace(String msg);
public void trace(String format, Object... arguments);
public void trace(String msg, Throwable t);
public void debug(String msg);
public void debug(String format, Object... arguments);
public void debug(String msg, Throwable t);
public void info(String msg);
public void info(String format, Object... arguments);
public void info(String msg, Throwable t);
public void warn(String msg);
public void warn(String format, Object... arguments);
public void warn(String msg, Throwable t);
public void error(String msg);
public void error(String format, Object... arguments);
public void error(String msg, Throwable t);
}
2. 日志级别 Level
logger 可以被分配级别。可能的级别(TRACE, DEBUG, INFO, WARN和ERROR)定义在 ch.qos.logback.classic.Level
类中。注意,在 logback 中,Level类是final类,不能子类化,因为 logback 提供了一种更灵活的方法即 Marker
。
如果给定的 logger 没有指定级别,那么它将从具有指定级别的最近祖先继承一个级别。更正式地:
给定 logger L
的有效级别等于其层次结构中的第一个非空级别,从 L
本身开始,在层次结构中向上直到根 logger 。
为了确保所有日志 logger 最终都能继承一个级别,根 logger 总是有一个已分配的级别。缺省情况下,该级别为DEBUG。
根据定义,打印方法决定日志记录请求的级别。例如,如果 L
是一个 logger 实例,那么语句 L.INFO("..")
是一个INFO级别的日志记录语句。
如果日志记录请求的级别高于或等于 logger 的有效级别,则表示启用了日志记录请求。否则,该请求将被禁用。如前所述,没有指定级别的 logger 将从其最近的祖先继承级别。
该规则是 logback 的核心。 它假设级别顺序如下:
TRACE < DEBUG < INFO < WARN < ERROR
可以通过 ch.qos.logback.classic.Logger
对象的 setLevel()
方法设置 logger 的级别。
也可以通过配置文件配置 根 logger 的级别,然后使得其他 logger 继承改级别,进而对日志的打印做出限制
注意:logger 对应名字是单例模式,通过 LoggerFactory.getLogger()
方法,传入相同参数时,获取到的 Logger
对象是同一个。
3. appender
logback 允许将日志请求打印到多个目的地。在 logback 中,日志输出的目的地称为 appender
。目前,appender 可以是控制台、文件、远程套接字服务器、MySQL、PostgreSQL、Oracle和其他数据库、JMS和远程UNIX Syslog守护进程。
同一个 logger 可以配置多个 appender。
addAppender
方法将一个 appender 添加到给定的 logger。对于给定 logger 的每个生效的日志记录请求将被转发到该 logger 中的所有 appender 以及层次结构中更高的 appender 。换句话说,appender 是从 logger 层次结构中继承下去。例如,如果将控制台 appender 配置到根 logger,那么所有启用的日志记录请求将至少打印在控制台上。另外,如果将文件 appender 添加到 logger L
中,那么启用的 L
和 L
的子日志请求将打印在文件和控制台上。通过将 logger 的可加性标志设置为false,可以覆盖此默认行为,从而使 appender 累加不再是可加的。
Appender Additivity
loggerL
的日志语句的输出将被输出到L
及其祖先的所有 appender 。这就是“ Appender Additivity ”一词的含义。但是,如果logger
L
的祖先(比如P
)的可加性标志设置为false,那么L
的日志输出将包含L
向上到P
(包含P
)的 logger 的所有 appender,而不包含P
的祖先。logger 的可加性标志默认设置为 true 。
4. layout
通常情况下,用户不仅希望自定义输出目的地,还希望自定义输出格式,在 logback 中,layout 复制定义日志的输出格式。可以通过将 layout 与 appender 关联来实现不同输出不同格式。layout 负责根据用户的意愿格式化日志请求,而 appender 负责将格式化的输出发送到目的地。作为标准logback分布的一部分,PatternLayout
允许用户根据类似于C语言printf函数的转换模式指定输出格式。
例如,以下
"%-4relative [%thread] %-5level %logger{32} - %msg%n"
将输出
176 [main] DEBUG manual.architecture.HelloWorld2 - Hello world.
第一个字段是程序开始后经过的毫秒数。
第二个字段是发出日志请求的线程。
第三个字段是日志请求的级别。
第四个字段是与日志请求相关联的 logger 的名称。
‘-’ 后面的文本是请求的消息。
5. 常用方法参数
logback-classic
中, logger 实现了 SLF4J 的 Logger接口,在该接口中,重载了许多方法,这些方法具有不同的参数列表。 这些打印方法变体主要是为了提高性能,同时最小化对代码可读性的影响。
以 debug方法为例:
void debug(String msg);
void debug(String format, Object... arguments);
void debug(String msg, Throwable t);
对于最基础的方法 debug(String msg)
,我们调用以下内容
logger.debug("Entry number: " + i + " is " + String.valueOf(entry[i]));
其中,将整数类型 i
,以及数组 entry
的第 i 项转化成字符串,并将字符串拼接,这部分代码是必然会发生的,即使当前 logger 没有达到对应的日志级别,因此可能会产生无用开销。
一个解决方案是:我们可以通过加入判断:
if(logger.isDebugEnabled()) {
logger.debug("Entry number: " + i + " is " + String.valueOf(entry[i]));
}
这样,如果当前 logger 配置的级别低于 DEBUG,就不会产生字符串拼接的开销。但于此同时,代码进行了两次
isDebugEnabled()
的判断(还有一次是在 debug 方法内部),所幸该开销很小,可以忽略。但也会造成代码的冗余,降低了可读性。
这里提供了一种更好的解决方案 debug(String format, Object... arguments);
Object entry = new SomeObject();
logger.debug("The entry is {}.", entry);
在该方法内部,只有当日志请求被确认执行时,logger 才会格式化消息并使用可选参数的字符串值替换“{}”对。换句话说,当日志语句被禁用时,这种形式不会导致参数构建的开销。
下面两行代码将产生完全相同的输出。然而,在禁用日志语句的情况下,第二种方式将比第一种方式的性能高出至少30倍。
logger.debug("The new entry is "+entry+".");logger.debug("The new entry is {}.", entry);
6. 日志打印流程
用户调用名为 com.wombat
的 logger 的 info()
方法时,logback所采取的步骤如下。
① 通过 filter chain 决定日志是否打印
如果存在,则调用 TurboFilter
链。TurboFilter
可以设置上下文范围的阈值,或者根据与每个日志请求相关联的 Marker、Level、Logger、message 或 Throwable 等信息过滤某些事件。如果过 filter chain 的应答为FilterReply.DENY,那么日志请求将被丢弃。如果是 FilterReply.NEUTRAL,那么执行第②步。如果是FilterReply.ACCEPT,直接跳到步骤③。
② 应用 basic selection rule,即上面的日志级别规则
在这一步,logback 将 logger 的有效级别与请求级别进行比较。 如果根据此测试禁用了日志记录请求,那么logback将删除该请求,而不进行进一步处理。 否则,将继续执行下一步。
③ 创建 LoggingEvent
对象
logback 将创建一个 ch.qos.logback.classic.LoggingEvent
对象包含所有请求的相关参数,如请求的 logger,请求级别,消息本身,请求异常,当前时间,当前线程,关于发出日志请求的类和MDC(Mapped Diagnostic Context,存放日志诊断的容器)的各种数据。注意,其中一些字段是 lazy 初始化的,只在实际需要时才进行初始化。MDC 用于用额外的上下文信息修饰日志记录请求。
④ 执行 appenders 的doAppend()方法
在创建LoggingEvent对象之后,logback将调用所有适用的 appenders 的doAppend()方法
logback发行版附带的所有 appenders 都扩展了AppenderBase抽象类,该抽象类在同步块中实现doAppend方法,以确保线程安全。如果存在自定义过滤器,那么 AppenderBase的 doAppend() 方法也会执行附加到该appender的自定义过滤器。
⑤ 格式化输出
被调用的 appender 负责格式化日志记录事件。然而,有些(但不是所有)appender 将格式化日志事件的任务委托给 layout。layout 格式化 LoggingEvent
实例,并以字符串形式返回结果。一些 appender ,如SocketAppender,不会将日志记录事件转换为字符串,而是序列化它。因此,它们没有也不需要布局。
⑥ 输出LoggingEvent
日志记录事件完全格式化后,每个 appender 将其发送到目的地。
下面是一个序列UML图,显示了 logback 是如何工作的
7. 性能
用户应该了解以下性能问题。
① 日志记录完全关闭时的日志记录性能
通过将根日志记录器的级别设置为 Level.OFF
,可以完全关闭日志记录,这是最高的日志级别。当完全关闭日志记录时,日志请求的开销包括方法调用和整数比较。在一台3.2Ghz的机器上,这一成本通常在20纳秒左右。
然而,任何方法调用都涉及参数构造的“隐藏”成本。例如,
x.debug("Entry number: " + i + "is " + Entry [i]);
参数构建的成本可能相当高,这取决于所涉及参数的大小,因此需要避免这种写法。
尽管如此,将日志语句放置在紧密循环(即非常频繁调用的代码)中是非常不建议的,可能会导致性能下降。即使关闭了日志记录,在紧密循环中日志记录也会减慢应用程序的速度,如果打开日志记录,将生成大量(因此是无用的)输出。
② 当日志记录打开时,决定是否记录日志的性能。
在logback中,不需要遍历 logger 层次结构。日志 logger 在创建时知道它的有效级别(也就是说,考虑到级别继承后,它的级别)。如果父记录器的级别发生了更改,那么将联系所有子记录器以注意更改。因此,在基于有效级别接受或拒绝请求之前,记录器可以做出准瞬时决定,而不需要咨询其祖先。
③ 实际日志记录(格式化并写入到输出设备)性能
这是格式化日志输出并将其发送到目标目的地的成本。在这里,我们再次努力使 layout (格式化器)尽可能快地执行。对于 appender 也是如此。当日志记录到本地机器上的一个文件时,实际日志记录的成本通常是9到12微秒。当登录到远程服务器上的数据库时,它会上升到几毫秒。
尽管logback功能丰富,但其最重要的设计目标之一是执行速度,这一需求仅次于可靠性。为了提高性能,一些日志回送组件已经被重写了好几次。