zoukankan      html  css  js  c++  java
  • The logback manual #02# Architecture

    参杂着原文翻译和自己的话。。感谢有道翻译。

    全文注释:

    • log请求=logging request=日志请求,指代logger.info、logger.debug之类的调用。
    • 祖先的意思根据语境变化,有时候也包含父母,大多数时候只指爷爷辈以上。

    Logback's architecture

    logback被分成三个模块:logback-core, logback-classic和logback-access,其中core模块是其它两个模块的基础。classic模块是core模块的扩展,它对应着log4j的一个显著改进版本。logback-classic实现了SLF4J API,这让你可以在各种日志系统(JUL、log4j等)和logback间无压力地切换。第三个名为access的模块与Servlet容器集成,以提供HTTP访问日志功能。文档的剩余部分,主要讲述logback-classic相关的内容。

    Logger, Appenders and Layouts

    Logback建立在三种主要类的基础上:LoggerAppender以及Layout。

    Logger类属于logback-classic模块,另外两个接口属于logback-core模块,作为一个通用模块,在logback-core模块中并没有logger的概念。

    Logger context

    任何日志API优于普通System.out.println的第一个也是最重要的优势在于:日志API能够禁用特定的log语句,同时允许其他log语句不受阻碍地打印。这暗示着log语句按照开发人员制定的标准分成若干类。在logback-classic中,这种分类是logger的固有部分。所有logger都共同隶属于一个LoggerContext,后者负责制造logger以及将它们排列在树状层次结构中。

    logger都是有名字的实体。它们的名字是大小写敏感,并且遵循分层命名规则。例如说,名为“com.mycompany”的logger被称作名为“com.mycompany.xxx”的logger的父母(直接关系!)。类似地,“com”被称为“com.mycompany.xxx”的祖先(隔代关系)。

    root logger位于logger的树状层次结构的顶端(根部)。它的特殊之处在于它是任何logger树状层次结构的一部分。像其它Logger一样,可以通过名字拿到它:

            Logger rootLogger = LoggerFactory.getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME);
            rootLogger.debug("大家好!我是root logger");

    Effective Level(有效等级)又名Level Inheritance

    logger可以被赋予等级,可以分配的等级有:TRACE, DEBUG, INFO, WARN和ERROR。Level类是final的,不能有子类!文档上的言下之意貌似是有更灵活的方式来改变Level类。如果,一个logger没有被赋予等级,则它的有效等级与和它最近的被赋予等级的祖先logger一样。言下之意就是,所有logger虽然不一定都有赋予等级,但是都是具有“有效等级”的(继承自祖先)。参考图:

    强调:子logger继承的是最近祖先的assigned level,而不是effective level!

    Printing methods and the basic selection rule

    形如logger.info("xxxxxxxx...")是一个等级为info的log请求。只有在log请求的等级高于或者等于它的logger的有效等级时才是“启用(enabled )”的,否则称该请求被禁用(disabled)。

    这个规则是logback的核心。它设定的级别(logger有效等级,log请求等级)顺序如下:TRACE < DEBUG < INFO < WARN < ERROR。

    basic selection rule例子程序:

    package org.sample.logback;
    
    import org.junit.Test;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    
    public class LogbackTest {
    
        @Test
        public void testLogback() {
            // root logger的默认赋予等级,也就是有效等级为DEBUG
            Logger rootLogger = LoggerFactory.getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME);
    
            // This request is disabled, because TRACE < DEBUG.
            rootLogger.trace("trace:大家好!我是root logger");
            // This request is enabled, because DEBUG >= DEBUG
            rootLogger.debug("debug:大家好!我是root logger");
            // This request is enabled, because INFO >= DEBUG
            rootLogger.info("info:大家好!我是root logger");
            // This request is enabled, because WARN >= DEBUG
            rootLogger.warn("warn:大家好!我是root logger");
    
            /*
            output=
            14:28:37.443 [main] DEBUG ROOT - debug:大家好!我是root logger
            14:28:37.448 [main] INFO ROOT - info:大家好!我是root logger
            14:28:37.448 [main] WARN ROOT - warn:大家好!我是root logger
            PS. 唯独不见trace
             */
        }
    }

    Retrieving Loggers - - 检索logger

    名字相同的logger就是同一个logger,给定logger的名字可以在应用程序中的任何地方拿到这个logger。

    按照生物学的观点,老子总是得先于孩子的。但是logback有那么点不同,它允许儿子先初始化、老子之后初始化,不过两个logger的父子关系在老子初始化后也会自动关联上去,让我们来看一个例子:

    package org.sample.logback;
    
    import ch.qos.logback.classic.Level;
    import org.junit.Test;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    
    public class LogbackTest {
    
        @Test
        public void testLogback() {
            Logger son = LoggerFactory.getLogger("father.son"); // 儿子先进行初始化
            // son默认继承root logger的赋予等级,也就是son的有效等级是DEBUG
            son.trace("help!"); // 这个log请求未被启用,因为 TRACE < DEBUG
    
            // Level类被定义在ch.qos.logback.classic里。所以必须用ch.qos.logback.classic.Logger才能设置它的level
            ch.qos.logback.classic.Logger father =
                    (ch.qos.logback.classic.Logger) LoggerFactory.getLogger("father"); 
            // 这个时候,初始化儿子他爹,将自动进行关联
            father.setLevel(Level.TRACE); // 这样儿子的有效等级也变成TRACE了
    
            son.trace("help!"); // 结果就是上一句不打印,这一句打印
        }
    }

    当然,在通常情况下,logger的配置在程序初始化的时候就完成了(通过读取配置文件)。用类全名来命名logger仍然是目前已知的最好的策略。

    Appenders and Layouts

    允许选择性地启用和禁用log请求只是logback功能的一部分。Logback还允许将log请求打印到多个目的地。用logback里的术语来讲,日志的“输出目的地”被称为appender。一个logger可以有多个appender。

    可以通过addAppender方法把一个appender添加到logger上。对于给定logger的每个enable的log请求都将被转发给该logger中的所有appender以及层次结构中更高所有appender(祖先、父母辈的appender)。换句话说,appender具有可加性。儿子能够共享老子(以及祖先,前提是老子没有setAdditive(false);)的所有appender。举个例子,在这里忽略root logger的存在,假设老子logger有且仅有一个输出到文件的appender,而儿子logger有且仅有一个输出到控制台的appender,那么老子就只能打印到文件,而儿子打印到控制台打印到文件,如果有一天儿子不想再打印到文件了,只要setAdditive(false);就可以了,并且孙子(儿子的儿子)也不会打印到文件(等于儿子全家都和老子断绝关系了。)

    用户通常并不满足于决定“输出目的地”,还希望能够随意操控打印格式。要做到这一点很简单,只需把layout和相应的appender关联起来就ok了。layout负责按照用户的期望格式化log请求。PatternLayout是标准logback发行版的一部分,它允许用户根据类似于C语言printf函数的转换模式指定输出格式。

    Parameterized logging参数化log请求

    不用这样写:

    if(logger.isDebugEnabled()) { 
      logger.debug("Entry number: " + i + " is " + String.valueOf(entry[i]));
    }

    这样写就可以了↓

    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);

    多个参数也是支持的。如下:

    package org.sample.logback;
    
    import org.junit.Test;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    
    public class LogbackTest {
    
        @Test
        public void testLogback() {
            Logger logger = LoggerFactory.getLogger(LogbackTest.class);
            logger.debug("Hello {} , how are {}", "xkfx", "you");
            // => 15:57:16.196 [main] DEBUG org.sample.logback.LogbackTest - Hello xkfx , how are you
            logger.debug("Hello {} , how are {}, what is your {}", "xkfx", "you", "name");
            // =>15:58:03.783 [main] DEBUG org.sample.logback.LogbackTest - Hello xkfx , how are you, what is your name
    
            // 还可以这样↓
            Object[] paramArray = {"xkfx", "you", "name"};
            logger.debug("Hello {} , how are {}, what is your {}", paramArray); // 输出同上
        }
    }

    A peek under the hood在引擎盖下面偷看一下。。

    介绍完了logback的几个基本组件,我们已经可以描述logback框架在用户发起log请求时所采取的步骤。现在让我们分析一下当用户调用名为com.wombat的logger的info()方法时,logback所采取的步骤。接下来用单步调试跟踪一下logger.info("Hello World");

    1. Get the filter chain decision

    首先step into进入logger.info方法:

    出现一个看到名字想不出来具体含义的方法,继续step into进入filterAndLog_0_Or3Plus方法:

        private void filterAndLog_0_Or3Plus(String localFQCN, Marker marker, Level level, String msg, Object[] params, Throwable t) {
            /*
            如果filter chain存在,则TurboFilter链将被调用
            1、Turbo过滤器可以设置上下文范围的阈值,
            2、或者根据特定的信息(Marker、Level、Logger、message以及与每个log请求关联的Throwable等)
            过滤掉某些事件。
            (PS. 硬翻,可能有错)
             */
            FilterReply decision = this.loggerContext.getTurboFilterChainDecision_0_3OrMore(marker, this, level, msg, params, t);
            if(decision == FilterReply.NEUTRAL) {
                if(this.effectiveLevelInt > level.levelInt) {
                    // 这里就比较顾名思义了。。
                    // 因为level.levelInt=20000(INFO)> 有效等级DEBUG的10000
                    // 所以logging request不会在这里被抛弃
                    return;
                }
            } else if(decision == FilterReply.DENY) {
                return;
            }
            this.buildLoggingEventAndAppend(localFQCN, marker, level, msg, params, t);
        }

    => 补充turbo filter的相关资料:在Logback-classic中提供两个类型的filters , 一种是regular filters,另一种是turbo filter。regular filters是与appender 绑定的,而turbo filter是与与logger context(logger上下文绑定的,区别就是,turbo filter过滤所有logging request(log请求),而regular filter只过滤某个appender的logging request。(作者:巫巫巫政霖,原文:https://blog.csdn.net/Doraemon_wu/article/details/52072360

    2. Apply the basic selection rule

    就是指上面代码中的这一段:

    如果decision == FilterReply.ACCEPT的话,就没有这个步骤了。

    3. Create a LoggingEvent object

    step into进入buildLoggingEventAndAppend(localFQCN, marker, level, msg, params, t);

    参数情况:

    我们将用这些参数,以及当前时间、当前线程、各种类的相关数据、MDC等构造一个LoggingEvent对象。

        private void buildLoggingEventAndAppend(String localFQCN, Marker marker, Level level, String msg, Object[] params, Throwable t) {
            LoggingEvent le = new LoggingEvent(localFQCN, this, level, msg, t, params);
            le.setMarker(marker);
            this.callAppenders(le);
        }

    请注意,其中一些字段是惰性初始化的,只有在实际需要时才初始化。MDC用于附加上下文信息来修饰logging request。MDC在后面的一章中讨论。

    4. Invoking appenders

    在创建LoggingEvent对象之后,logback将调用所有当前logger可用的appender(就是自己的、老子的、祖先的)的doAppend()方法。step into进入callAppenders(ILogginEvent event);↓

        public void callAppenders(ILoggingEvent event) {
            int writes = 0;
    
            for(Logger l = this; l != null; l = l.parent) {
                writes += l.appendLoopOnAppenders(event);
                if(!l.additive) {
                    break;
                }
            }
    
            if(writes == 0) {
                this.loggerContext.noAppenderDefinedWarning(this);
            }
    
        }

    这个for循环写得也是非常酷炫。。首先检查com.wombat的AppenderAttachableImpl,发现没有,再找com的aai,发现也没有,最后找root logger的aai,终于找到了,于是对这个aai(应该是和logger的特定appender共同存亡的一个对象)调用AppenderAttachableImpl类的appendLoopOnAppenders(event)方法,如下所示,外围的appendLoopOnAppenders方法是属于ch.qos.logback.classic.Logger类的:

        private int appendLoopOnAppenders(ILoggingEvent event) {
            return this.aai != null?this.aai.appendLoopOnAppenders(event):0;
        }

    在appendLoopOnAppenders的方法体里拿到对应appender的引用,调用doAppend方法(传入logginevent对象作为参数)。所有logback发行版里的appender都继承自抽象类AppenderBase,这个抽象类在synchronized块里实现了doAppend方法,从而确保了线程安全。doAppend方法体中同样会调用appender所关联的过滤器,同样是会拿到一个FilterReply来决定要不要放弃掉这个logging request。

    但是我却进入了一个叫做UnsynchronizedAppenderBase的doAppend方法里,并没有看到synchronized块:

        public void doAppend(E eventObject) {
            if(!Boolean.TRUE.equals(this.guard.get())) {
                try {
                    this.guard.set(Boolean.TRUE);
                    if(this.started) {
                        if(this.getFilterChainDecision(eventObject) == FilterReply.DENY) {
                            return;
                        }
    
                        this.append(eventObject);
                        return;
                    }
    
                    if(this.statusRepeatCount++ < 3) {
                        this.addStatus(new WarnStatus("Attempted to append to non started appender [" + this.name + "].", this));
                    }
                } catch (Exception var6) {
                    if(this.exceptionCount++ < 3) {
                        this.addError("Appender [" + this.name + "] failed to append.", var6);
                    }
    
                    return;
                } finally {
                    this.guard.set(Boolean.FALSE);
                }
    
            }
        }
    doAppend

    5. Formatting the output

    被调用的appender负责格式化logging event。然而,有些(但不是全部)appender将格式化logging event的任务委托给一个layout。layout格式化LoggingEvent实例并以字符串的形式返回结果。注意,有些appender(比如SocketAppender)不会将logging event转换为字符串,而是序列化它。因此,它们既没有也不需要layout。

    6. Sending out the LoggingEvent

    logging event完全格式化后,由每个appender发送到其目的地。

    贴张自己做的图:

    Performance

    详细参考原文末尾

  • 相关阅读:
    基于HttpListener的web服务器
    基于TcpListener的web服务器
    一个简单的web服务器
    c# 6.0新特性(二)
    c# 6.0新特性(一)
    c#之Redis实践list,hashtable
    html5摇一摇[转]
    在Microsoft-IIS/10.0上面部署mvc站点的时候,出现404的错误
    [实战]MVC5+EF6+MySql企业网盘实战(28)——其他列表
    让DELPHI自带的richedit控件显示图片
  • 原文地址:https://www.cnblogs.com/xkxf/p/9971665.html
Copyright © 2011-2022 走看看