掘金 后端 ( ) • 2024-04-01 13:50

theme: smartblue

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

appender

负责将日志输出到控制台、文件等,对应logback中的接口如下:

package ch.qos.logback.core;

import ch.qos.logback.core.spi.ContextAware;
import ch.qos.logback.core.spi.FilterAttachable;
import ch.qos.logback.core.spi.LifeCycle;

public interface Appender<E> extends LifeCycle, ContextAware, FilterAttachable<E> {

    String getName();
    // 负责输出日志到目的地,比如console或者file
    void doAppend(E event) throws LogbackException;

    void setName(String name);
}

每一个appender都需要被设置一个name, doAppend(E event)负责输出日志到目的地,比如console或者file。appender的视线类有很多,主要看以下几个:

  • ConsoleAppender:把日志输出到console,支持System.out或者System.err
  • FileAppender:把日志添加到文件
  • RollingFileAppender:滚动记录文件,先将日志记录到指定文件,当满足条件时,将日志备份到其他文件。它是FileAppender的子类
  • SocketAppender:将日志通过socket发给远端
  • ServerSocketAppender:将日志通过socket发给远端

强制使用两个 属性值 :name 和 class

  • name :appender 的名称,该值主要用于 <appender-ref> 的 ref
  • class:定义appender 的权限定名或叫组件!

appender标签解析原理:AppenderAction 解析成 AppenderModel 交给 AppenderModelHandler,然后通过class反射生成Appender子实现,回调其(LifeCycle)start()方法

ConsoleAppender

<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
    <!-- System.err或者System.out(默认) -->
    <target>System.out</target>
    <withJansi>false</withJansi>
    <encoder>
        <pattern>stdout %d %p - %m%n</pattern>
    </encoder>
</appender>
属性名 说明 encoder 输出格式 target System.out 或者 System.err ,默认 System.out withJansi 默认为false。设置Jansi到true会激活Jansi库,它为Windows机器上的ANSI颜色代码提供支持。使用很少可以忽略

FileAppender

将log全部写一个文件中。目标文件由“file”属性指定。如果文件已经存在,则会根据append属性的值将其追加或截断。

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

    <!-- appender:AppenderAction 解析成 AppenderModel 交给 AppenderModelHandler
        name:非空
        class:非空,反射生成Appender子实现,回调其(LifeCycle)start()方法
    -->
    <appender name="FILE" class="ch.qos.logback.core.FileAppender">
        <file>D:\test_logback\test_logback.log</file>
        <prudent>false</prudent>
        <append>true</append>
        <!-- 底层缓冲区的大小,默认8 k,也可以写成8 kb、8 mb,具体参考ch.qos.logback.core.util.FileSize-->
        <bufferSize>8192</bufferSize>
        <!-- 如果为true,每打印一条log就会刷新缓冲区(也就是将日志写到文件);
         如果为false,日志会暂存在缓冲区中,待缓冲区满的时候在写到文件中,所以此种情况下日志并不会立刻写到文件中,而且如果应用程序没有正确关闭的情况下退出,缓冲区中的日志可能会丢失
         但是设置为false可以显著的增加日志吞吐量,提升性能
         默认为true -->
        <immediateFlush>false</immediateFlush>
        <encoder>
            <pattern>stdout %d %p - %m%n</pattern>
        </encoder>
    </appender>
    
    <root>
        <appender-ref ref="FILE"/>
    </root>
    
</configuration>
属性名 说明 file 要写入的文件的名称。如果该文件不存在,则创建该文件,可以是相对目录,也可以是绝对目录,如果上级目录不存在会自动创建。在MS Windows平台上,file可以为d:\logback/test.log,或者d:\logback\test.log。 append 如果设为true,日志将会在文件结尾处增加。如果为false就清空现有文件。默认为true encoder 输出格式 bufferSize 底层缓冲区的大小,默认8192,也可以写成8 kb、8 mb,具体参考ch.qos.logback.core.util.FileSize immediateFlush 如果为true,每打印一条log就会刷新缓冲区(也就是将日志写到文件);如果为false,日志会暂存在缓冲区中,待缓冲区满的时候在写到文件中,所以此种情况下日志并不会立刻写到文件中,而且如果应用程序没有正确关闭的情况下退出,缓冲区中的日志可能会丢失。但是设置为false可以显著的增加日志吞吐量,提升性能,默认为true prudent 将日志安全谨慎的写入到指定文件,即使有其他FileAppender也将日志写入此文件中。效率低下,该模式默认值为false。注意:当该值设为true时,append将默认为true

