掘金 后端 ( ) • 2024-03-28 10:25

theme: smartblue

本篇文章中涉及到的所有代码都已经上传到gitee中: https://gitee.com/sss123a/log/tree/master/matio-slf4j/slf4j_logback/src/main/java/com/matio/logback

# logback官方手册

logback启动流程

image.png

核心入口:

ch.qos.logback.classic.spi.LogbackServiceProvider#initialize();

由slf4j通过spi机制扫描到LogbackServiceProvider后调用initialize()方法,不清楚的可以去看 # 05.java日志之slf4j门面 中的日志绑定原理

下面具体看看initialize()方法内部逻辑:

private LoggerContext defaultLoggerContext;  
private IMarkerFactory markerFactory;  
private MDCAdapter mdcAdapter;

@Override  
public void initialize() {  
    // LoggerContext本质上是一个org.slf4j.ILoggerFactory
    defaultLoggerContext = new LoggerContext();  
    defaultLoggerContext.setName("default");
    // 找配置文件去初始化LoggerContext
    initializeLoggerContext();  
    
    // 发布事件给LoggerContextListener
    defaultLoggerContext.start();  
    
    markerFactory = new BasicMarkerFactory();  
    mdcAdapter = new LogbackMDCAdapter();  
}

private void initializeLoggerContext() {  
    new ContextInitializer(defaultLoggerContext).autoConfig();  
}

基于ContextInitializer类初始化LoggerContext

public void autoConfig(ClassLoader classLoader) throws JoranException {  
    String versionStr = EnvUtil.logbackVersion();  
    if (versionStr == null) {  
        versionStr = CoreConstants.NA;  
    } 
    // logback内部日志
    loggerContext.getStatusManager().add(new InfoStatus("This is logback-classic version " + versionStr, loggerContext));
    // 读取系统属性logback.statusListenerClass尝试注册StatusListener
    StatusListenerConfigHelper.installIfAsked(loggerContext);  
    // 通过SPI机制去加载classpath中META-INF/services/ch.qos.logback.classic.spi.Configurator文件内容
    List<Configurator> configuratorList = ClassicEnvUtil.loadFromServiceLoader(Configurator.class, classLoader);  
    sortByPriority(configuratorList);  

    for (Configurator c : configuratorList) {  
        c.setContext(loggerContext);  
        Configurator.ExecutionStatus status = c.configure(loggerContext);  
        if (status == Configurator.ExecutionStatus.DO_NOT_INVOKE_NEXT_IF_ANY) {  
           return;  
        }  
    }  
    // at this stage invoke basicConfigurator  
    fallbackOnToBasicConfigurator();
}

以上代码执行主要分三步:

  1. 读取系统属性logback.statusListenerClass尝试注册StatusListener,用来输出logback内部日志status
  2. 通过SPI机制去加载classpath中META-INF/services/ch.qos.logback.classic.spi.Configurator文件内容,默认返回ch.qos.logback.classic.util.DefaultJoranConfigurator,见下图:

image.png

  1. 利用DefaultJoranConfigurator解析xml配置文件,初始化logback上下文,具体逻辑见下:
public final void doConfigure(final InputSource inputSource) throws JoranException {  
  
    context.fireConfigurationEvent(newConfigurationStartedEvent(this));  
    long threshold = System.currentTimeMillis();  

    SaxEventRecorder recorder = populateSaxEventRecorder(inputSource);  
    List<SaxEvent> saxEvents = recorder.getSaxEventList();  
    if (saxEvents.isEmpty()) {  
        addWarn("Empty sax event list");  
        return;  
    }  
    Model top = buildModelFromSaxEventList(recorder.getSaxEventList());  
    if (top == null) {  
        addError(ErrorCodes.EMPTY_MODEL_STACK);  
        return;  
    }  
    sanityCheck(top);  
    processModel(top);  

    // no exceptions at this level  
    StatusUtil statusUtil = new StatusUtil(context);  
    if (statusUtil.noXMLParsingErrorsOccurred(threshold)) {  
        addInfo("Registering current configuration as safe fallback point");  
        registerSafeConfiguration(top);  
    }  
    context.fireConfigurationEvent(newConfigurationEndedEvent(this));
}

以上代码执行主要分三步:

  1. 发布一个ConfigurationEvent事件,表示准备开始解析xml配置文件了
context.fireConfigurationEvent(newConfigurationStartedEvent(this));
  1. 构建一个SaxEventRecorder解析xml,将xml文件解析成一个个的ch.qos.logback.core.joran.event.SaxEvent(这步不需要深究,直接忽略)
SaxEventRecorder recorder = populateSaxEventRecorder(inputSource);  
List<SaxEvent> saxEvents = recorder.getSaxEventList();
  1. 将这些SaxEvent转成一个个的ch.qos.logback.core.model.Model,转换规则见1ch.qos.logback.classic.joran.JoranConfigurator#addElementSelectorAndActionAssociations(RuleStore)`:
Model top = buildModelFromSaxEventList(recorder.getSaxEventList());

调用addElementSelectorAndActionAssociations(rs);这行代码的时候会添加转换规则,如下:

image.png

  1. 基于以下规则,找到各个Model对应的ModelHandlerBase处理器,然后回调其ch.qos.logback.core.model.processor.ModelHandlerBase#handle()postHandle()方法

image.png

processModel(top);

