日志框架 Logback 官方手册(第二章:架构)

以下内容翻译整理自logback官方手册,地址:logback官方手册


logback 的架构

logback的基本架构足够通用,可以应用于不同的环境。目前,logback分为三个模块:logback-corelogback-classiclogback-access

core模块是其它两个模块的基础,classic模块继承core模块,classic模块相对log4j版本有显著的改进,logback-classic天生实现了SLF4J API,所以你可以在logback和其他日志框架之间自由切换,比如log4jJDK1.4引入的JUL(java.util.logging)access模块集成了Servlet容器,用来提供HTTP-access日志功能,一个单独的文档包含访问模块文档。

在本文档的其余部分中,我们将引用logback-classic模块来编写logback

Logger,Appenders 和 Layouts

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

Logger类是logback-classic模块的一部分,AppenderLayout接口是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.foologger是名称为com.foo.Barlogger的父,类似地,javajava.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, XX.Y.Z都有指定的级别,X.Y没有指定级别,是从父日志记录器X继承的级别。

示例4

Logger name 指定级别 有效级别
root DEBUG DEBUG
X INFO INFO
X.Y none INFO
X.Y.Z none INFO

示例4中,日志记录器rootX有指定的级别,X.YX.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");

XY是相同的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 的子节点启用的日志记录请求将打印在文件里和控制台上。通过将loggeradditivity 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

通常,用户不仅希望自定义输出目的地,还希望自定义输出格式。可以通过将layoutappender关联来实现。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实现了SLF4JLogger接口,某些打印方法允许多个参数。这些打印方法变体主要是为了提高性能,同时降低对代码可读性的影响。

普通写法

对于一些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将调用所有适用的appenderdoAppend()方法,即从日志程序上下文中继承的appender

logback发行版附带的所有附加程序都扩展了AppenderBase抽象类,该类在确保线程安全的同步块中实现doAppend方法。如果存在附加的自定义过滤器,AppenderBasedoAppend()方法也能调用。可以动态附加到任何附加器的自定义过滤器将在单独的一章中介绍。

5. 格式化输出

被调用的附加程序负责格式化日志事件。然而,一些(但不是所有)附加程序将格式化日志事件的任务委托给了layout,布局可以格式化LoggingEvent实例并以字符串的形式返回结果。注意,有些附加程序,如SocketAppender,不将日志事件转换为字符串,而是序列化它。因此,它们没有也不需要布局。

6. 发送 LoggingEvent

日志事件完全格式化后,由每个附加程序将其发送到目的地。下面是一个序列 UML 图,展示了所有事情是如何工作的。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 200,667评论 5 472
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,361评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 147,700评论 0 333
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,027评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,988评论 5 361
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,230评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,705评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,366评论 0 255
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,496评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,405评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,453评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,126评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,725评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,803评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,015评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,514评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,111评论 2 341

推荐阅读更多精彩内容