示例代码:

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

    <!-- appender:AppenderAction 解析成 AppenderModel 交给 AppenderModelHandler
        name:非空
        class:非空,反射生成Appender子实现,回调其(LifeCycle)start()方法
    -->
    <appender name="FILE1" class="ch.qos.logback.core.FileAppender">
        <file>D:\test_logback\test_logback.log</file>
        <prudent>false</prudent>
        <append>true</append>
        <!-- 底层缓冲区的大小,默认8 k,也可以写成8 kb、8 mb,具体参考ch.qos.logback.core.util.FileSize-->
        <bufferSize>8192</bufferSize>
        <!-- 如果为true,每打印一条log就会刷新缓冲区(也就是将日志写到文件);
         如果为false,日志会暂存在缓冲区中,待缓冲区满的时候在写到文件中,所以此种情况下日志并不会立刻写到文件中,而且如果应用程序没有正确关闭的情况下退出,缓冲区中的日志可能会丢失
         但是设置为false可以显著的增加日志吞吐量,提升性能
         默认为true -->
        <immediateFlush>false</immediateFlush>
        <encoder>
            <pattern>stdout %d %p - %m%n</pattern>
        </encoder>
    </appender>

    <timestamp key="bySecond1" datePattern="yyyyMMdd'T'HHmmssSSS"/>

    <timestamp key="bySecond2" datePattern="yyyyMMdd'T'HHmmssSSS" timeReference="contextBirth"/>

    <appender name="FILE2" class="ch.qos.logback.core.FileAppender">
        <file>D:\test_logback\test_logback-${bySecond1}-${bySecond2}.log</file>
        <prudent>false</prudent>
        <append>true</append>
        <!-- 底层缓冲区的大小,默认8 k,也可以写成8 kb、8 mb,具体参考ch.qos.logback.core.util.FileSize-->
        <bufferSize>8192</bufferSize>
        <!-- 如果为true,每打印一条log就会刷新缓冲区(也就是将日志写到文件);
         如果为false,日志会暂存在缓冲区中,待缓冲区满的时候在写到文件中,所以此种情况下日志并不会立刻写到文件中,而且如果应用程序没有正确关闭的情况下退出,缓冲区中的日志可能会丢失
         但是设置为false可以显著的增加日志吞吐量,提升性能
         默认为true -->
        <immediateFlush>false</immediateFlush>
        <encoder>
            <pattern>stdout %d %p - %m%n</pattern>
        </encoder>
    </appender>

    <root>
        <appender-ref ref="FILE1"/>
        <appender-ref ref="FILE2"/>
    </root>

</configuration>

生成的文件如下: image.png

RollingFileAppender

RollingFileAppender扩展了FileAppender,具有滚动日志文件的能力。例如,RollingFileAppender可以记录到一个名为log.txt的文件中,一旦满足特定条件,就可以将其日志记录目标更改为另一个文件。

有两个与RollingFileAppender交互的重要子组件。

第一个RollingFileAppender子组件,即RollingPolicy(见下文),负责执行滚动所需的操作。

RollingFileAppender的第二个子组件,即TriggeringPolicy(见下文),将确定是否以及何时发生滚动。因此,RollingPolicy负责什么,而TriggeringPolicy负责什么时候发生。

要使用 RollingFileAppender,必须同时设置 RollingPolicy 和 TriggeringPolicy。但是,如果其 RollingPolicy 也实现了 TriggeringPolicy 接口,则只需明确指定前者。

属性名 类型 类型 file String 被写入的文件名,可以是相对目录,也可以是绝对目录,如果上级目录不存在会自动创建,没有默认值。 append boolean 如果设为true,日志将会在文件结尾处增加。如果为false就清空现有文件。默认为true encoder String 输出格式 prudent boolean 将日志安全谨慎的写入到指定文件,即使有其他FileAppender也将日志写入此文件中。效率低下,该模式默认值为false。注意:当该值设为true时,append将默认为true rollingPolicy RollingPolicy 如何rolling,涉及文件移动和重命名 triggeringPolicy TriggeringPolicy 何时触发rolling

每天生成一个日志文件,且被压缩成.gz(如果不想被压缩,去掉结尾的.gz即可)

<property name="APP_NAME" value="app-name"/>
<property name="LOG_HOME" value="/home/log/app-name"/>

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

压缩格式目前仅支持.gz.zip,只需要修改后缀即可。当然如果不想被压缩,去掉结尾的后缀即可