public void processModel(Model model) {  
    buildModelInterpretationContext();  
    DefaultProcessor defaultProcessor = new DefaultProcessor(context, this.modelInterpretationContext);  
    // 添加model跟handler的映射
    addModelHandlerAssociations(defaultProcessor);  

    synchronized (context.getConfigurationLock()) {
        // 开始调用handler去处理model了
        defaultProcessor.process(model);  
    }  
}
  1. 发布一个ConfigurationEvent事件,表示xml配置文件解析完成了
context.fireConfigurationEvent(newConfigurationEndedEvent(this));

logback xml标签

前置知识点

  1. 属性的scope

有三种:local、context、system,默认为local

可以在本地范围、上下文范围或系统范围中定义用于插入的属性。本地作用域是默认的。虽然可以从操作系统环境中读取变量,但不可能写入操作系统环境。

local:具有局部作用域的属性从其在配置文件中的定义开始一直存在,直到所述配置文件的解释/执行结束。因此,每次解析和执行配置文件时,都会重新定义本地范围内的变量。

context:将具有上下文范围的属性插入到上下文中,并与上下文一样长或直到其被清除。一旦定义,上下文范围中的属性就是上下文的一部分。因此,它在所有日志事件中都可用,包括那些通过序列化发送到远程主机的事件。

system:会被保存到JVM的系统属性中,我们可以在我们的程序中使用System.getProperty()读取。

读取变量的顺序:首先在local中查找属性,其次在context中查找,第三在system中查找,最后在操作系统环境中查找。

  1. 变量的默认值、嵌套变量

在某些情况下,如果变量未声明或其值为 null,则可能希望变量具有默认值。与Bash shell中一样,可以使用“:-”运算符指定默认值 。例如"${aName:-golden}",假设名为aName的变量未定义,将被解释为“golden”。

一个变量的默认值可以引用另一个变量。例如,假设变量“id”未分配,并且变量“userid”被分配值“alice”,则表达式“${id :- ${userid}}”将返回“alice”。

configuration 根标签

<?xml version="1.0" encoding="UTF-8"?>
<configuration debug="false" scan="true" scanPeriod="60 seconds" packagingData="true">
    ...
</configuration>

支持以下四个属性:

  • debug:是否印出logback内部日志信息status,查看logback运行状态,系统属性logback.debug优先级更高。如果为true则跟在xml中注册一个OnConsoleStatusListener功能一致
  • scan:为true时启动一个定时任务ReconfigureOnChangeTask去监听该xml,如果发生改变,将会被重新加载
  • scanPeriod:定时任务间隔,当scan为true时有效,默认为1 minute,格式参考Duration#valueOf();
  • packagingData:默认false。如果为true,logback可以包括它输出的堆栈跟踪行的每一行的打包数据。打包数据由jar文件的名称和版本组成,堆栈跟踪行的类源自jar文件。打包数据对于识别软件版本控制问题非常有用。然而,计算成本相当高,尤其是在频繁抛出异常的应用程序中。以下是输出示例:
14:28:48.835 [btpool0-7] INFO  c.q.l.demo.prime.PrimeAction - 99 is not a valid value
java.lang.Exception: 99 is invalid
  at ch.qos.logback.demo.prime.PrimeAction.execute(PrimeAction.java:28) [classes/:na]
  at org.apache.struts.action.RequestProcessor.processActionPerform(RequestProcessor.java:431) [struts-1.2.9.jar:1.2.9]
  at org.apache.struts.action.RequestProcessor.process(RequestProcessor.java:236) [struts-1.2.9.jar:1.2.9]
  at org.apache.struts.action.ActionServlet.doPost(ActionServlet.java:432) [struts-1.2.9.jar:1.2.9]
  at javax.servlet.http.HttpServlet.service(HttpServlet.java:820) [servlet-api-2.5-6.1.12.jar:6.1.12]
  at org.mortbay.jetty.servlet.ServletHolder.handle(ServletHolder.java:502) [jetty-6.1.12.jar:6.1.12]
  at ch.qos.logback.demo.UserServletFilter.doFilter(UserServletFilter.java:44) [classes/:na]
  at org.mortbay.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1115) [jetty-6.1.12.jar:6.1.12]
  at org.mortbay.jetty.servlet.ServletHandler.handle(ServletHandler.java:361) [jetty-6.1.12.jar:6.1.12]
  at org.mortbay.jetty.webapp.WebAppContext.handle(WebAppContext.java:417) [jetty-6.1.12.jar:6.1.12]
  at org.mortbay.jetty.handler.ContextHandlerCollection.handle(ContextHandlerCollection.java:230) [jetty-6.1.12.jar:6.1.12]

也可以在java程序中手动启用/禁用打包数据

LoggerContext lc = (LoggerContext) LoggerFactory.getILoggerFactory(); lc.setPackagingDataEnabled(true);

原理:ConfigurationAction 解析成 ConfigurationModel 交给 ConfigurationModelHandler

contextName 上下文名称

<contextName>myApplicationName</contextName>

原理:ContextNameAction 解析成 ContextNameModel 交给 ContextNameModelHandler

用来设置上下文名称,每个logger都关联到logger上下文,默认上下文名称为default。但可以使用该标签设置成其他名字, 用于区分不同应用程序的记录。一旦设置,不能修改

可以通过%contextName来打印日志上下文名称

