java代碼動態自定義logback日志Appender


Java 程序中使用 Logback,需要依賴三個 jar 包,分別是 slf4j-api,logback-core,logback-classic,在 maven 項目中依賴如下:

<!-- springboot項目默認了logback的依賴,無需手動添加 -->
 <dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-api</artifactId>
    <version>1.7.5</version>
</dependency>
<dependency>
    <groupId>ch.qos.logback</groupId>
    <artifactId>logback-core</artifactId>
    <version>1.0.11</version>
</dependency>
<dependency>
    <groupId>ch.qos.logback</groupId>
    <artifactId>logback-classic</artifactId>
    <version>1.0.11</version>
</dependency>

Logback 在啟動時,根據以下步驟尋找配置文件:

1. 在 classpath 中尋找 logback-test.xml文件;

2. 如果找不到 logback-test.xml,則在 classpath 中尋找 logback.groovy 文件;

3. 如果找不到 logback.groovy,則在 classpath 中尋找 logback.xml文件;

4. 如果上述的文件都找不到,則 logback 會使用 JDK 的 SPI 機制查找 META-INF/services/ch.qos.logback.classic.spi.Configurator 中的 logback 配置實現類, 這個實現類必須實現 Configuration 接口,使用它的實現來進行配置;

5. 如果上述操作都不成功,logback 就會使用它自帶的 BasicConfigurator 來配置,並將日志輸出到 console;

logback的變量作用於有三種:local,context,system

1. local 作用域在配置文件內有效;

2. context 作用域的有效范圍延伸至 logger context;

3. system 作用域的范圍最廣,整個 JVM 內都有效;

logback 在替換變量時,首先搜索 local 變量,然后搜索 context,然后搜索 system,在spring項目中,應將變量的作用域設置為context,並交給spring控制

## application.yml文件配置
spring:
  profiles:
    active: dev
  application:
    name: msg-consumer
logging:
  ## 自定義logback配置文件名,交給spring
  config: classpath:logback-custom.xml
logback:
  ## 在配置文件中指定日志路徑
  logHome: logs