此外还可以加上以下两个参数:生成的所有日志文件的大小,应用重启时清除历史记录

<!-- 保存多少天(单位取决于FileNamePattern格式)的日志,默认无上限-->
<maxHistory>0</maxHistory>
<!-- 控制日志文档存储的总大小,如 3GB、100M,默认无上限-->
<totalSizeCap>30GB</totalSizeCap>
<!-- 如果为true,将清理历史记录,在appender启动时执行存档删除操作。默认false -->
<cleanHistoryOnStart>false</cleanHistoryOnStart>

提供一个RollingFileAppender生产环境下的的使用案例

<?xml version="1.0" encoding="UTF-8"?>
<configuration debug="true">
    
    <property name="APP_NAME" value="appname"/>
    <property name="LOG_HOME" value="D:\test_logback"/>

    <property name="FILE_LOG_PATTERN" value="%-5p %d{yyyy-MM-dd HH:mm:ss:SSS} - [%thread] %c{0} - %m%n"/>

    <!-- 每天生成一个日志文件,压缩成.gz,单个文件大小限制为128MB,保留30天的日志 -->
    <appender name="FILE_ROLLING1" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <encoder>
            <pattern>${FILE_LOG_PATTERN}</pattern>
        </encoder>
        <file>${LOG_HOME}/${APP_NAME}.log</file>
        <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
            <!-- 如果class为SizeAndTimeBasedRollingPolicy,则该参数非空 -->
            <maxFileSize>${LOG_FILE_MAX_SIZE:-10KB}</maxFileSize>
            <!-- 后缀暂支持.gz和.zip格式,没有则不压缩  -->
            <fileNamePattern>${LOG_HOME}/${APP_NAME}.%d{yyyy-MM-dd}.%i.gz</fileNamePattern>
            <!-- 保存多少天(单位取决于fileNamePattern中%d{yyyy-MM-dd}格式)的日志 -->
            <maxHistory>${LOG_FILE_MAX_HISTORY:-30}</maxHistory>
            <!-- 如果为true,将清理历史记录,在appender启动时执行存档删除操作。此参数一般不配置,默认false -->
            <!--<cleanHistoryOnStart>false</cleanHistoryOnStart>-->
            <!-- 控制日志文档存储的总大小,如 3GB、100M,默认无上限-->
            <!--<totalSizeCap>30GB</totalSizeCap>-->
        </rollingPolicy>
    </appender>

    <!-- 每小时生成一个日志文件 -->
    <appender name="FILE_ROLLING2" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
            <pattern>${FILE_LOG_PATTERN}</pattern>
        </encoder>
        <file>${LOG_HOME}/${APP_NAME}.log</file>
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <FileNamePattern>
                ${LOG_HOME}/${APP_NAME}_%d{yyyy-MM-dd_HH}.log
            </FileNamePattern>
            <!-- 保存多少小时(单位取决于fileNamePattern中%d{yyyy-MM-dd_HH}格式)的日志 -->
            <maxHistory>24</maxHistory>
        </rollingPolicy>
    </appender>
    
    <root>
        <appender-ref ref="FILE_ROLLING1"/>
        <!--<appender-ref ref="FILE_ROLLING2"/>-->
    </root>

</configuration>

AsyncAppender

一个异步记录日志的appender

实现原理:它只是将日志存放到它维护的一个定长队列中,然后由另一个异步线程不断地从这个队列中取出日志,再交给另一个appender去真正地输出日志

以下三点内容翻译自:# logback用户手册

1.如果80%FULL AsyncAppender缓冲BlockingQueue中的事件。AsyncAppender创建的工作线程从队列的头部获取事件,并将它们分派到附加到AsyncAppander的单个附加程序。请注意,默认情况下,如果队列已满80%,AsyncAppender将丢弃级别为TRACE、DEBUG和INFO的事件。这种策略以事件损失为代价,对性能产生了惊人的有利影响。

2.应用程序关闭或重新部署后,必须停止AsyncAppender,以便停止和回收工作线程,并从队列中清除日志事件。这可以通过停止LoggerContext来实现,LoggerContext将关闭所有附加程序,包括任何AsyncAppender实例。AsyncAppender将等待工作线程刷新到maxFlushTime中指定的超时。如果在关闭LoggerContext期间发现排队的事件被丢弃,则可能需要增加超时时间。为maxFlushTime指定值0将强制AsyncAppender在从stop方法返回之前等待所有排队的事件被刷新。