<?xml version="1.0" encoding="UTF-8"?>
<configuration>

    <!-- contextName:ContextNameAction 解析成 ContextNameModel 交给 ContextNameModelHandler
    用来设置上下文名称,每个logger都关联到logger上下文,默认上下文名称为default。但可以使用该标签设置成其他名字,
    用于区分不同应用程序的记录。一旦设置,不能修改。-->
    <contextName>myApplicationName</contextName>

    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>stdout %d %p %contextName - %m%n</pattern>
        </encoder>
    </appender>
    
    <root>
        <appender-ref ref="STDOUT"/>
    </root>

</configuration>

property 键值对定义

  • 有5个属性,name、value、file、resource和local
  • 通过<property>定义的键值对会被插入到logger上下文中
  • 通过使" ${xxx} "来使用变量
name: 变量的名称,
value: 变量定义的值,然后调用ActionUtil.setProperty保存kv
scope可选值:system、context、local,默认local
file:映射properties文件,然后调用ModelUtil.setProperties保存kv
resource:映射properties文件,然后调用ModelUtil.setProperties保存kv

在Logback中,变量有三种不同的scope:local scope ,context scope,system scope。 在变量替换的时候,首先从local scope中找,然后是context scope,然后是system properties中找,最后是操作系统环境变量中找。 可以在 元素中使用scope属性,它的属性值可以是:local,context,system。如果不指定,默认是local。

原文链接:https://blog.csdn.net/youxijishu/article/details/106172826

只有满足以下三种情况中的一种,property标签才有意义:

  1. name和value非空,file为空和resource为空:name和value才有效
  2. name和value为空,file非空和resource为空:file有效
  3. name和value为空,file为空和resource非空:resource有效

用法如下:

<!-- 定义kv -->
<property name="APP_Name" value="myAppName"/>

<!-- 从resources中读取property -->
<property resource="property/test.properties"/>

<!-- 从磁盘上读取property -->
<property file="E:\WorkspaceIdea\01src\log\matio-slf4j\slf4j_logback\src\main\resources\property\xxx.properties"/>

test.propertiesxxx.properties文件内容格式如下:

k1=v1
k2=v2
com.mx.a=yyy
name=matio
<?xml version="1.0" encoding="UTF-8"?>
<configuration>

    <!-- property同variable:PropertyAction 解析成 PropertyModel 交给 PropertyModelHandler
        用来定义变量值,它有两个属性name和value,通过<property>定义的值会被插入到logger上下文中,可以使用“${}”来使用变量。
        只有满足以下三种情况中的一种,property才有意义:
        1.name和value非空,file为空和resource为空:name和value才有效
        2.name和value为空,file非空和resource为空:file有效
        3.name和value为空,file为空和resource非空:resource有效
        
        name: 变量的名称,
        value: 变量定义的值,然后调用ActionUtil.setProperty
        scope可选值:system、context、local,默认local
        file:映射properties文件,然后调用ModelUtil.setProperties
        resource:映射properties文件,然后调用ModelUtil.setProperties
    -->

    <property name="APP_Name" value="myAppName"/>
    <property name="xx" value="yyy"/>

    <!-- 从resources中读取property -->
    <property resource="property/test.properties"/>

    <!-- 从磁盘上读取property -->
    <property file="E:\WorkspaceIdea\01src\log\matio-slf4j\slf4j_logback\src\main\resources\property\xxx.properties"/>

    <contextName>${APP_Name}</contextName>

    <property name="Console_Pattern"
              value="%d{yyyy-MM-dd HH:mm:ss.SSS} ${k1} ${name} %contextName %-5level [%logger{50}] - %msg%n"/>

    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>${Console_Pattern}</pattern>
        </encoder>
    </appender>

    <root>
        <appender-ref ref="STDOUT"/>
    </root>

</configuration>

include 引入文件

可以使用<include>标签在一个配置文件中包含另外一个配置文件

支持从多种源头包含:

<!-- 从文件中包含 -->
<include file="src/main/java/chapters/configuration/config.xml"/>

<!-- 从 classpath 中包含 -->
<include resource="config.xml"/>

<!-- 从 URL 中包含 -->
<include url="http://host:port/config.xml"/>

<include resource="org/springframework/boot/logging/logback/defaults.xml" />

如果包含不成功,那么 logback 会打印出一条警告信息,如果不希望 logback打印告警信息,只需这样做:

<include optional="true" ..../>

optional:如果include不成功,logback会输出日志,设置为true可以关闭,默认false

file、url和resource只能三选一

另外被包含的文件必须有以下格式:

<included>
    ...
</included>

原理:IncludeAction 解析成 IncludeModel

示例代码如下:

package com.matio.logback.include;

import com.matio.logback.consoleplugin.TestConsolePlugin;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class TestInclude {
    public static void main(String[] args) {
        // 读取resources下logback-include.xml作为logback配置文件
        System.setProperty("logback.configurationFile", "logback-include.xml");
        
        Logger logger = LoggerFactory.getLogger(TestConsolePlugin.class);
        logger.info("info");
        logger.debug("debug");
    }
}

logback-include.xml文件如下:

