logback 基本概念

本文主要内容为 logback 官网内容的翻译

目前,logback 主要分为三个模块:

  • logback-core:是其他两个模块的基础模块
  • logback-classic:是对 core 模块的扩展,相当于 log4j 的改良版。classic 模块实现了 Slf4j 的 API 因此可以便于和其他日志框架直接切换
  • logback-access:与Servlet容器集成,以提供http访问日志功能。

Logger, Appenders and Layouts

logback 基于三个主要类: LoggerAppenderLayout。这三种类的组件协同工作,使开发人员能够通过日志类型和级别来记录日志,并在运行时控制这些日志的格式和报告位置。

Logger 类定义在 classic 模块中,而 AppenderLayout 定义在 core 模块中。

1. Logger context

与普通System.out.println相比,任何日志API的第一个也是最重要的优势在于它能够禁用某些日志语句,同时允许其他日志语句不受阻碍地打印。该功能假定日志空间(即所有可能的日志语句的空间)根据开发人员选择的一些标准进行分类。在 logback-classic 中,这种分类是 loggers 的固有部分。每个logger 都连接到一个LoggerContext,它负责制造 logger,并将它们排列成树状的层次结构。

logger 是命名实体。它们的名字区分大小写,并且遵循分层命名规则:

命名的层次结构
如果一个 logger 的名称后面跟着一个点是其后代 logger 名称的前缀,则该 logger 被称为另一个 logger 的祖先。如果一个 logger 本身和后代记录器之间没有祖先,那么它就被称为子 logger 的父 logger 。

比较绕,看例子很容易懂

com.foocom.foo.Bar 的父 logger,javajava.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 中,那么启用的 LL 的子日志请求将打印在文件和控制台上。通过将 logger 的可加性标志设置为false,可以覆盖此默认行为,从而使 appender 累加不再是可加的。

Appender Additivity
logger L 的日志语句的输出将被输出到 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 是如何工作的

underTheHoodSequence2.gif

7. 性能

用户应该了解以下性能问题。

① 日志记录完全关闭时的日志记录性能

通过将根日志记录器的级别设置为 Level.OFF,可以完全关闭日志记录,这是最高的日志级别。当完全关闭日志记录时,日志请求的开销包括方法调用和整数比较。在一台3.2Ghz的机器上,这一成本通常在20纳秒左右。

然而,任何方法调用都涉及参数构造的“隐藏”成本。例如,

x.debug("Entry number: " + i + "is " + Entry [i]);

参数构建的成本可能相当高,这取决于所涉及参数的大小,因此需要避免这种写法。

尽管如此,将日志语句放置在紧密循环(即非常频繁调用的代码)中是非常不建议的,可能会导致性能下降。即使关闭了日志记录,在紧密循环中日志记录也会减慢应用程序的速度,如果打开日志记录,将生成大量(因此是无用的)输出。

② 当日志记录打开时,决定是否记录日志的性能。

在logback中,不需要遍历 logger 层次结构。日志 logger 在创建时知道它的有效级别(也就是说,考虑到级别继承后,它的级别)。如果父记录器的级别发生了更改,那么将联系所有子记录器以注意更改。因此,在基于有效级别接受或拒绝请求之前,记录器可以做出准瞬时决定,而不需要咨询其祖先。

③ 实际日志记录(格式化并写入到输出设备)性能

这是格式化日志输出并将其发送到目标目的地的成本。在这里,我们再次努力使 layout (格式化器)尽可能快地执行。对于 appender 也是如此。当日志记录到本地机器上的一个文件时,实际日志记录的成本通常是9到12微秒。当登录到远程服务器上的数据库时,它会上升到几毫秒。

尽管logback功能丰富,但其最重要的设计目标之一是执行速度,这一需求仅次于可靠性。为了提高性能,一些日志回送组件已经被重写了好几次。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值