3.使用AsyncAppender时,最好搭配shundownHook标签一起使用: 根据JVM关闭的模式,处理排队事件的worker线程可能会中断,导致log滞留在blockingqueue中。这种情况通常发生在LoggerContext未完全停止或JVM在典型控制流之外终止时。为了避免在这些情况下中断工作线程,可以向JVM运行时插入一个shundownHook钩,以便在JVM关闭启动后正确停止LoggerContext。当其他关闭挂钩试图记录事件时,关闭挂钩也可能是干净关闭Logback的首选方法。

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

    <!-- appender:AppenderAction 解析成 AppenderModel 交给 AppenderModelHandler
        name:非空
        class:非空,反射生成Appender子实现,回调其(LifeCycle)start()方法
    -->
    <appender name="Async-Appender" class="ch.qos.logback.classic.AsyncAppender">
        <!-- 是否包含调用者数据,默认false -->
        <includeCallerData>true</includeCallerData>
        <!-- 更改默认的队列的深度,该值会影响性能,至少为1,基于该值创建一个ArrayBlockingQueue,默认值为256 -->
        <queueSize>2048</queueSize>
        <!-- 不丢失日志.默认的,如果队列的80%已满,则会丢弃TRACT、DEBUG、INFO级别的日志。默认-1
         如果队列的剩余长度小于该值,那么就将丢弃新到来的TRACT、DEBUG、INFO级别的日志
         如果为-1,则discardingThreshold = queueSize * 20%
         设置为0(零) 可以防止事件丢失
         如果为0或负数,则根据neverBlock将日志存放到队列中
         -->
        <discardingThreshold>0</discardingThreshold>
        <!-- 在AsyncAppender停止运行的时候,回去中断worker线程,最多等待worker运行多少毫秒,
        如果worker过了该时间后还没有消费完队列中的日志,那么队列中剩余的日志可能会丢失
         一句话概括:AsyncAppender停止运行的时候,给多少时间让worker去输出队列中的日志,如果时间到了,队列中剩余的日志可能会丢失
         默认1000,
         -->
        <maxFlushTime>1000</maxFlushTime>
        <!--
        如果为true,会尝试将log防到队列中,失败了也无所谓
        如果为false,会尝试一直将log防到队列中,默认为false
        -->
        <neverBlock>false</neverBlock>
        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
            <level>debug</level>
        </filter>
        <!-- 添加附加的appender,至少包含一个appender-ref,否则该AsyncAppender无意义 -->
        <appender-ref ref="CONSOLE"/>
    </appender>

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

    <root>
        <appender-ref ref="Async-Appender"/>
    </root>

    <shutdownHook class="ch.qos.logback.core.hook.DefaultShutdownHook">
        <!-- delay支持的参数形式可以参考Duration#valueOf() -->
        <delay>5 seconds</delay>
    </shutdownHook>

</configuration>
  1. includeCallerData:通过log是否可以获取到调用者数据,默认false
  2. queueSize:该值至少为1,基于该值创建一个ArrayBlockingQueue,默认256
  3. discardingThreshold:不丢失日志.默认的,如果队列的80%已满,则会丢弃TRACT、DEBUG、INFO级别的日志。默认-1。 如果队列的剩余长度小于该值,那么就将丢弃新到来的TRACT、DEBUG、INFO级别的日志 如果为-1,则discardingThreshold = queueSize * 20% 设置为0(零) 可以防止事件丢失 如果为0或负数,则根据neverBlock将日志存放到队列中,防止日志丢失
  4. maxFlushTime:在AsyncAppender停止运行的时候,会去中断worker守护线程,最多等待worker运行多少毫秒,如果worker过了该时间后还没有消费完队列中的日志,那么队列中剩余的日志可能会丢失。(一句话概括:AsyncAppender停止运行的时候,给多少时间让worker去输出队列中的日志,如果时间到了,队列中剩余的日志可能会丢失。)如果指定为0,则强制等待worker消费完所有的日志,保证了日志不会有丢失(单位:ms)
  5. neverBlock: 如果为true,会尝试将log存放队列中,失败了就说明该日志丢弃了;如果为false,会尝试一直将log放到队列中,直到成功或者appender停止运行。默认false
  6. appender-ref:添加附加的appender,至少有一个该标签,否则该AsyncAppender无意义,就是通过该标签找到真正的appender去实现真正的日志输出

如果对这些参数不清楚的,可以看看下面这个流程图: image.png

SiftingAppender