<?xml version="1.0" encoding="UTF-8"?>
<configuration debug="true">

    <!-- include:IncludeAction 解析成 IncludeModel
    optional:跟logback内部日志有关,可以忽略,默认false
    file、url和resource只能三选一
    -->
    <include optional="false" resource="logback-include-resource.xml"/>

    <!--<include file="E:\WorkspaceIdea\01src\log\matio-slf4j\slf4j_logback\src\main\resources\logback-include-resource.xml"/>-->

    <root level="${LEVEL}">
        <appender-ref ref="Console"/>
    </root>

    <appender name="Console" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <Pattern>%d %p - %m%n</Pattern>
            <charset>UTF-8</charset>
        </encoder>
    </appender>
</configuration>

被包含的文件logback-include-resource.xml必须有以下格式:

<included>
    <property name="LEVEL" value="debug"/>
    
    <appender class="ch.qos.logback.core.rolling.RollingFileAppender" name="ROLLING_FILE_APPENDER">
        <encoder>
            <charset>UTF-8</charset>
            <pattern>%d{dd.MMM.yyyy HH:mm:ss.SSS z}, [%6t], %6p, %C:%M %m%n</pattern>
        </encoder>
        <file>${LOGS_DIR}/${LOGFILE}.log</file>
        <triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
            <maxFileSize>5MB</maxFileSize>
        </triggeringPolicy>
        <rollingPolicy class="ch.qos.logback.core.rolling.FixedWindowRollingPolicy">
            <fileNamePattern>${LOGS_DIR}/${LOGFILE}%i.log.gz</fileNamePattern>
            <minIndex>1</minIndex>
            <maxIndex>21</maxIndex>
        </rollingPolicy>
    </appender>
    <appender class="ch.qos.logback.classic.AsyncAppender" name="ASYNC_APPENDER">
        <queueSize>2048</queueSize>
        <includeCallerData>true</includeCallerData>
        <discardingThreshold>0</discardingThreshold>
        <appender-ref ref="${FILE_APPENDER}"/>
        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
            <level>${LOG_LEVEL}</level>
        </filter>
    </appender>
</included>

也可以参考:https://blog.csdn.net/xiyang_1990/article/details/136835965

consolePlugin

<consolePlugin port="4321"/>

仅支持一个属性port,默认为4321,支持多个该标签

原理:由ConsolePluginAction解析该标签后,会生成一个SocketAppender(localhost:4321),然后注册到root logger中,

也就是说增加该标签后,默认logback中所有logger(只要继承了root)打印日志后,都会通过socket客户端发送给本地(固定为localhost)的4321端口

示例代码:

package com.matio.logback.consoleplugin;

import ch.qos.logback.classic.spi.LoggingEventVO;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.ObjectInputStream;
import java.net.ServerSocket;
import java.net.Socket;

public class TestConsolePlugin {
    public static void main(String[] args) throws InterruptedException {
        // 启用logback内部日志
        System.setProperty("logback.debug111", "true");
        // 读取resources下logback-consoleplugin.xml作为logback配置文件
        System.setProperty("logback.configurationFile", "logback-consoleplugin.xml");
        
        // 模拟启动一个socketserver准备接受日志
        new Thread(TestConsolePlugin::openSocketServer).start();
        // 等待socketserver启动完成
        Thread.sleep(3000);
        
        Logger logger = LoggerFactory.getLogger(TestConsolePlugin.class);
        logger.info("info");
        logger.debug("debug");
    }
    