<!--  logback-custom.xml文件配置,放在resources目錄下,與application.yml同級 -->
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <!-- logback.xml和logback-test.xml會被logback組件直接讀取 -->
    <!-- 如果要交給spring管理,需要修改配置文件名為logback-spring.xml -->
    <!-- springProfile標簽可以為不同的環境使用不同的配置,設置scope="context",則在項目上下文中可以使用該變量 -->
    <springProperty scope="context" name="LOG_HOME" source="logback.logHome" defaultValue="log"/>
    <springProperty scope="context" name="LOG_NAME_PREFIX" source="spring.application.name" defaultValue=""/>
    <!-- %m輸出的信息,%p日志級別,%t線程名,%d日期,%c類的全名,%i索引【從數字0開始遞增】,,, -->
    <property scope="context" name="pattern" value="%d{yyyy-MM-dd HH:mm:ss} [%thread] %level %logger{35}:%line - %msg%n"/>
    <timestamp scope="context" key="bySecond" datePattern="yyyyMMddHHmmss"/>
    <property scope="context" name="logPath" value="${LOG_HOME}/${LOG_NAME_PREFIX}"/>
    <!-- appender是configuration的子節點,是負責寫日志的組件。 -->
    <!-- ch.qos.logback.core.ConsoleAppender:把日志輸出到控制台 -->
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <!-- pattern節點,用來設置日志的輸入格式 -->
            <pattern>${pattern}</pattern>
            <!-- 記錄日志的編碼 -->
            <charset>UTF-8</charset>
        </encoder>
    </appender>
    <!-- RollingFileAppender:滾動記錄文件,先將日志記錄到指定文件,當符合某個條件時,將日志記錄到其他文件 -->
    <appender name="ALL" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <File>${logPath}-all.log</File>
        <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
            <!-- 活動文件的名字會根據fileNamePattern的值,每隔一段時間改變一次,文件名:logger/sys.2020-03-28.0.logger -->
            <fileNamePattern>${logPath}/${LOG_NAME_PREFIX}-all.%d.%i.log</fileNamePattern>
            <maxFileSize>50MB</maxFileSize>
            <maxHistory>30</maxHistory>
            <totalSizeCap>2GB</totalSizeCap>
            <cleanHistoryOnStart>true</cleanHistoryOnStart>
        </rollingPolicy>
        <encoder>
            <pattern>${pattern}</pattern>
            <charset>UTF-8</charset>
        </encoder>
    </appender>

    <appender name="INFO" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <File>${logPath}-info.log</File>
        <!--只輸出INFO-->
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <!--過濾 INFO-->
            <level>INFO</level>
            <!--匹配到就禁止-->
            <onMatch>ACCEPT</onMatch>
            <!--沒有匹配到就允許-->
            <onMismatch>DENY</onMismatch>
        </filter>
        <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
            <fileNamePattern>${logPath}/${LOG_NAME_PREFIX}-info.%d.%i.log</fileNamePattern>
            <maxFileSize>50MB</maxFileSize>
            <maxHistory>30</maxHistory>
            <totalSizeCap>2GB</totalSizeCap>
            <cleanHistoryOnStart>true</cleanHistoryOnStart>
        </rollingPolicy>
        <encoder>
            <pattern>${pattern}</pattern>
            <charset>UTF-8</charset>
        </encoder>
    </appender>

    <appender name="ERROR" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <File>${logPath}-error.log</File>
        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
            <!--設置日志級別,過濾掉info日志,只輸入error日志-->
            <level>ERROR</level>
        </filter>
        <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
            <fileNamePattern>${logPath}/${LOG_NAME_PREFIX}-error.%d.%i.log</fileNamePattern>
            <maxFileSize>50MB</maxFileSize>
            <maxHistory>30</maxHistory>
            <totalSizeCap>2GB</totalSizeCap>
            <cleanHistoryOnStart>true</cleanHistoryOnStart>
        </rollingPolicy>
        <encoder>
            <pattern>${pattern}</pattern>
            <charset>UTF-8</charset>
        </encoder>
    </appender>

    <!-- 記錄sql -->
    <appender name="SQL" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <File>${logPath}sql.log</File>
        <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
            <fileNamePattern>${logPath}/${LOG_NAME_PREFIX}-sql.%d.%i.log</fileNamePattern>
            <maxFileSize>50MB</maxFileSize>
            <maxHistory>30</maxHistory>
            <totalSizeCap>2GB</totalSizeCap>
            <cleanHistoryOnStart>true</cleanHistoryOnStart>
        </rollingPolicy>
        <encoder>
            <pattern>${pattern}</pattern>
            <charset>UTF-8</charset>
        </encoder>
    </appender>
    
    <!-- 按發布環境,控制激活的日志級別 -->
    <springProfile name="dev,test">
        <!-- 控制台輸出日志級別 -->
        <root level="INFO">
            <appender-ref ref="STDOUT"/>
            <appender-ref ref="ALL"/>
        </root>

        <!-- 定項目中某個包,eg:cn.henry.study為根包,也就是只要是發生在這個根包下面的所有日志操作行為的權限都是INFO -->
        <!-- 級別依次為【從高到低】:FATAL > ERROR > WARN > INFO > DEBUG > TRACE  -->
        <logger name="cn.henry.study" level="INFO" additivity="false">
            <appender-ref ref="INFO"/>
            <appender-ref ref="ERROR"/>
            <appender-ref ref="STDOUT"/>
        </logger>
        
        <!-- mybatis loggers 可以按包的層級指定不同的日志級別 -->
        <logger name="cn.henry.study.web.mapper" level="DEBUG" additivity="false">
            <appender-ref ref="SQL"/>
            <appender-ref ref="STDOUT"/>
        </logger>
    </springProfile>

    <springProfile name="pro">
        <root level="INFO">
            <appender-ref ref="SERVICE_ALL"/>
            <appender-ref ref="STDOUT"/>
        </root>
    </springProfile>
</configuration>

以上配置可滿足日常開發的大部分需求,可以很方便的將info日志與error隔離開,並按照給定logger輸出不同配置文件中,但存在以下問題:

