Logger, Appenders 和 Layouts
工作原理概述
在介绍了基本的Logback组件之后,我们现在可以描述当用户调用Logger的打印方法时,Logback框架日志请求的执行步骤。
现在让我们分析一下当用户调用名为com.wombat的Logger的info()
方法时,Logback日志请求的执行步骤。如果
过滤器链返回FilterReply.DENY
,则日志请求被拒绝;如果返回 FilterReply.NEUTRAL
,则继续下一步骤(即步骤2);
如果返回FilterReply.ACCEPT
,则跳过下一步,直接进入步骤3。
- 获取过滤器链的决策结果
如果存在,TurboFilter
过滤器链将会被调用。Turbo过滤器可以设置一个全局阈值,或者基于日志请求关联的信息
(如Marker
, Level
, Logger
、消息或Throwable
)过滤掉某些事件。
- 应用基本选择规则
在这个步骤中,Logback会比较Logger的生效级别和日志请求的级别。如果请求日志的级别验证不通过,则日志请求被拒绝;
否则,它将继续执行下一步。
- 创建
LoggingEvent
对象
如果日志请求通过前面所述的过滤器链,Logback会创建一个ch.qos.logback.classic.LoggingEvent
对象。
这个对象包含日志请求的所有信息,如logger对象、日志级别、日志消息、可能随请求一起传递的异常对象、当前时间、
当前线程、发出日志请求的类的各种数据以及MDC
。请注意,有些属性字段是在需要时才会初始化(延迟加载)。
MDC 用于为日志请求添加额外的上下文信息。MDC的相关内容将在后续章节中讨论。
- 调用Appender
创建完LoggingEvent
对象后,Logback会调用所有适用的Appender的doAppend()
方法,这些Appender是从Logger Context中继承的。
所有随Logback发行版提供的Appender都扩展自AppenderBase
抽象类,该类在一个同步块中实现了doAppend
方法以确保线程安全。
AppenderBase
的doAppend()
方法还会调用添加到该Appender的自定义过滤器(如果存在这样的过滤器)。
动态添加自定义过滤器到Appender将在单独的章节中介绍。
- 格式化输出
一般来讲,被调用的Appender会负责日志事件的格式化输出。然而,一些(但不是全部)Appender会将格式化任务委托给Layout。
Layout会格式化LoggingEvent
实例并返回结果字符串。请注意,一些Appender(如SocketAppender
)不会将日志事件转换为字符串,
而是对其进行序列化。因此,它们不需要也不依赖Layout。
- 发送
LoggingEvent
在日志事件完全格式化之后,每个Appender都会将其发送到目标地址。
下面是一个UML时序图,显示了所有的工作流程。
性能
日志记录的计算成本是一个值得关注的问题,因为即使是一个中等规模的应用程序也可能产生成千上万的日志请求。我们投入了大量开发精力来测量和微调
Logback的性能。然而,无论这些努力如何,用户仍然应该意识到以下性能问题。
- 当日志记录全部被关闭时的性能表现
可以通过将Root Logger的日志级别设置成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()
方法调用相比,它在性能上会有显著提升。
只有当日志请求需要发送到Appender时,才会格式化消息。此外,负责格式化消息的组件也经过了高度优化。
尽管如此,将日志语句放在紧密循环中(即频繁调用的代码)仍是一个双输的做法,极易导致性能下降。
即使日志功能被关闭,紧密循环中的日志也会拖慢你的应用程序;如果日志功能开启,则会产生大量无用的输出。
- 开启日志功能时,决策是否记录日志的性能表现
在Logback中,判定日志额生效级别是不需要遍历Logger的层级结构的。当Logger创建创建时他就已经设定好了生效级别(考虑了级别继承之后的实际级别)。
如果某个父Logger的级别发生了状态变化,那么所有子Logger都会收到通知以更新状态。因此,在根据有效级别接受或拒绝一个日志请求之前,Logger可以
几乎实时地做出决策,而无需咨询其祖先。
- 实际的日志记录(格式化并写入输出设备)
这是指格式化日志输出并将它发送到目标地址的成本。再次强调,我们做出了大量努力来使Layout(格式化器)尽可能快速执行。Appender同样也是如此优化的。
当向本地机器上的文件进行日志记录时,实际的日志记录成本通常在9到12微秒之间;而在远程服务器上向数据库写入日志时,这一成本会上升到几毫秒。
尽管Logback功能丰富,但其设计中最优先考虑的目标之一就是执行速度,仅次于可靠性。为了提高性能,一些Logback组件已经被重写了多次。