    // 使用文心一言写的demo
    private static void openSocketServer() {
        int portNumber = 4321;
        try (
                ServerSocket serverSocket = new ServerSocket(portNumber);
                Socket clientSocket = serverSocket.accept();
                ObjectInputStream ois = new ObjectInputStream(clientSocket.getInputStream());
        ) {
            Object resp;
            while ((resp = ois.readObject()) != null) {
                LoggingEventVO event = (LoggingEventVO) resp;
                System.out.println("Received: " + event.getLevel().toString() + " : " + event.getMessage());
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

logback-consoleplugin.xml文件如下:

<?xml version="1.0" encoding="UTF-8"?>
<configuration debug="${logback.debug111}" scan="false">
    <!-- consolePlugin:ConsolePluginAction解析,port默认4321
    生成一个SocketAppender(localhost:4321)注册到root中
        -->
    <consolePlugin port="4321"/>
</configuration>

receiver

后续补全

contextListener 上下文监听器

侦听与LoggerContext生命周期相关的事件

    <!-- contextListener:LoggerContextListenerAction 解析成 LoggerContextListenerModel 交给 LoggerContextListenerModelHandler
    class:非空,反射生成LoggerContextListener子实现,然后回调其(LifeCycle)start()方法,最后注册到LoggerContext中
    -->
<contextListener class="com.matio.logback.contextlistener.CustomLoggerContextListener"/>

<!-- 将对logback记录器所做的级别更改传播到jul中的等效记录器中 -->
<contextListener class="ch.qos.logback.classic.jul.LevelChangePropagator">
    <resetJUL>true</resetJUL>
</contextListener>

仅支持一个属性class,非空,否则该标签无效。支持多个contextListener标签

原理:LoggerContextListenerAction 解析成 LoggerContextListenerModel 交给 LoggerContextListenerModelHandler,class会被反射生成LoggerContextListener子实现,回调其(LifeCycle)start()方法,最后注册到LoggerContext中,

也就是说通过该标签可以监听LoggerContext的start、reset、stop和levelChange事件

levelChange事件是指logback中logger的level发生更改,然后会通知该LoggerContextListener

示例代码:

自定义LoggerContextListener子实现如下:

package com.matio.logback.contextlistener;

import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.Logger;
import ch.qos.logback.classic.LoggerContext;
import ch.qos.logback.classic.spi.LoggerContextListener;
import ch.qos.logback.core.spi.ContextAwareBase;
import ch.qos.logback.core.spi.LifeCycle;

// 参考LoggerContextListenerModelHandler
// 参考LevelChangePropagator
public class CustomLoggerContextListener extends ContextAwareBase implements LoggerContextListener, LifeCycle {
    boolean isStarted = false;
    
    @Override
    public void start() {
        isStarted = true;
    }
    
    @Override
    public void stop() {
        isStarted = false;
    }
    
    @Override
    public boolean isStarted() {
        return isStarted;
    }
    
    @Override
    public boolean isResetResistant() {
        return false;
    }
    
    @Override
    public void onStart(LoggerContext context) {
        System.out.println("context start...");
    }
    
    @Override
    public void onReset(LoggerContext context) {
        System.out.println("context reset...");
    }
    
    @Override
    public void onStop(LoggerContext context) {
        System.out.println("context stop...");
    }
    
    @Override
    public void onLevelChange(Logger logger, Level newLevel) {
        System.out.println("修改logger[" + logger.getName() + "]的level为" + newLevel.toString());
    }
}

statusListener

监听logback内部日志status

<!-- statusListener:StatusListenerAction 解析成 StatusListenerModel 交给 StatusListenerModelHandler
    class:非空,反射生成StatusListener子实现,注册到Context.getStatusManager()中,顺便回调其(LifeCycle)start()方法
-->
<statusListener class="com.matio.logback.statuslistener.CusStatusListener"/>

原理:statusListener标签的解析交由StatusListenerModelHandler实现

参考我之前写的关于status的文章:# 07.java日志之logback内部状态数据Status

另外,我们也可以通过Java代码注册StatusListener,示例如下:

   LoggerContext lc = (LoggerContext) LoggerFactory.getILoggerFactory();
   StatusManager statusManager = lc.getStatusManager();
   OnConsoleStatusListener onConsoleListener = new OnConsoleStatusListener();
   statusManager.add(onConsoleListener);

请注意,已注册的状态侦听器StatusListener将仅接收其注册后的状态事件。它不会接收之前的消息。因此,通常最好将状态侦听器StatusListener注册指令放置在配置文件顶部的其他指令之前。

shutdownHook

安装 JVM 关闭钩子是关闭 logback 并释放相关资源的便捷方法

<!-- jvm停止后,延迟5s执行ShutdownHookBase#stop(),delay默认为0 -->
<shutdownHook class="ch.qos.logback.core.hook.DefaultShutdownHook">
    <!-- delay支持的参数形式可以参考Duration#valueOf() -->
    <delay>5 seconds</delay>
</shutdownHook>

<!-- 自定义ShutdownHookBase实现 -->
<shutdownHook class="com.matio.logback.shutdownhook.CusShutdownHook">
    <!-- 通过反射调用setName();注入到当前对象中,也就是CusShutdownHook,具体可以参考PropertySetter#setProperty(String, String); -->
    <name>matio</name>
</shutdownHook>

仅支持一个属性class,如果class没有值,则默认为ch.qos.logback.core.hook.DefaultShutdownHook ,支持多个该标签

原理:ShutdownHookAction 解析成 ShutdownHookModel 交给 ShutdownHookModelHandler,class会被反射生成ShutdownHookBase子实现,回调其(LifeCycle)start()方法,然后注册到jvm中等待jvm关闭时回调。

也就是说增加该标签后,程序停止时可以优雅停止logback,也可以实现用户自己的业务

自定义ShutdownHookBase实现示例代码:


package com.matio.logback.shutdownhook;
import ch.qos.logback.core.hook.ShutdownHookBase;

public class CusShutdownHook extends ShutdownHookBase {
    private String name;
    
    @Override
    public void run() {
        System.out.println(name);
        super.stop();
    }
    
    // 字段通过反射调用
    public void setName(String name) {
        this.name = name;
    }   
}

logger

<!--additivity:是否继承root节点,默认是true继承。默认情况下子Logger会继承父Logger的appender,
也就是说子Logger会在父Logger的appender里输出。
若是additivity设为false,则子Logger只会在自己的appender里输出,而不会在父Logger的appender里输出。-->
<logger name="org.springframework" level="INFO" additivity="false">
    <appender-ref ref="Console"/>
    <appender-ref ref="RollingFileInfo"/>
</logger>

name:该logger的名称。非空

level:接受不区分大小写的字符串值:TRACE、DEBUG、INFO、WARN、ERROR、ALL 或 OFF 之一。特殊的不区分大小写的值INHERITED或其同义词NULL,将强制从层次结构中的更高层继承记录器的级别

additivity:是否继承父parent(根据name判断,默认是root),默认是true。默认情况下子Logger会继承父Logger的appender,也就是说子Logger会在父Logger的appender里输出。 若是additivity设为false,则子Logger只会在自己的appender里输出,而不会在父Logger的appender里输出

<logger>元素可以包含零个或多个<appender-ref>元素;这样引用的每个附加程序都会添加到指定的记录器中。与log4j不同,logback-classic在配置给定logger时 不会关闭或删除任何先前引用的附加程序

root

<!-- 从低到高为:All < Trace < Debug < Info < Warn < Error < Fatal < OFF-->
<root level="ALL">
    <appender-ref ref="Console"/>
    <appender-ref ref="RollingFileWarn"/>
    <appender-ref ref="RollingFileError"/>
</root>

只支持一个属性level。由于根记录器已被命名为“ROOT”,因此它也不允许使用名称属性。

level 属性的值可以是不区分大小写的字符串:TRACE、DEBUG、INFO、WARN、ERROR、ALL 或 OFF 之一。root的level值不能设置为 INHERITED 或 NULL

与元素<logger>类似, <root>元素可以包含零个或多个 <appender-ref>元素;这样引用的每个附加程序都会添加到根记录器中。与 log4j 不同,logback-classic在配置root时 不会关闭或删除任何先前引用的附加程序

appender

<property name="APP_NAME" value="web-things"/>
<property name="LOG_HOME" value="home/shuncom/log/web-things"/>

<contextName>${APP_NAME}</contextName>

<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
    <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
        <pattern>%-5p %d{yyyy-MM-dd HH:mm:ss:SSS} - [%thread] %c{0} - %m%n</pattern>
    </encoder>
</appender>

<appender name="CONSOLE_DEBUG" class="ch.qos.logback.core.ConsoleAppender">
    <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
        <pattern>%-5p %d{yyyy-MM-dd HH:mm:ss:SSS} - [%thread] %c - %m%n%caller%n</pattern>
    </encoder>
</appender>

<!-- File Rolling Appender -->
<appender name="FILE_ROLLING" class="ch.qos.logback.core.rolling.RollingFileAppender">
    <file>${LOG_HOME}/${APP_NAME}.log</file>
    <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
        <pattern>%-5p %d{yyyy-MM-dd HH:mm:ss:SSS} - [%thread] %c{0} - %m%n</pattern>
    </encoder>
    <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
        <!-- 压缩模式.zip | .gz , WW 按周进行rollover-->
        <FileNamePattern>
            ${LOG_HOME}/${APP_NAME}_%d{yyyy-MM-dd}.log.gz
        </FileNamePattern>
    </rollingPolicy>
</appender>

<appender name="error" class="ch.qos.logback.core.rolling.RollingFileAppender">
    <File>${LOG_HOME}/${APP_NAME}_error.log</File>
    <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
        <fileNamePattern>${LOG_HOME}/${APP_NAME}_error_%d{yyyy-MM-dd}.log.gz</fileNamePattern>
    </rollingPolicy>
    <encoder>
        <pattern>%-5p %d{yyyy-MM-dd HH:mm:ss:SSS} - [%thread] %c{0} - %m%n</pattern>
    </encoder>
    <filter class="ch.qos.logback.classic.filter.LevelFilter">
        <level>ERROR</level>
        <onMatch>ACCEPT</onMatch>
        <onMismatch>DENY</onMismatch>
    </filter>
    <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
        <!-- 压缩模式.zip | .gz , WW 按周进行rollover-->
        <FileNamePattern>
            ${LOG_HOME}/${APP_NAME}_error_%d{yyyy-MM-dd}.log.gz
        </FileNamePattern>
    </rollingPolicy>
</appender>

两个强制属性name和class: name:指定appender的名称, class:指定要实例化的appender类的完全限定名称

import

后续补全

define 动态声明变量

<!-- key=hostname,value=当前主机名 -->
<define name="hostname" class="ch.qos.logback.core.property.CanonicalHostNamePropertyDefiner" scope="system"/>

<!-- key=fileExists,value=true/false(文件是否存在) -->
<define name="fileExists" class="ch.qos.logback.core.property.FileExistsPropertyDefiner" scope="system">
    <path>E:\WorkspaceIdea\01src\log\matio-slf4j\slf4j_logback\src\main\resources\logback-define.xml</path>
</define>

<!-- key=resourceExists,value=true/false(资源是否存在) -->
<define name="resourceExists" class="ch.qos.logback.core.property.ResourceExistsPropertyDefiner" scope="system">
    <resource>logback-define.xml</resource>
</define>

<!-- key=k1,value=v1(见ValuePropertyDefiner)这种方式同property标签 -->
<define name="k1" class="com.matio.logback.define.ValuePropertyDefiner" scope="system">
    <value>v1</value>
</define>

您可以使用<define>元素动态定义属性。define元素具有两个强制属性:nameclassname属性指定要设置的属性的名称,而class属性指定实现PropertyDefiner接口的任何类。PropertyDefiner实例的getPropertyValue()方法返回的值将是命名属性的值。也可以通过指定scope属性来指定命名属性的作用域。

原理:DefinePropertyAction 解析成 DefineModel 交给 DefineModelHandler,然后通过反射将class生成PropertyDefiner子实现,回调其(LifeCycle)start()方法进行初始化,后调用其getPropertyValue()方法返回真正的value值,调用ActionUtil.setProperty()保存kv

logback 附带了一些相当简单的PropertyDefiner

CanonicalHostNamePropertyDefiner 将命名变量设置为本地主机的规范主机名,可能需要几秒钟的时间 FileExistsPropertyDefiner 如果路径属性指定的文件存在,则将变量值置为“true” ,否则为“false” ResourceExistsPropertyDefiner 如果用户指定的资源在类路径上可用, 则将变量值为“true” ,否则为“false”
package com.matio.logback.define;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class DefineTest {
    public static void main(String[] args) {
        // 读取resources下logback-define.xml作为logback配置文件
        System.setProperty("logback.configurationFile", "logback-define.xml");
        
        Logger logger = LoggerFactory.getLogger("logger123");
        logger.info("info");
        logger.debug("debug");
        
        // 如果scope=system,则logback会把kv存储到系统属性中,见ActionUtil.setProperty
        System.out.println("hostname=" + System.getProperty("hostname"));
        System.out.println("fileExists=" + System.getProperty("fileExists"));
        System.out.println("resourceExists=" + System.getProperty("resourceExists"));
        System.out.println("k1=" + System.getProperty("k1"));
        System.out.println("k2=" + System.getProperty("k2"));
        System.out.println("k3=" + System.getProperty("k3"));
        System.out.println("k3=" + System.getProperty("k4"));
    }
}

打印结果如下:

hostname=WIN-VKULP3QRAGE
fileExists=true
resourceExists=true
k1=v1
k2=null
k3=null
k3=v4

logback-define.xml文件如下:

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <!-- define:DefinePropertyAction 解析成 DefineModel 交给 DefineModelHandler
        name:非空
        class:非空,反射生成PropertyDefiner子实现,回调其(LifeCycle)start()方法,后调用其getPropertyValue()方法,ActionUtil.setProperty()
        scope可选值:system、context、local,默认local
    -->
    <!-- key=hostname,value=当前主机名 -->
    <define name="hostname" class="ch.qos.logback.core.property.CanonicalHostNamePropertyDefiner" scope="system"/>

    <!-- key=fileExists,value=true/false(文件是否存在) -->
    <define name="fileExists" class="ch.qos.logback.core.property.FileExistsPropertyDefiner" scope="system">
        <path>E:\WorkspaceIdea\01src\log\matio-slf4j\slf4j_logback\src\main\resources\logback-define.xml</path>
    </define>

    <!-- key=resourceExists,value=true/false(资源是否存在) -->
    <define name="resourceExists" class="ch.qos.logback.core.property.ResourceExistsPropertyDefiner" scope="system">
        <resource>logback-define.xml</resource>
    </define>

    <!-- key=k1,value=v1(见ValuePropertyDefiner),这种方式同property标签 -->
    <define name="k1" class="com.matio.logback.define.ValuePropertyDefiner" scope="system">
        <value>v1</value>
    </define>

    <property name="k2" value="v2" scope="local"/>
    <property name="k3" value="v3" scope="context"/>
    <property name="k4" value="v4" scope="system"/>
</configuration>

自定义ValuePropertyDefiner如下:

package com.matio.logback.define;
import ch.qos.logback.core.PropertyDefinerBase;
public class ValuePropertyDefiner extends PropertyDefinerBase {
    private String value;
    
    @Override
    public String getPropertyValue() {
        return value;
    }
    // logback通过反射注入value
    public void setValue(String value) {
        this.value = value;
    }
}

timestamp 时间戳

<!-- timestamp:
获取时间戳字符串,他有两个属性key和datePattern
-->
<timestamp key="kkk" datePattern="yyyy-MM-dd" timeReference="contextBirth" scope="local"/>
  • key: 非空,标识此<timestamp> 的名字;
  • datePattern: 非空,设置将当前时间(解析配置文件的时间)转换为字符串的模式,遵循java.txt.SimpleDateFormat的格式
  • timeReference:如果值为contextBirth,value=context.getBirthTime(),否则value=System.currentTimeMillis()
  • scope可选值:system、context、local,默认local,见ActionUtil.setProperty

原理:TimestampAction 解析成 TimestampModel 交给 TimestampModelHandler,解析后作为属性注册到logback中

sequenceNumberGenerator 日志序列号生成器

<sequenceNumberGenerator class="com.matio.logback.sequencenumbergenerator.CusSequenceNumberGenerator"/>

仅支持一个属性class,如果class没有值,则该标签无效,支持多个该标签

原理:SequenceNumberGeneratorAction 解析成 SequenceNumberGeneratorModel 交给 SequenceNumberGeneratorModelHandler,class会被反射生成SequenceNumberGenerator子实现,然后赋值给LoggerContext对象

也就是说增加该标签后,logback会给生成的每一条log生成一个唯一的标识,即sequenceNumber,我们也可以在打印日志的时候通过%sequenceNumber(具体参考PatternLayout类)显示该表示,比如:

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <sequenceNumberGenerator class="com.matio.logback.sequencenumbergenerator.CusSequenceNumberGenerator"/>
    
    <!-- 可以通过%sn或者%sequenceNumber去读取每条日志的sequenceNumber,具体参考PatternLayout -->
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%-5level %d{yyyy-MM-dd HH:mm:ss.SSS} %sequenceNumber [%logger{50}] %file %-4line %method - %msg%n</pattern>
        </encoder>
    </appender>
    
    <root>
        <appender-ref ref="STDOUT"/>
    </root>
</configuration>

CusSequenceNumberGenerator示例代码如下:

package com.matio.logback.sequencenumbergenerator;

import ch.qos.logback.core.spi.ContextAwareBase;
import ch.qos.logback.core.spi.SequenceNumberGenerator;
import java.util.concurrent.atomic.AtomicLong;

// BasicSequenceNumberGenerator
public class CusSequenceNumberGenerator extends ContextAwareBase implements SequenceNumberGenerator {
    
    private static final AtomicLong x = new AtomicLong();
    public CusSequenceNumberGenerator() {
        System.out.println("init CusSequenceNumberGenerator...");
    }
    
    @Override
    public long nextSequenceNumber() {
        // 也可以在分布式环境下基于redis为每个日志生成唯一key
        return x.incrementAndGet();
    }
}

evaluator

conversionRule

newRule

insertFromJNDI

<insertFromJNDI env-entry-name="java:comp/env/appName" as="appName1" scope="local"/>

支持多个该标签

  • env-entry-name:非空,内容必须要以java:开头
  • as:非空,key值
  • scope可选值:system、context、local,默认local

原理:InsertFromJNDIAction 解析成 InsertFromJNDIModel 交给 InsertFromJNDIModelHandler,然后通过env-entry-name去提取存储在JNDI中的env条目,并使用as属性作为key保存到不同的作用域中,最后通过${xxx}去引用。

在某些情况下,您可能希望使用JNDI中存储的env条目。<insertFromJNDI>配置指令提取存储在JNDI中的env条目,并使用as属性指定的键在本地作用域中插入属性。作为所有属性,可以在scope属性的帮助下将新属性插入到不同的作用域中。

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <!-- insertFromJNDI:InsertFromJNDIAction 解析成 InsertFromJNDIModel 交给 InsertFromJNDIModelHandler
        env-entry-name:非空,内容必须要以java:开头
        as:非空
        scope可选值:system、context、local,默认local
        最终还是调用ModelUtil.setProperty
    -->
    <insertFromJNDI env-entry-name="java:comp/env/appName" as="appName" scope="local"/>
    
    <contextName>${appName}</contextName>

    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d ${CONTEXT_NAME} %level -%kvp- %msg %logger{50}%n</pattern>
        </encoder>
    </appender>

    <root level="DEBUG">
        <appender-ref ref="CONSOLE" />
    </root>
</configuration>

if、then、else 条件判断

在项目中,存在使用条件判断的场景,例如想为测试和生产设置不同的日志记录级别。幸好的是,logback本身已经支持这种场景。

<dependency>
    <groupId>org.codehaus.janino</groupId>
    <artifactId>janino</artifactId>
    <version>3.0.6</version>
</dependency>

如果使用if,需要引入janino依赖,否则logback会提示以下信息:

15:21:42,469 |-ERROR in ch.qos.logback.core.model.processor.conditional.IfModelHandler - Could not find Janino library on the class path. Skipping conditional processing.
15:21:42,469 |-ERROR in ch.qos.logback.core.model.processor.conditional.IfModelHandler - See also http://logback.qos.ch/codes.html#ifJanino
15:21:42,469 |-ERROR in ch.qos.logback.core.model.processor.conditional.IfModelHandler - Unexpected unexpected empty model stack.

if语法有两种:

   <!-- if-then form -->
   <if condition="some conditional expression">
    <then>
      ...
    </then>
  </if>

  <!-- if-then-else form -->
  <if condition="some conditional expression">
    <then>
      ...
    </then>
    <else>
      ...
    </else>
  </if>

接下来着重看一下condition

condition是一个Java表达式,其中只能访问上下文属性或系统属性。

对于作为参数传递的键,property("x")返回该属性的String值,跟p("x")等价。如果键为“x”的属性未定义,则属性方法将返回空字符串,而不是null。这就避免空指针。

isDefined() :检查是否定义了属性,示例:isDefined("x")

isNull():检查属性是否为null,示例:isNull("x")

示例代码:

package com.matio.logback.ifelsethen;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class IfElseThenTest {
    public static void main(String[] args) {
        // 模拟测试环境
        System.setProperty("app.mode", "test");
        
        // 读取resources下logback-ifelse-then.xml作为logback配置文件
        System.setProperty("logback.configurationFile", "logback-ifelse-then.xml");
        
        Logger logger = LoggerFactory.getLogger("logger123");
        logger.info("info");
        logger.debug("debug");
    }
}

logback-ifelse-then.xml文件如下:

<?xml version="1.0" encoding="UTF-8"?>
<configuration debug="true">

    <property name="APP_NAME" value="test_logback"/>
    <property name="LOG_HOME" value="D:/test_logback/"/>

    <contextName>${APP_NAME}</contextName>
    
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
            <pattern>%-5p %d{yyyy-MM-dd HH:mm:ss:SSS} - [%thread] %c - %m%n</pattern>
        </encoder>
    </appender>

    <!-- File Rolling Appender -->
    <appender name="FILE_ROLLING" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>${LOG_HOME}/${APP_NAME}.log</file>
        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
            <pattern>%-5p %d{yyyy-MM-dd HH:mm:ss:SSS} - [%thread] %c{0} - %m%n</pattern>
        </encoder>
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <!-- 压缩模式.zip | .gz , WW 按周进行rollover-->
            <FileNamePattern>
                ${LOG_HOME}/${APP_NAME}_%d{yyyy-MM-dd}.log.gz
            </FileNamePattern>
        </rollingPolicy>
    </appender>
    
    <root level="debug">
        <!-- property("xx") 也可以写成 p("xx") -->
        <if condition='property("app.mode").equals("test")'>
            <then>
                <appender-ref ref="CONSOLE"/>
            </then>
            <else>
                <appender-ref ref="FILE_ROLLING"/>
            </else>
        </if>
    </root>

</configuration>