1. 如果需要按照業務,將某些不同包下的日志,集中輸出到指定的日志文件中,上述配置就難以實現;

2. 上述xml文件會產生大量重復配置,如appender和logger的配置,添加非常的繁瑣,造成配置文件龐大;

解決方案:

1. 通過logback的SiftingAppender,通過ThreadLocal的方式動態切換,這個方案在我之前的博客中有詳細實現,與業務耦合較高;

2. 在java代碼中動態生成Appender,輕量,易拓展,實現代碼如下;

import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.LoggerContext;
import ch.qos.logback.classic.encoder.PatternLayoutEncoder;
import ch.qos.logback.classic.filter.LevelFilter;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.Appender;
import ch.qos.logback.core.ConsoleAppender;
import ch.qos.logback.core.FileAppender;
import ch.qos.logback.core.filter.Filter;
import ch.qos.logback.core.rolling.RollingFileAppender;
import ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP;
import ch.qos.logback.core.rolling.TimeBasedRollingPolicy;
import ch.qos.logback.core.spi.FilterReply;
import ch.qos.logback.core.util.FileSize;
import ch.qos.logback.core.util.OptionHelper;
import cn.henry.study.common.enums.LogNameEnum;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.nio.charset.Charset;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Optional;

/**
 * description: 自定義的日志工具類,
 * 需要在logback.xml,logback-spring.xml或自定義的logback-custom.xml中寫入基礎配置
 *
 * @citation https://blog.csdn.net/lw656697752/article/details/84904938
 * @citation https://www.cnblogs.com/leohe/p/12117183.html
 * @author Hlingoes
 * @date 2020/6/10 19:38
 */
public class LoggerUtils {
    private static String consoleAppenderName = "serve-console";
    private static String maxFileSize = "50MB";
    private static String totalSizeCap = "10GB";
    private static int maxHistory = 30;
    private static ConsoleAppender defaultConsoleAppender = null;

    static {
        Map<String, Appender<ILoggingEvent>> appenderMap = allAppenders();
        appenderMap.forEach((key, appender) -> {
            // 如果logback配置文件中,已存在窗口輸出的appender,則直接使用;不存在則重新生成
            if (appender instanceof ConsoleAppender) {
                defaultConsoleAppender = (ConsoleAppender) appender;
                return;
            }
        });
    }

    /**
     * description: 獲取自定義的logger日志,在指定日志文件logNameEnum.getLogName()中輸出日志
     * 日志中會包括所有線程及方法堆棧信息
     *
     * @param logNameEnum
     * @param clazz
     * @return org.slf4j.Logger
     * @author Hlingoes 2020/6/10
     */
    public static Logger getLogger(LogNameEnum logNameEnum, Class clazz) {
        ch.qos.logback.classic.Logger logger = (ch.qos.logback.classic.Logger) LoggerFactory.getLogger(clazz);
        LoggerContext loggerContext = logger.getLoggerContext();
        RollingFileAppender errorAppender = createAppender(logNameEnum.getLogName(), Level.ERROR, loggerContext);
        RollingFileAppender infoAppender = createAppender(logNameEnum.getLogName(), Level.INFO, loggerContext);
        Optional<ConsoleAppender> consoleAppender = Optional.ofNullable(defaultConsoleAppender);
        ConsoleAppender realConsoleAppender = consoleAppender.orElse(createConsoleAppender(loggerContext));
        // 設置不向上級打印信息
        logger.setAdditive(false);
        logger.addAppender(errorAppender);
        logger.addAppender(infoAppender);
        logger.addAppender(realConsoleAppender);
        return logger;
    }

