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的片段,留有對比參數,希望可以匯總大家的實踐和想法,完善最佳實踐,共勉。