【Logback】<logger>、<root>标签详解

文章目录

  • 背景
  • 一、\<logger>使用
    • 1.1、使用示例
    • 1.1、属性配置说明 & 演示
      • 1.1.1、name
      • 1.1.2、level
      • 1.1.3、additivity
        • 1.1.3.1、效果演示:additivity=true
        • 1.1.3.1、效果演示:additivity=”false”
    • 1.2 appender-ref
  • 二、\<root>使用
    • 2.1、属性
  • 三、解析
    • 3.1、\<logger>链表
      • 3.2、root是一个名为 ROOT 的特殊logger,其 parent 为 null
      • 3.3、 name属性和\<logger>继承关系
    • 3.3、level属性继承 和 优先级
    • 3.4、additivity属性
      • 3.4.1、源码分析
      • 3.4.2、演示
  • 四、日志规范
  • 五、参考资料

背景

排查一个项目的问题,发现打印了一堆重复日志…,为避免日志爆盘被投诉,写一下logback的使用和解析。 因为重复日志问题引发的,所以本文优先<logger>、<root>标签,其余的后续在写…

版本:logback-1.2.3

一、<logger>使用

如果需要定制指定模块或者类的日志信息,可以通过<logger>标签来实现,通过其name属性指定包或者类全路径即可

<logger>是有父子关系的
<root>是一个特殊的<logger> ,其name="ROOT",是所有<logger>的祖先节点,先了解,下面会讲。
父子关系是根据name属性的层级来确定的

<logger name="com.qbhj.logback.controller">,其父子关系如下(先了解结论,下面分析中会讲):

<logger name="ROOT">
└─<logger name="com">
│ └─<logger name="com.qbhj">
│ │ └─<logger name="com.qbhj.logback">
│ │ │ └─<logger name="com.qbhj.logback.controller">

1.1、使用示例

<logger name="com.qbhj" level="INFO" additivity="true">
    <!-- 可配置 N 个appender-ref -->
    <appender-ref ref="CONSOLE"/>
    <appender-ref ref="appender1"/>
    <appender-ref ref="appender1"/>
</logger>

<logger name="com.qbhj.logback" additivity="true">
    <appender-ref ref="CONSOLE"/>
</logger>

<logger name="com.qbhj.logback.empty" additivity="true">
</logger>

Tips:

1、<logger>可以配置 0-N
2、<logger>属性子元素<appender-ref>2个部分构成,日志输出是<appender-ref>所引用的<appender>来执行的
3、<logger>本身是一个链表,有一个父节点和N个子节点,具体在1.3、解析中详细说明

1.1、属性配置说明 & 演示

属性名属性值是否必填说明
name包或者类路径如: com.qbhj 或 com.qbhj.Test.class
levelOFF、ERROR、WARN、INFO、DEBUG、TRACE、ALL日志级别 ,默认继承父<logger>的日志级别
additivitytrue | false是否叠加,默认true ,建议配置为false,见日志规范

1.1.1、name

指定包或者类全路径,则该<logger>的配置就对其生效,默认使用<root>的日志配置

1.1.2、level

日志级别
Tips:

<logger>日志级别level属性是有继承关系,其优先级如下(此表摘自logback官网):

Logger nameAssigned LevelEffective Level
rootDEBUGDEBUG
chapters.configurationINFOINFO
chapters.configuration.MyApp3nullINFO
chapters.configuration.FooDEBUGDEBUG

总结:
若自身没设置 level 则使用其父Logger的level
若父级都没设置,则使用<root>节点的,<root>不配置则其默认级别level为DEBUG

1.1.3、additivity

是否允许父级<logger>打印自身name指定范围内的日志。若允许,则会重复打印日志。

1.1.3.1、效果演示:additivity=true

1)、打印日志代码

package com.qbhj.logback.controller;
// import .....

@Slf4j
@RestController
public class LogController {
    @GetMapping("/additivity")
    public void additivity(@RequestParam(defaultValue = "1") Integer num) {
        System.out.println("###################################### num: " + num);
        for (int i = 0; i < num; i++) {
            log.info("/additivity......");
        }
    }
}

2.1)、日志配置,只配置root,无<logger>

    <root level="INFO">
        <!-- 控制台 -->
        <appender-ref ref="CONSOLE"/>
    </root>

2.2)、结果,日志只打印了一遍

###################################### num: 1
20:36:06.170  INFO --- [     http-nio-8080-exec-1] c.qbhj.logback.controller.LogController  : /additivity......

2.3)、说明

所有Logger都没有配置appender,所以无日志输出
root配置了appender=CONSOLE,所以控制台会打印日志

Loggerappenderadditivity(默认true打印日志是否被调用
<logger name="ROOT">CONSOLEtrue
└─<logger name="com">true
│ └─<logger name="com.qbhj">true
│ │ └─<logger name="com.qbhj.logback">true
│ │ │ └─<logger name="com.qbhj.logback.controller">true

3.1)、增加<logger>配置name=com.qbhj.logback.controller<root><logger>中都有配置控制台(CONSOLE)输出,共配置了2遍

    <logger name="com.qbhj.logback.controller" level="INFO" additivity="true">
        <!-- 控制台 -->
        <appender-ref ref="CONSOLE"/>
    </logger>

    <root level="INFO">
        <!-- 控制台 -->
        <appender-ref ref="CONSOLE"/>
    </root>

3.2)、结果,日志打了2遍,是<logger><root>各自打一遍

###################################### num: 1
20:54:51.165  INFO --- [     http-nio-8080-exec-2] c.qbhj.logback.controller.LogController  : /additivity......
20:54:51.165  INFO --- [     http-nio-8080-exec-2] c.qbhj.logback.controller.LogController  : /additivity......

3.3)、说明

<logger name="com.qbhj.logback.controller">配置了appender=CONSOLE ,控制台打印日志
root配置了appender=CONSOLE,所以控制台会打印日志

Loggerappenderadditivity(默认true打印日志是否被调用
<logger name="ROOT">CONSOLEtrue
└─<logger name="com">true
│ └─<logger name="com.qbhj">true
│ │ └─<logger name="com.qbhj.logback">true
│ │ │ └─<logger name="com.qbhj.logback.controller">CONSOLEtrue

4.1)、再增加<logger>配置name=com.qbhj.logback logback包肯定包含 LogController类的,即<root><logger>中都有配置控制台(CONSOLE)输出,共配置了3遍

    <logger name="com.qbhj.logback.controller" level="INFO">
        <!-- 控制台 -->
        <appender-ref ref="CONSOLE"/>
    </logger>
    <logger name="com.qbhj.logback" level="INFO">
        <!-- 控制台 -->
        <appender-ref ref="CONSOLE"/>
    </logger>
    <root level="INFO">
        <!-- 控制台 -->
        <appender-ref ref="CONSOLE"/>
    </root>

4.2)、结果,日志打了3遍,2个<logger><root>各自打一遍

###################################### num: 1
20:58:23.914  INFO --- [     http-nio-8080-exec-1] c.qbhj.logback.controller.LogController  : /additivity......
20:58:23.914  INFO --- [     http-nio-8080-exec-1] c.qbhj.logback.controller.LogController  : /additivity......
20:58:23.914  INFO --- [     http-nio-8080-exec-1] c.qbhj.logback.controller.LogController  : /additivity......

4.3)、说明

2个 <logger >配置了appender=CONSOLE ,控制台打印日志2遍
root配置了appender=CONSOLE,控制台会打印日志

Loggerappenderadditivity(默认true打印日志是否被调用
<logger name="ROOT">CONSOLEtrue
└─<logger name="com">true
│ └─<logger name="com.qbhj">true
│ │ └─<logger name="com.qbhj.logback">CONSOLEtrue
│ │ │ └─<logger name="com.qbhj.logback.controller">CONSOLEtrue
1.1.3.1、效果演示:additivity=“false”

1.1)、同样的配置,<logger>全部改为additivity=“false”

    <logger name="com.qbhj.logback.controller" level="INFO" additivity="false">
        <!-- 控制台 -->
        <appender-ref ref="CONSOLE"/>
    </logger>
    <logger name="com.qbhj.logback" level="INFO" additivity="false">
        <!-- 控制台 -->
        <appender-ref ref="CONSOLE"/>
    </logger>
    <root level="INFO">
        <!-- 控制台 -->
        <appender-ref ref="CONSOLE"/>
    </root>

1.2)、结果:日志只打印了一遍

###################################### num: 1
21:00:24.598  INFO --- [     http-nio-8080-exec-1] c.qbhj.logback.controller.LogController  : /additivity......

1.3)、说明

<logger name="com.qbhj.logback.controller" level="INFO" additivity="false"> ,配置了appender=CONSOLE所以打印日志。其additivity=“false”,不调用父级,结束

Loggerappenderadditivity(默认true打印日志是否被调用
<logger name="ROOT">CONSOLEtrue
└─<logger name="com">true
│ └─<logger name="com.qbhj">true
│ │ └─<logger name="com.qbhj.logback">CONSOLEfalse
│ │ │ └─<logger name="com.qbhj.logback.controller">CONSOLEfalse

1.2 appender-ref

示例

<!-- 建议配置为false,避免日志重复输出 -->
<logger name="com.qbhj" level="INFO" additivity="false">
    <!-- 可配置 N 个appender-ref -->
    <appender-ref ref="CONSOLE"/>
    <appender-ref ref="appender1"/>
    <appender-ref ref="appender1"/>
</logger>

<logger name="com.qbhj.logback" additivity="false">
    <appender-ref ref="CONSOLE"/>
</logger>

<logger name="com.qbhj.logback.empty" additivity="false">
</logger>

1、每个logger可以配置任意(含0)个<appender-ref>
2、语法:ref=appender[name],声明 <appender>name属性值

二、<root>使用

2.1、属性

属性名属性值是否必填说明
levelOFF、ERROR、WARN、INFO、DEBUG、TRACE、ALL日志级别,默认DEBUG

<root>的本质是一个名为ROOT的特殊logger,即<logger name="ROOT">
root是所有logger的根节点

<root level="DEBUG">
    <!-- 控制台 -->
    <appender-ref ref="CONSOLE"/>

    <!-- 文件 -->
    <appender-ref ref="FILE_ALL"/>
    <appender-ref ref="FILE_WARN"/>
    <appender-ref ref="FILE_ERROR"/>

</root>

三、解析

3.1、<logger>链表

1、<logger>是一个链表,有父节点和子节点
2、除<root>父节点为null外,所有logger一定有父节点,<root>为所有logger的根节点

ch.qos.logback.classic.Logger

package ch.qos.logback.classic;
//..........

public final class Logger implements org.slf4j.Logger, LocationAwareLogger, AppenderAttachable<ILoggingEvent>, Serializable {
    // ..........

    /**
     * The name of this logger
     */
    private String name;

    // The assigned levelInt of this logger. Can be null.
    transient private Level level;

    // The effective levelInt is the assigned levelInt and if null, a levelInt is
    // inherited form a parent.
    transient private int effectiveLevelInt;


// root 为所有logger的祖先(根)节点
    /**
     * The parent of this category. All categories have at least one ancestor
     * which is the root category.
     */
    transient private Logger parent;

    /**
     * The children of this logger. A logger may have zero or more children.
     */
    transient private List<Logger> childrenList;

// additive 默认 true
    transient private boolean additive = true;

    final transient LoggerContext loggerContext;
    // ..........
    
    private boolean isRootLogger() {
      // only the root logger has a null parent
// 只有root的parent为null
      return parent == null;
    }
   
    // .......... 
}

3.2、root是一个名为 ROOT 的特殊logger,其 parent 为 null

LoggerContext 初始化的时候就在其构造函数中创建了root了
root默认日志级别为 DEBUG

ch.qos.logback.classic.LoggerContext

public class LoggerContext extends ContextBase implements ILoggerFactory, LifeCycle {

    /** Default setting of packaging data in stack traces */
    public static final boolean DEFAULT_PACKAGING_DATA = false;

    final Logger root;
    private int size;
    private int noAppenderWarning = 0;
    final private List<LoggerContextListener> loggerContextListenerList = new ArrayList<LoggerContextListener>();

    private Map<String, Logger> loggerCache;
    // .............
    public LoggerContext() {
        super();
        this.loggerCache = new ConcurrentHashMap<String, Logger>();

        this.loggerContextRemoteView = new LoggerContextVO(this);

// 初始化 <root>
        /**
        package org.slf4j;
        public interface Logger {
            String ROOT_LOGGER_NAME = "ROOT";
        */
        this.root = new Logger(Logger.ROOT_LOGGER_NAME, null, this);
// ROOT的默认日志级别 DEBUG
        this.root.setLevel(Level.DEBUG);

        loggerCache.put(Logger.ROOT_LOGGER_NAME, root);
        initEvaluatorMap();
        size = 1;
        this.frameworkPackages = new ArrayList<String>();
    }
    // .......... 

}

3.3、 name属性和<logger>继承关系

1、会根据name属性的值,逐级创建子logger,如com.qbhj.Test.class中 log.info(“print log…”),一共会创建3个logger

1)、com
2)、com.qbhj
3)、com.qbhj.Test

ch.qos.logback.classic.LoggerContext#getLogger(java.lang.Class<?>)

    public final Logger getLogger(final Class<?> clazz) {
        return getLogger(clazz.getName());
    }
    @Override
    public final Logger getLogger(final String name) {

        if (name == null) {
            throw new IllegalArgumentException("name argument cannot be null");
        }
// root直接返回
        // if we are asking for the root logger, then let us return it without
        // wasting time
        if (Logger.ROOT_LOGGER_NAME.equalsIgnoreCase(name)) {
            return root;
        }

// 非root,先拿到root,然后逐级创建其子logger
        int i = 0;
        Logger logger = root;

        // check if the desired logger exists, if it does, return it
        // without further ado.
        Logger childLogger = (Logger) loggerCache.get(name);
        // if we have the child, then let us return it without wasting time
        if (childLogger != null) {
            return childLogger;
        }

        // if the desired logger does not exist, them create all the loggers
        // in between as well (if they don't already exist)
        String childName;
        while (true) {
            int h = LoggerNameUtil.getSeparatorIndexOf(name, i);
            if (h == -1) {
                childName = name;
            } else {
                childName = name.substring(0, h);
            }
            // move i left of the last point
            i = h + 1;
            synchronized (logger) {  // 此logger为 root
                childLogger = logger.getChildByName(childName);
                if (childLogger == null) {
// 调用Logger创建子节点
                    childLogger = logger.createChildByName(childName);
                    loggerCache.put(childName, childLogger);
                    incSize();
                }
            }
            logger = childLogger;
            if (h == -1) {
                return childLogger;
            }
        }
    }


// ch.qos.logback.classic.Logger#createChildByName
    Logger createChildByName(final String childName) {
        int i_index = LoggerNameUtil.getSeparatorIndexOf(childName, this.name.length() + 1);
        if (i_index != -1) {
            throw new IllegalArgumentException("For logger [" + this.name + "] child name [" + childName
                            + " passed as parameter, may not include '.' after index" + (this.name.length() + 1));
        }

        if (childrenList == null) {
            childrenList = new CopyOnWriteArrayList<Logger>();
        }
        Logger childLogger;
        childLogger = new Logger(childName, this, this.loggerContext);
        childrenList.add(childLogger);
        childLogger.effectiveLevelInt = this.effectiveLevelInt;
        return childLogger;
    }

3.3、level属性继承 和 优先级

1、若自身level为空,则继承父级Logger的level值

ch.qos.logback.classic.Logger#setLevel

    public synchronized void setLevel(Level newLevel) {
        if (level == newLevel) {
            // nothing to do;
            return;
        }
        // <root> 的 level 必填
        if (newLevel == null && isRootLogger()) {
            throw new IllegalArgumentException("The level of the root logger cannot be set to null");
        }

        level = newLevel;
// 若level为空,则使用其父logger的有效level
        if (newLevel == null) {
            effectiveLevelInt = parent.effectiveLevelInt;
            newLevel = parent.getEffectiveLevel();
        } else {
            effectiveLevelInt = newLevel.levelInt;
        }

        if (childrenList != null) {
            int len = childrenList.size();
            for (int i = 0; i < len; i++) {
                Logger child = (Logger) childrenList.get(i);
                // tell child to handle parent levelInt change
                child.handleParentLevelChange(effectiveLevelInt);
            }
        }
        // inform listeners
        loggerContext.fireOnLevelChange(this, newLevel);
    }

3.4、additivity属性

3.4.1、源码分析

1、调用自身的appender进行日志输出
2、若additive=false,则跳出循环,否则调用父logger的appender进行日志输出

ch.qos.logback.classic.Logger#callAppenders

// aai 就是<appender-ref>中指定的<appender> AppenderAttachableImpl
    transient private AppenderAttachableImpl<ILoggingEvent> aai;

// additive 默认为 true
    transient private boolean additive = true;
    // ............
    
    public void callAppenders(ILoggingEvent event) {
        int writes = 0;
// 1、遍历当前logger的父logger
        for (Logger l = this; l != null; l = l.parent) {
// 2、若当前logger配置了append,则遍历append输出日志信息
            writes += l.appendLoopOnAppenders(event);
// 3、若当前logger的属性additive=false 则跳出循环,不再调用父级
            if (!l.additive) {
                break;
            }
        }
        // No appenders in hierarchy
        if (writes == 0) {
            loggerContext.noAppenderDefinedWarning(this);
        }
    }

    private int appendLoopOnAppenders(ILoggingEvent event) {
// 若当前logger配置了append,则调用其进行日志输出
        if (aai != null) {
            return aai.appendLoopOnAppenders(event);
        } else {
            return 0;
        }
    }

3.4.2、演示

还是上述的例子

1)、只把name=“com.qbhj.logback.controller” 的logger,additivity属性改为”true”

    <logger name="com.qbhj.logback.controller" level="INFO" additivity="true">
        <!-- 控制台 -->
        <appender-ref ref="CONSOLE"/>
    </logger>
    <logger name="com.qbhj.logback" level="INFO" additivity="false">
        <appender-ref ref="CONSOLE"/>
    </logger>
    <root level="INFO">
        <!-- 控制台 -->
        <appender-ref ref="CONSOLE"/>
    </root>

1.1)、结果:打印两遍

###################################### num: 1
21:02:30.800  INFO --- [     http-nio-8080-exec-2] c.qbhj.logback.controller.LogController  : /additivity......
21:02:30.800  INFO --- [     http-nio-8080-exec-2] c.qbhj.logback.controller.LogController  : /additivity......

说明:

<logger name=“com.qbhj.logback.controller” level=“INFO” additivity=“true”> ,且配置了appender=CONSOLE所以打印日志。其additivity=“true”,调用父级
<logger name="com.qbhj.logback" level="INFO" additivity="false"> ,且配置了appender=CONSOLE,所以打印日志。其additivity=“false”,跳出循环,结束

Loggerappenderadditivity(默认true打印日志是否被调用
<logger name="ROOT">CONSOLEtrue
└─<logger name="com">true
│ └─<logger name="com.qbhj">true
│ │ └─<logger name="com.qbhj.logback">CONSOLEfalse
│ │ │ └─<logger name="com.qbhj.logback.controller">CONSOLEtrue

2)、只把name=“com.qbhj.logback.controller” 的logger,additivity属性改为”false”, name=”com.qbhj.logback”的logger改为false

    <logger name="com.qbhj.logback.controller" level="INFO" additivity="false">
        <!-- 控制台 -->
        <appender-ref ref="CONSOLE"/>
    </logger>
    <logger name="com.qbhj.logback" level="INFO" additivity="true">
        <appender-ref ref="CONSOLE"/>
    </logger>
    <root level="INFO">
        <!-- 控制台 -->
        <appender-ref ref="CONSOLE"/>
    </root>

2.1)、结果:打印1遍

###################################### num: 1
21:04:10.603  INFO --- [     http-nio-8080-exec-1] c.qbhj.logback.controller.LogController  : /additivity......

说明:

<logger name=“com.qbhj.logback.controller” level=“INFO” additivity=“false”> ,且配置了appender=CONSOLE所以打印日志。其additivity=“false”,跳出循环,结束

Loggerappenderadditivity(默认true打印日志是否被调用
<logger name="ROOT">CONSOLEtrue
└─<logger name="com">true
│ └─<logger name="com.qbhj">true
│ │ └─<logger name="com.qbhj.logback">CONSOLEtrue
│ │ │ └─<logger name="com.qbhj.logback.controller">CONSOLEfalse

四、日志规范

阿里开发手册(黄山版)中约定如下:
二、异常日志
(三) 日志规约

7.【强制】避免重复打印日志,浪费磁盘空间,务必在日志配置文件中设置 additivity=false
正例:<logger name=“com.taobao.dubbo.config” additivity=“false”>

五、参考资料

阿里开发手册(黄山版).pdf
https://logback.qos.ch/manual/configuration.html#rootElement

文章出处登录后可见!

已经登录?立即刷新

共计人评分,平均

到目前为止还没有投票!成为第一位评论此文章。

(0)
青葱年少的头像青葱年少普通用户
上一篇 2023年12月20日
下一篇 2023年12月20日

相关推荐