    /**
     * description: 創建日志文件的file appender
     *
     * @param name
     * @param level
     * @return ch.qos.logback.core.rolling.RollingFileAppender
     * @author Hlingoes 2020/6/10
     */
    private static RollingFileAppender createAppender(String name, Level level, LoggerContext loggerContext) {
        RollingFileAppender appender = new RollingFileAppender();
        // 這里設置級別過濾器
        appender.addFilter(createLevelFilter(level));
        // 設置上下文,每個logger都關聯到logger上下文,默認上下文名稱為default。
        // 但可以使用<scope="context">設置成其他名字,用於區分不同應用程序的記錄。一旦設置,不能修改。
        appender.setContext(loggerContext);
        // appender的name屬性
        appender.setName(name.toUpperCase() + "-" + level.levelStr.toUpperCase());
        // 讀取logback配置文件中的屬性值,設置文件名
        String logPath = OptionHelper.substVars("${logPath}-" + name + "-" + level.levelStr.toLowerCase() + ".log", loggerContext);
        appender.setFile(logPath);
        appender.setAppend(true);
        appender.setPrudent(false);
        // 加入下面兩個節點
        appender.setRollingPolicy(createRollingPolicy(name, level, loggerContext, appender));
        appender.setEncoder(createEncoder(loggerContext));
        appender.start();
        return appender;
    }

    /**
     * description: 創建窗口輸入的appender
     *
     * @param
     * @return ch.qos.logback.core.ConsoleAppender
     * @author Hlingoes 2020/6/10
     */
    private static ConsoleAppender createConsoleAppender(LoggerContext loggerContext) {
        ConsoleAppender appender = new ConsoleAppender();
        appender.setContext(loggerContext);
        appender.setName(consoleAppenderName);
        appender.addFilter(createLevelFilter(Level.DEBUG));
        appender.setEncoder(createEncoder(loggerContext));
        appender.start();
        return appender;
    }

    /**
     * description: 設置日志的滾動策略
     *
     * @param name
     * @param level
     * @param context
     * @param appender
     * @return ch.qos.logback.core.rolling.TimeBasedRollingPolicy
     * @author Hlingoes 2020/6/10
     */
    private static TimeBasedRollingPolicy createRollingPolicy(String name, Level level, LoggerContext context, FileAppender appender) {
        // 讀取logback配置文件中的屬性值,設置文件名
        String fp = OptionHelper.substVars("${logPath}/${LOG_NAME_PREFIX}-" + name + "-" + level.levelStr.toLowerCase() + "_%d{yyyy-MM-dd}_%i.log", context);
        TimeBasedRollingPolicy rollingPolicyBase = new TimeBasedRollingPolicy<>();
        // 設置上下文,每個logger都關聯到logger上下文,默認上下文名稱為default。
        // 但可以使用<scope="context">設置成其他名字,用於區分不同應用程序的記錄。一旦設置,不能修改。
        rollingPolicyBase.setContext(context);
        // 設置父節點是appender
        rollingPolicyBase.setParent(appender);
        // 設置文件名模式
        rollingPolicyBase.setFileNamePattern(fp);
        SizeAndTimeBasedFNATP sizeAndTimeBasedFNATP = new SizeAndTimeBasedFNATP();
        // 最大日志文件大小
        sizeAndTimeBasedFNATP.setMaxFileSize(FileSize.valueOf(maxFileSize));
        rollingPolicyBase.setTimeBasedFileNamingAndTriggeringPolicy(sizeAndTimeBasedFNATP);
        // 設置最大歷史記錄為30條
        rollingPolicyBase.setMaxHistory(maxHistory);
        // 總大小限制
        rollingPolicyBase.setTotalSizeCap(FileSize.valueOf(totalSizeCap));
        rollingPolicyBase.start();

        return rollingPolicyBase;
    }

    /**
     * description: 設置日志的輸出格式
     *
     * @param context
     * @return ch.qos.logback.classic.encoder.PatternLayoutEncoder
     * @author Hlingoes 2020/6/10
     */
    private static PatternLayoutEncoder createEncoder(LoggerContext context) {
        PatternLayoutEncoder encoder = new PatternLayoutEncoder();
        // 設置上下文,每個logger都關聯到logger上下文,默認上下文名稱為default。
        // 但可以使用<scope="context">設置成其他名字,用於區分不同應用程序的記錄。一旦設置,不能修改。
        encoder.setContext(context);
        // 設置格式
        String pattern = OptionHelper.substVars("${pattern}", context);
        encoder.setPattern(pattern);
        encoder.setCharset(Charset.forName("utf-8"));
        encoder.start();
        return encoder;
    }