需要再这里特别说明一下,logback-access模块中也有SiftingAppender,但是我们今天这里只看logback-classic中的SiftingAppender

<dependency>
    <groupId>ch.qos.logback</groupId>
    <artifactId>logback-access</artifactId>
    <version>1.3.6</version>
</dependency>

SiftingAppender可以用于根据给定的运行时属性来分离(或筛选)日志记录。例如,SiftingAppender可以根据用户会话来分隔日志事件,这样不同用户生成的日志就可以进入不同的日志文件中,每个用户一个日志文件。

SiftingAppender底层原理如下:

public abstract class SiftingAppenderBase<E> extends AppenderBase<E> {
    @Override
    protected void append(E event) {
        if (!isStarted()) {
            return;
        }
        // 获取区分的那个关键值
        String discriminatingValue = discriminator.getDiscriminatingValue(event);
        long timestamp = getTimestamp(event);
        
        // 通过这个值生成对应的Appender
        Appender<E> appender = appenderTracker.getOrCreate(discriminatingValue, timestamp);
        // marks the appender for removal as specified by the user
        
        // logger.info(ClassicConstants.FINALIZE_SESSION_MARKER, "ceshi");
        if (eventMarksEndOfLife(event)) {
            appenderTracker.endOfLife(discriminatingValue);
        }
        // 移除过时的appender
        appenderTracker.removeStaleComponents(timestamp);
        
        // 输出日志
        appender.doAppend(event);
    }
}

重点看这样一个调用:

appenderTracker.removeStaleComponents(timestamp);

这行代码主要负责移除过时的appender,那么什么样的appender是过时的呢?继续往下看。

public synchronized void removeStaleComponents(long now) {
    // 至少过1s后才执行remove逻辑
    if (isTooSoonForRemovalIteration(now))
        return;
    removeExcedentComponents();
    removeStaleComponentsFromMainMap(now);
    removeStaleComponentsFromLingerersMap(now);
}
  1. removeExcedentComponents();

如果发现维护的appender数量超过Integer.MAX_VALUE,则移除并停止所有的appender

  1. removeStaleComponentsFromMainMap(now);

如果一个appender过了30分钟还没有输出日志,就将它移除

  1. removeStaleComponentsFromLingerersMap(now);

在许多情况下,在代码中精确定位一个位置可能会更容易,在此位置之后就不再需要嵌套的appender了。用户可以使用FINALIZE_SESSION标记从该位置进行日志记录(logger.info(ClassicConstants.FINALIZE_SESSION_MARKER, "ceshi");)。每当SiftingAppender看到标记为FINALIZE_SESSION的日志事件时,它将终止相关的appender的生命。在到达其寿命结束时,嵌套的appender将停留几秒钟(默认10s)以处理任何延迟发生的事件(如果有的话),然后将关闭。

通过SiftingAppender + MDC实现不同的用户生成不同的日志文件

示例代码如下:

package com.matio.logback.appender.sifting;

import ch.qos.logback.classic.ClassicConstants;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;

// 根据mdc中存储不同的值,把日志输出到不同的文件中
public class MDCSiftingAppenderTest {
    public static void main(String[] args) {
        // 读取resources下logback-mdcsiftingappender.xml作为logback配置文件
        System.setProperty("logback.configurationFile", "appender/sifting/logback-mdcsiftingappender.xml");
        
        Logger logger = LoggerFactory.getLogger("logger123");
    
        // 清除MDC中userid对应的appender,当然也可以重新生成
        logger.info(ClassicConstants.FINALIZE_SESSION_MARKER, "ceshi");
        
        // 没有设置userid时日志都输出到test_unknown.log中了
        for (int i = 0; i < 100; i++) {
            logger.info("info 打印匿名用户的日志" + 111);
        }
        
        // 设置userid=matio,日志都会输出到test_matio.log中
        MDC.put("userid", "matio");
        try {
            logger.info("info 打印用户matio的日志" + 222);
        } finally {
            MDC.remove("userid");
        }
        // 设置userid=zcf,日志都会输出到test_zcf.log中
        MDC.put("userid", "zcf");
        try {
            logger.info("info 打印用户zcf的日志" + 333);
        } finally {
            MDC.remove("userid");
        }
        
        MDC.clear();
    }
}

logback-mdcsiftingappender.xml文件内容如下:

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

    <!-- appender:AppenderAction 解析成 AppenderModel 交给 AppenderModelHandler
        name:非空
        class:非空,反射生成Appender子实现,回调其(LifeCycle)start()方法
    -->
    <appender name="SIFT" class="ch.qos.logback.classic.sift.SiftingAppender">
        <!-- in the absence of the class attribute, it is assumed that the
             desired discriminator type is
             ch.qos.logback.classic.sift.MDCBasedDiscriminator -->
        <!-- 如果class为空,默认是ch.qos.logback.classic.sift.MDCBasedDiscriminator -->
        <discriminator class="ch.qos.logback.classic.sift.MDCBasedDiscriminator">
            <key>userid</key>
            <defaultValue>unknown</defaultValue>
        </discriminator>

        <!-- sift:SiftAction 解析成 SiftModel 交给 SiftModelHandler
            只能包含一个appender
            利用AppenderTracker动态创建appender
        -->
        <sift>
            <appender name="FILE-${userid}" class="ch.qos.logback.core.FileAppender">
                <file>D:\test_logback\test_${userid}.log</file>
                <append>false</append>
                <layout class="ch.qos.logback.classic.PatternLayout">
                    <pattern>%d [%thread] %level %mdc %logger{35} -%kvp -%msg%n</pattern>
                </layout>
            </appender>
        </sift>
    </appender>

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

最终生成了3个日志文件,如下:

image.png

通过SiftingAppender实现不同的contextName生成不同的日志文件

示例代码如下:

package com.matio.logback.appender.sifting;

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

public class ContextSiftingAppenderTest {
    public static void main(String[] args) {
         
        System.setProperty("test.app.name", "app2");
        
        // 读取resources下logback-contextsiftingappender.xml作为logback配置文件
        System.setProperty("logback.configurationFile", "appender/sifting/logback-contextsiftingappender.xml");
        
        Logger logger = LoggerFactory.getLogger("logger123");
        logger.info("info 打印匿名用户的日志" + 111);
    }
}

logback-contextsiftingappender.xml文件如下:

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

    <!-- appender:AppenderAction 解析成 AppenderModel 交给 AppenderModelHandler
        name:非空
        class:非空,反射生成Appender子实现,回调其(LifeCycle)start()方法
        
        可以通过${CONTEXT_NAME}获取contextName
    -->
    
    <contextName>${test.app.name}</contextName>
    
    <appender name="SIFT" class="ch.qos.logback.classic.sift.SiftingAppender">
        <!-- in the absence of the class attribute, it is assumed that the
             desired discriminator type is
             ch.qos.logback.classic.sift.MDCBasedDiscriminator -->
        <!-- 如果class为空,默认是ch.qos.logback.classic.sift.MDCBasedDiscriminator -->
        <discriminator class="ch.qos.logback.classic.sift.ContextBasedDiscriminator">
            <defaultValue>unknown</defaultValue>
        </discriminator>

        <!-- sift:SiftAction 解析成 SiftModel 交给 SiftModelHandler
            只能包含一个appender
            利用AppenderTracker动态创建appender
        -->
        <sift>
            <appender name="FILE-${CONTEXT_NAME}" class="ch.qos.logback.core.FileAppender">
                <file>D:\test_logback\test_${CONTEXT_NAME}.log</file>
                <append>false</append>
                <layout class="ch.qos.logback.classic.PatternLayout">
                    <pattern>%d [%thread] %level %mdc %logger{35} -%kvp -%msg%n</pattern>
                </layout>
            </appender>
        </sift>
    </appender>

    <root level="DEBUG">
        <appender-ref ref="SIFT"/>
    </root>

</configuration>

DBAppender

输出日志到数据库中

logback的DBAppender单独在此包中,需要额外引入依赖:

<!-- logback的DBAppender单独在此包中,需要额外引入 -->
<dependency>
    <groupId>ch.qos.logback.db</groupId>
    <artifactId>logback-classic-db</artifactId>
    <version>1.2.11.1</version>
</dependency>

包结构如下:

image.png

以myslq为例测试DBAppender:

  1. 运行mysql脚本

位置:logback-classic-db-1.2.11.1.jar包中的ch\qos\logback\classic\db\script\mysql.sql

sql脚本执行成功后,会创建以下三张表: image.png

  1. 程序中添加mysql依赖
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.19</version>
</dependency>
  1. 示例代码如下:
package com.matio.logback.appender;

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

public class DBAppenderTest {
    public static void main(String[] args) {
        // 读取resources下logback-dbappender.xml作为logback配置文件
        System.setProperty("logback.configurationFile", "appender/logback-dbappender.xml");
        
        Logger logger = LoggerFactory.getLogger("logger123");
        
        logger.info("info" + 1);
        logger.info("test info {} {}", "matio", 123);
        logger.debug("debug" + 2);
        logger.error("error", new IllegalArgumentException("参数无效"));
    }
}

logback-dbappender.xml文件如下:

<?xml version="1.0" encoding="UTF-8"?>
<configuration debug="true">
    
    <!-- appender:AppenderAction 解析成 AppenderModel 交给 AppenderModelHandler
        name:非空
        class:非空,反射生成Appender子实现,回调其(LifeCycle)start()方法
    -->
    <appender name="DB" class="ch.qos.logback.classic.db.DBAppender">
        <connectionSource class="ch.qos.logback.core.db.DriverManagerConnectionSource">
            <driverClass>com.mysql.cj.jdbc.Driver</driverClass>
            <url>jdbc:mysql://192.168.1.1:3306/demo?useSSL=false&amp;useUnicode=true&amp;characterEncoding=UTF-8&amp;serverTimezone=GMT%2B0</url>
            <user>user</user>
            <password>pwd</password>
        </connectionSource>
    </appender>

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

SocketAppender

通过socket向目标服务端发送日志

每次写日志的时候,都会把日志写到一个LinkedBlockingDeque中,然后由异步线程不断从该队列中取出log发送给服务端

logback-socketappender.xml文件如下:

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

    <!-- appender:AppenderAction 解析成 AppenderModel 交给 AppenderModelHandler
        name:非空
        class:非空,反射生成Appender子实现,回调其(LifeCycle)start()方法
    -->
    <appender name="SOCKET-APPENDER" class="ch.qos.logback.classic.net.SocketAppender">
        <includeCallerData>false</includeCallerData>
        <remoteHost>localhost</remoteHost>
        <port>4560</port>
        <reconnectionDelay>30000</reconnectionDelay>
        <queueSize>128</queueSize>
        <eventDelayLimit>100</eventDelayLimit>
    </appender>
    <root>
        <appender-ref ref="SOCKET-APPENDER"/>
    </root>

</configuration>
  1. includeCallerData:通过log是否可以获取到调用者数据,默认false
  2. remoteHost:远端ip
  3. port:远端port,默认4560
  4. reconnectionDelay:连接失败了等待多少ms重新连接,默认30s,具体格式参考ch.qos.logback.core.util.Duration#valueOf()。如果设置为0,则代表关闭重新连接功能
  5. queueSize: 创建一个以该值为容量的LinkedBlockingDeque,每次写日志的时候会把日志保存到该队列中。当队列大小为1时,到远程接收器的事件传递是同步的。当队列大小大于1时,假设队列中有可用空间,则会对新事件进行排队。使用大于1的队列长度可以通过消除由瞬时网络延迟引起的延迟来提高性能。默认128
  6. eventDelayLimit:它表示在本地队列已满的情况下,在丢弃事件之前等待的时间。如果远程主机持续缓慢地接受事件,则可能会发生这种情况。默认为100ms

示例代码

package com.matio.logback.appender.socket;

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

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

public class SocketAppenderTest {
    public static void main(String[] args) throws InterruptedException {
        // 模拟启动一个socketserver准备接受日志
        new Thread(SocketAppenderTest::openSocketServer).start();
        // 等待socketserver启动完成
        Thread.sleep(3000);
        
        // 读取resources下logback-socketappender.xml作为logback配置文件
        System.setProperty("logback.configurationFile", "appender/logback-socketappender.xml");
        
        Logger logger = LoggerFactory.getLogger("logger123");
        logger.info("info");
        logger.debug("debug");
    }
    
    // 使用文心一言写的demo
    private static void openSocketServer() {
        int portNumber = 4560;
        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("Received2: " + event.getMessage());
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

ServerSocketAppender

原理

启动一个ServerSocket,不断接收客户端连接,而且给每一个客户端创建一个ArrayBlockingQueue,长度为clientQueueSize,然后不断地从这个队列中取log发生给客户端

写日志的时候会将日志插入到每个客户端的ArrayBlockingQueue中,如果插入失败了则代表日志丢失了。所以必须要客户端先建立连接成功,才会收到日志。

logback-serversocketappender.xml文件内容如下:

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

    <!-- appender:AppenderAction 解析成 AppenderModel 交给 AppenderModelHandler
        name:非空
        class:非空,反射生成Appender子实现,回调其(LifeCycle)start()方法
    -->
    <appender name="SOCKET-APPENDER" class="ch.qos.logback.classic.net.server.ServerSocketAppender">
        <includeCallerData>false</includeCallerData>
        <address>localhost</address>
        <port>4560</port>
        <!-- 请求的传入连接队列的最大长度。默认50 -->
        <backlog>50</backlog>
        <clientQueueSize>100</clientQueueSize>
    </appender>

    <root>
        <appender-ref ref="SOCKET-APPENDER"/>
    </root>

</configuration>
  1. includeCallerData:通过log是否可以获取到调用者数据,默认false
  2. address:本机ip,必填
  3. port:端口,默认4560
  4. backlog:请求的传入连接队列的最大长度。默认50
  5. clientQueueSize:appender会为每一个建立连接的客户端创建一个ArrayBlockingQueue,长度为该值,默认100

源码解析:

@Override
public void start() {
    if (isStarted())
        return;
    try {
        ServerSocket socket = getServerSocketFactory().createServerSocket(getPort(), getBacklog(),
                getInetAddress());
        ServerListener<RemoteReceiverClient> listener = createServerListener(socket);
        runner = createServerRunner(listener, getContext().getScheduledExecutorService());
        runner.setContext(getContext());
        getContext().getScheduledExecutorService().execute(runner);
        super.start();
    } catch (Exception ex) {
        addError("server startup error: " + ex, ex);
    }
}
  1. 实例化一个ServerSocket对象,准备接收客户端连接了
  2. 实例化一个ServerListener对象,它主要负责为每个连接到该ServerSocket的客户端实例化一个RemoteReceiverStreamClient对象
  3. 实例化一个RemoteReceiverServerRunner对象,主要负责接收客户端连接,然后调用ServerListener去创建RemoteReceiverStreamClient
  4. 异步启动任务RemoteReceiverServerRunner,执行见下:
public void run() {
    setRunning(true);
    try {
        addInfo("listening on " + listener);
        while (!Thread.currentThread().isInterrupted()) {
            // 接收客户端连接,等价于Socket socket = serverSocket.accept()
            T client = listener.acceptClient();
            // 为该客户端创建一个ArrayBlockingQueue,长度为clientQueueSize
            if (!configureClient(client)) {
                addError(client + ": connection dropped");
                client.close();
                continue;
            }
            // 异步任务处理该客户端
            executor.execute(new ClientWrapper(client));
        }
    } catch (InterruptedException ex) {
        assert true; // ok... we'll shut down
    } catch (Exception ex) {
        addError("listener: " + ex);
    }

    setRunning(false);
    addInfo("shutting down");
    listener.close();
}

在一个死循环中,不断接收客户端连接,然后为每个客户端创建一个ArrayBlockingQueue,长度为clientQueueSize,然后启动异步任务,不断地从这个队列中取出log发送给该客户端

写日志逻辑:

@Override
protected void append(E event) {
    if (event == null)
        return;
    postProcessEvent(event);
    final Serializable serEvent = getPST().transform(event);
    runner.accept(new ClientVisitor<RemoteReceiverClient>() {
        public void visit(RemoteReceiverClient client) {
            client.offer(serEvent);
        }
    });
}

将日志插入到每个客户端的ArrayBlockingQueue中,如果插入失败了则代表日志丢失了。所以必须要客户端先建立连接成功,才会收到日志。

示例代码

先启动serversocket打印日志:

package com.matio.logback.appender.socket;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Random;
import java.util.concurrent.TimeUnit;

public class ServerSocketAppenderTest {
    public static void main(String[] args) throws InterruptedException {
        // 读取resources下logback-serversocketappender.xml作为logback配置文件
        System.setProperty("logback.configurationFile", "appender/logback-serversocketappender.xml");
        
        // 解析xml,启动一个SocketServer
        Logger logger = LoggerFactory.getLogger("logger123");
        while (true) {
            logger.info("info");
            logger.debug("debug");
            TimeUnit.MILLISECONDS.sleep(new Random().nextInt(1000));
        }
    }
}

再启动socket客户端准备接收日志:

package com.matio.logback.appender.socket;

import ch.qos.logback.classic.spi.LoggingEventVO;

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

public class Client {
    public static void main(String[] args) {
        try {
            Socket socket = new Socket("localhost", 4560);
            ObjectInputStream ois = new ObjectInputStream(socket.getInputStream());
            Object resp;
            while ((resp = ois.readObject()) != null) {
                LoggingEventVO event = (LoggingEventVO) resp;
                System.out.println("Received: " + event.getMessage());
            }
            ois.close();
            socket.getInputStream().close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}