    /**
     * description: 設置打印日志的級別
     *
     * @param level
     * @return ch.qos.logback.core.filter.Filter
     * @author Hlingoes 2020/6/10
     */
    private static Filter createLevelFilter(Level level) {
        LevelFilter levelFilter = new LevelFilter();
        levelFilter.setLevel(level);
        levelFilter.setOnMatch(FilterReply.ACCEPT);
        levelFilter.setOnMismatch(FilterReply.DENY);
        levelFilter.start();
        return levelFilter;
    }

    /**
     * description: 讀取logback配置文件中的所有appender
     *
     * @param
     * @return java.util.Map<java.lang.String, ch.qos.logback.core.Appender < ch.qos.logback.classic.spi.ILoggingEvent>>
     * @author Hlingoes 2020/6/10
     */
    private static Map<String, Appender<ILoggingEvent>> allAppenders() {
        Map<String, Appender<ILoggingEvent>> appenderMap = new HashMap<>();
        LoggerContext context = (LoggerContext) LoggerFactory.getILoggerFactory();
        for (ch.qos.logback.classic.Logger logger : context.getLoggerList()) {
            for (Iterator<Appender<ILoggingEvent>> index = logger.iteratorForAppenders(); index.hasNext(); ) {
                Appender<ILoggingEvent> appender = index.next();
                appenderMap.put(appender.getName(), appender);
            }
        }
        return appenderMap;
    }

}
import org.apache.commons.lang3.StringUtils;

/**
 * description: 日志枚舉類,防止隨意生成日志文件
 *
 * @author Hlingoes 2020/6/10
 */
public enum LogNameEnum {
    COMMON("common"),
    WEB_SERVER("webServer"),
    TEST("test"),
    ;

    private String logName;

    LogNameEnum(String fileName) {
        this.logName = fileName;
    }

    public String getLogName() {
        return logName;
    }

    public void setLogName(String logName) {
        this.logName = logName;
    }

    /**
     * description: 獲取枚舉類
     *
     * @param value
     * @return cn.henry.study.common.enums.LogNameEnum
     * @author Hlingoes 2020/6/10
     */
    public static LogNameEnum getAwardTypeEnum(String value) {
        LogNameEnum[] arr = values();
        for (LogNameEnum item : arr) {
            if (null != item && StringUtils.isNotBlank(item.logName)) {
                return item;
            }
        }
        return null;
    }
}

 

import ch.qos.logback.classic.LoggerContext;
import ch.qos.logback.core.util.OptionHelper;
import cn.henry.study.common.enums.LogNameEnum;
import cn.henry.study.common.utils.LoggerUtils;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * description:
 *
 * @author Hlingoes
 * @date 2020/5/22 23:45
 */
public class PracticeTest {
    private static Logger logger = LoggerFactory.getLogger(PracticeTest.class);
    private static Logger testLogger = LoggerUtils.getLogger(LogNameEnum.TEST, PracticeTest.class);

    @Test
    public void loggerUtilsTest() {
        LoggerContext context = (LoggerContext) LoggerFactory.getILoggerFactory();
        /**
         *  <property scope="context" name="LOG_HOME" value="log"/>
         *  <property scope="context" name="LOG_NAME_PREFIX" value="common"/>
         */
        String oph = OptionHelper.substVars("${LOG_HOME}/${LOG_NAME_PREFIX}/test-log.log", context);
     // 在日志文件common-info.log中 logger.info(
"logger默認配置的日志輸出");
     // 在日志文件common-test-info.log中 testLogger.info(
"testLogger#####{}####", oph); testLogger.info("testLogger看到這條信息就是info");
     // 在日志文件common-test-error.log中 testLogger.error(
"testLogger看到這條信息就是error"); } }

 

代碼是本人的git項目file-message-server的片段,留有對比參數,希望可以匯總大家的實踐和想法,完善最佳實踐,共勉。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM