前提
当前(2022-02
前后)日志框架logback
的最新版本1.3.0
已经更新到1.3.0-alpha14
版本,此版本为非stable
版本,相对于最新稳定版1.2.10
来说,虽然slf4j-api
版本升级了,但使用的API
大体不变,对于XML
配置来看提供了import
标签对于多appender
来说可以简化配置。鉴于软件最新版本强迫症,这里基于1.3.0-alpha14
版本分析一下常用的logback
配置项以及一些实践经验。
日志等级
日志等级的定义见Level
类:
序号 | 日志级别 | 值 | 备注 |
---|---|---|---|
1 |
OFF |
Integer.MAX_VALUE |
关闭日志打印 |
2 |
TRACE |
5000 |
- |
3 |
DEBUG |
10000 |
- |
4 |
INFO |
20000 |
- |
5 |
WARN |
30000 |
- |
6 |
ERROR |
40000 |
- |
7 |
ALL |
Integer.MIN_VALUE |
打印所有日志 |
日志等级的值越大,级别越高,级别由低到高(左到右)排列如下:
TRACE < DEBUG < INFO < WARN < ERROR
日志等级一般会作为日志事件的过滤条件或者查询条件,在一些特定组件中,可以通过配置项去决定丢弃低级别的日志事件或者忽略指定级别的日志事件。
依赖引入
因为当前的1.3.0-alpha14
版本太过"新",大部分主流框架尚未集成,如果要尝鲜最好通过BOM
全局指定对应依赖的版本:
org.slf4j
slf4j-api
2.0.0-alpha6
ch.qos.logback
logback-classic
1.3.0-alpha14
ch.qos.logback
logback-core
1.3.0-alpha14
org.slf4j
slf4j-api
ch.qos.logback
logback-core
ch.qos.logback
logback-classic
logback.xml基本配置示例
1.2.x
和1.3.x
提供的API
基本没有改变,并且1.3.x
向前兼容了旧的配置方式,提供了import
标签用于简化class
的指定:
1.2.x
(旧的配置方式)前的配置方式:
[%date{ISO8601}] [%level] %logger{80} [%thread] [%X{TRACE_ID}] - %msg%n
1.3.x
可用的新配置方式:
[%date{ISO8601}] [%level] %logger{80} [%thread] [%X{TRACE_ID}] - %msg%n
对于单个Appender
配置来看,import
标签的引入看起来无法简化配置,但是对于多Appender
配置来看可以相对简化class
的指定,例如:
/data/log-center/${app}/${filename}.log
/data/log-center/${app}/${filename}.%d{yyyy-MM-dd}.log
14
[%date{ISO8601}] [%level] %logger{80} [%thread] [%X{TRACE_ID}] ${app} - %msg%n
INFO,WARN
ACCEPT
DENY
/data/log-center/${app}/${filename}-error.log
/data/log-center/${app}/${filename}-error.%d{yyyy-MM-dd}.log
14
[%date{ISO8601}] [%level] %logger{80} [%thread] [%X{TRACE_ID}] ${app} - %msg%n
ERROR
[%date{ISO8601}] [%level] %logger{80} [%thread] [%X{TRACE_ID}] - %msg%n
DEBUG
1024
0
256
0
上面的配置是某个API
网关的logback.xml
配置示例,这里用到了一个自定义Filter
实现IncludeLevelSetFilter
:
// cn.vlts.logback.IncludeLevelSetFilter
public class IncludeLevelSetFilter extends AbstractMatcherFilter {
private String levels;
private Set levelSet;
@Override
public FilterReply decide(ILoggingEvent event) {
return levelSet.contains(event.getLevel()) ? onMatch : onMismatch;
}
public void setLevels(String levels) {
this.levels = levels;
this.levelSet = Arrays.stream(levels.split(","))
.map(item -> Level.toLevel(item, Level.INFO)).collect(Collectors.toSet());
}
@Override
public void start() {
if (Objects.nonNull(this.levels)) {
super.start();
}
}
}
IncludeLevelSetFilter
用于接受指定日志级别集合的日志记录,如果有更加精细的日志过滤条件(内置常用的LevelFilter
、ThresholdFilter
等无法满足实际需求),可以自行实现ch.qos.logback.core.filter.Filter
接口定制日志事件过滤策略。这份文件定义了五个appender
,其中有2
个用于异步增强,核心appender
有3
个:
STDOUT
:ConsoleAppender
,标准输出同步日志打印,级别为DEBUG
或以上ASYNC_INFO
(INFO
):RollingFileAppender
,异步滚动文件追加日志打印,级别为INFO
或者WARN
,追加到文件/data/log-center/api-gateway/server.log
,归档文件格式为/data/log-center/api-gateway/server-${yyyy-MM-dd}.log.${compression_suffix}
,归档文件最多保存14
个副本ASYNC_ERROR
(ERROR
):RollingFileAppender
,异步滚动文件追加日志打印,级别为ERROR
,追加到文件/data/log-center/api-gateway/server-error.log
,归档文件格式为/data/log-center/api-gateway/server-error-${yyyy-MM-dd}.log.${compression_suffix}
,归档文件最多保存14
个副本
常用的Appender及其参数
常用的Appender
有:
ConsoleAppender
FileAppender
RollingFileAppender
AsyncAppender
其中,RollingFileAppender
是FileAppender
的扩展(子类),现实场景中ConsoleAppender
和RollingFileAppender
的适用范围更广。从类继承关系上看,ConsoleAppender
和FileAppender
都支持定义Encoder
,最常用的Encoder
实现就是PatternLayoutEncoder
,用于定制日志事件的最终输出格式。关于Encoder
,由于其参数格式太过灵活,参数众多,限于篇幅本文不会展开介绍。
ConsoleAppender
ConsoleAppender
用于追加日志到控制台,对于Java
应用来说就是追加到System.out
或者System.err
。ConsoleAppender
支持的参数如下:
参数 | 类型 | 默认值 | 描述 |
---|---|---|---|
encoder |
ch.qos.logback.core.encoder.Encoder |
PatternLayoutEncoder |
用于定义Encoder |
target |
String |
System.out |
定义输出目标,可选值System.out 或System.err |
withJansi |
boolean |
false |
是否支持Jansi ,这是一个支持多彩ANSI 编码的类库,用于输出彩色控制台字体 |
ConsoleAppender
的使用例子如下:
[%date{ISO8601}] [%level] %logger{80} [%thread] [%X{TRACE_ID}] - %msg%n
RollingFileAppender
RollingFileAppender
是FileAppender
的子类,支持输出日志到文件中,并且支持通过滚动规则(RollingPolicy
)的设置,可以安装内置或者自定义规则去分割、归档日志文件。RollingFileAppender
支持的参数如下:
参数 | 类型 | 默认值 | 描述 |
---|---|---|---|
file |
String |
- |
用于定义当前日志输出的目标文件 |
append |
boolean |
true |
用于定义当前日志输出是否追加模式 |
rollingPolicy |
ch.qos.logback.core.rolling.RollingPolicy |
- |
日志文件滚动策略 |
triggeringPolicy |
ch.qos.logback.core.rolling.TriggeringPolicy |
- |
日志文件滚动时机触发策略 |
prudent |
boolean |
false |
是否支持prudent 模式(开启此模式会在FileLock 保护下写入日志文件),FileAppender 支持此模式 |
常用的RollingPolicy
内置实现有:
TimeBasedRollingPolicy
:最常用的日志滚动策略,基于日期时间进行滚动分割和归档
参数 | 类型 | 默认值 | 描述 |
---|---|---|---|
fileNamePattern |
String |
- |
文件名格式,例如/var/log/app/server.%d{yyyy-MM-dd, UTC}.log.gz |
maxHistory |
int |
- |
最大归档文件数量 |
totalSizeCap |
FileSize |
- |
所有归档文件总大小的上限 |
cleanHistoryOnStart |
boolean |
false |
标记为true 则Appender 启动时候清理(不合法的)归档日志文件 |
SizeAndTimeBasedRollingPolicy
:基于日志文件大小或者日期时间进行滚动分割和归档
参数 | 类型 | 默认值 | 描述 |
---|---|---|---|
fileNamePattern |
String |
- |
文件名格式,例如/var/log/app/server.%d{yyyy-MM-dd, UTC}.%i.log.gz |
maxHistory |
int |
- |
最大归档文件数量 |
totalSizeCap |
FileSize |
- |
所有归档文件总大小的上限 |
cleanHistoryOnStart |
boolean |
false |
标记为true 则Appender 启动时候清理(不合法的)归档日志文件 |
FixedWindowRollingPolicy
:基于日志文件大小或者日期时间进行滚动分割和归档
参数 | 类型 | 默认值 | 描述 |
---|---|---|---|
fileNamePattern |
String |
- |
文件名格式,例如/var/log/app/server.%d{yyyy-MM-dd, UTC}.log.gz |
minIndex |
int |
- |
窗口索引下界 |
maxIndex |
int |
- |
窗口索引上界 |
常用的TriggeringPolicy
内置实现有:
SizeBasedTriggeringPolicy
:基于文件大小的触发策略DefaultTimeBasedFileNamingAndTriggeringPolicy
(logback
内部使用):基于日期时间和文件名通过判断系统日期时间触发
这里值得注意的几点:
TimeBasedRollingPolicy
自身也实现了TriggeringPolicy
接口(委托到DefaultTimeBasedFileNamingAndTriggeringPolicy
中执行),提供了兜底的日志文件滚动时机触发策略,所以在使用TimeBasedRollingPolicy
的时候可以不需要指定具体的triggeringPolicy
实例SizeAndTimeBasedRollingPolicy
使用了子组件SizeAndTimeBasedFNATP
实现,旧版本一般使用SizeAndTimeBasedFNATP
实现基于文件大小或者日期时间进行日志滚动归档功能,此组件在新版本中建议使用SizeAndTimeBasedRollingPolicy
替代logback
会基于参数fileNamePattern
中定义的文件名后缀去选择对应的归档日志文件压缩算法,例如.zip
会选用ZIP
压缩算法,.gz
会选用GZIP
压缩算法SizeAndTimeBasedRollingPolicy
和FixedWindowRollingPolicy
的fileNamePattern
参数都支持%i
占位符,用于定义归档文件的索引值,其实索引为0
FixedWindowRollingPolicy
和SizeBasedTriggeringPolicy
组合使用可以实现基于文件大小进行日志滚动的功能(TimeBasedRollingPolicy
的对标功能)
AsyncAppender
AsyncAppender
用于异步记录日志,需要搭配其他类型的Appender
使用,直观上看就是把"异步"功能赋予其他Appender
实例。AsyncAppender
支持的参数如下:
参数 | 类型 | 默认值 | 描述 |
---|---|---|---|
queueSize |
int |
256 |
存放日志事件的阻塞队列的最大容量 |
discardingThreshold |
int |
queueSize / 5 |
日志事件丢弃阈值,阻塞队列剩余容量小于此阈值,会丢弃除了WARN 和ERROR 级别的其他所有级别的日志事件,此阈值设置为0 相当于不会丢弃任意日志事件 |
includeCallerData |
boolean |
false |
日志事件中是否包含调用者数据,设置为true 会添加调用线程信息、MDC 中的数据等 |
maxFlushTime |
int |
1000 |
异步日志写入工作线程退出的最大等待时间,单位为毫秒 |
neverBlock |
boolean |
false |
是否永不阻塞(当前应用的调用线程),设置为true 的时候队列满了会直接丢弃当前新添加的日志事件 |
需要通过
标签关联一个已经存在的Appender
实例到一个全新的AsyncAppender
实例中,并且一个AsyncAppender
实例是可以基于多个
标签添加多个Appender
实例,例如:
[%date{ISO8601}] [%level] %logger{80} [%thread] [%X{TRACE_ID}] - %msg%n
1024
0
指定配置文件进行初始化
logback
内置的初始化策略(按照优先级顺序)如下:
- 通过
ClassPath
中的logback-test.xml
文件初始化 - 通过
ClassPath
中的logback.xml
文件初始化 - 通过
SPI
的方式由ClassPath
中的META-INF\services\ch.qos.logback.classic.spi.Configurator
进行初始化 - 如果前面三步都没有配置,则通过
BasicConfigurator
初始化,提供最基础的日志处理功能
可以通过命令行参数logback.configurationFile
直接指定外部的logback
配置文件(后缀必须为.xml
或者.groovy
),这种初始化方式会忽略内置的初始化策略,例如:
java -Dlogback.configurationFile=/path/conf/config.xml app.jar
或者设置系统参数(下面的Demo
来自官方例子):
public class ServerMain {
public static void main(String args[]) throws IOException, InterruptedException {
// must be set before the first call to LoggerFactory.getLogger();
// ContextInitializer.CONFIG_FILE_PROPERTY is set to "logback.configurationFile"
System.setProperty(ContextInitializer.CONFIG_FILE_PROPERTY, "/path/to/config.xml");
...
}
}
这种方式要求尽量不能存在静态成员变量调用了LoggerFactory.getLogger()
方法,因为有可能会导致提前使用内置的初始化策略进行初始化。
编程式初始化
为了完全控制logback
的初始化,可以使用纯编程式进行设置(下面的编程式配置按照"最佳实践"中的配置文件进行编写):
import ch.qos.logback.classic.AsyncAppender;
import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.Logger;
import ch.qos.logback.classic.LoggerContext;
import ch.qos.logback.classic.encoder.PatternLayoutEncoder;
import ch.qos.logback.classic.filter.ThresholdFilter;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.ConsoleAppender;
import ch.qos.logback.core.rolling.RollingFileAppender;
import ch.qos.logback.core.rolling.TimeBasedRollingPolicy;
import org.slf4j.LoggerFactory;
/**
* @author throwable
* @version v1
* @description
* @since 2022/2/13 13:09
*/
public class LogbackLauncher {
public static void main(String[] args) throws Exception {
LoggerContext loggerContext = (LoggerContext) org.slf4j.LoggerFactory.getILoggerFactory();
loggerContext.reset();
Logger rootLogger = loggerContext.getLogger(Logger.ROOT_LOGGER_NAME);
// 移除所有Appender
rootLogger.detachAndStopAllAppenders();
// RollingFileAppender
PatternLayoutEncoder fileEncoder = new PatternLayoutEncoder();
fileEncoder.setContext(loggerContext);
fileEncoder.setPattern("[%date{ISO8601}] [%level] %logger{80} [%thread] [%X{TRACE_ID}] ${app} - %msg%n");
fileEncoder.start();
RollingFileAppender fileAppender = new RollingFileAppender<>();
fileAppender.setContext(loggerContext);
fileAppender.setName("FILE");
fileAppender.setFile("/data/log-center/api-gateway/server.log");
fileAppender.setAppend(true);
fileAppender.setEncoder(fileEncoder);
ThresholdFilter fileFilter = new ThresholdFilter();
fileFilter.setLevel("INFO");
fileAppender.addFilter(fileFilter);
TimeBasedRollingPolicy rollingPolicy = new TimeBasedRollingPolicy<>();
rollingPolicy.setParent(fileAppender);
rollingPolicy.setContext(loggerContext);
rollingPolicy.setFileNamePattern("/data/log-center/api-gateway/server.%d{yyyy-MM-dd}.log.gz");
rollingPolicy.setMaxHistory(14);
rollingPolicy.start();
fileAppender.setRollingPolicy(rollingPolicy);
fileAppender.start();
AsyncAppender asyncAppender = new AsyncAppender();
asyncAppender.setName("ASYNC_FILE");
asyncAppender.setContext(loggerContext);
asyncAppender.setDiscardingThreshold(0);
asyncAppender.setQueueSize(1024);
asyncAppender.addAppender(fileAppender);
asyncAppender.start();
// ConsoleAppender
PatternLayoutEncoder consoleEncoder = new PatternLayoutEncoder();
consoleEncoder.setContext(loggerContext);
consoleEncoder.setPattern("[%date{ISO8601}] [%level] %logger{80} [%thread] [%X{TRACE_ID}] - %msg%n");
consoleEncoder.start();
ConsoleAppender consoleAppender = new ConsoleAppender<>();
consoleAppender.setContext(loggerContext);
consoleAppender.setEncoder(consoleEncoder);
ThresholdFilter consoleFilter = new ThresholdFilter();
consoleFilter.setLevel("DEBUG");
consoleAppender.addFilter(consoleFilter);
consoleAppender.start();
rootLogger.setLevel(Level.DEBUG);
rootLogger.setAdditive(false);
rootLogger.addAppender(consoleAppender);
rootLogger.addAppender(asyncAppender);
org.slf4j.Logger logger = LoggerFactory.getLogger(LogbackDemo1.class);
logger.debug("debug nano => {}", System.nanoTime());
logger.info("info nano => {}", System.nanoTime());
logger.warn("warn nano => {}", System.nanoTime());
logger.error("error nano => {}", System.nanoTime());
}
}
最佳实践
实践中建议使用logback
文档中提到的最常用的:RollingFileAppender
+ TimeBasedRollingPolicy
+ ConsoleAppender
(这个是为了方便本地开发调试)组合。一般来说,日志文件最终会通过Filebeat
等日志收集组件上传到ELK
体系,在合理定义日志的输出格式(例如在输出格式中指定Level
参数)前提下,其实可以不拆分不同级别的日志文件并且输出所有INFO
或者以上级别的日志,最终在Kibana
中也可以轻易通过参数level: ${LEVEL}
进行不同级别的日志查询。而对于性能要求比较高的服务例如API
网关,建议把RollingFileAppender
关联到AsyncAppender
实例中,在内存足够的前提下调大queueSize
参数并且设置discardingThreshold = 0
(队列满了不丢弃日志事件,有可能会阻塞调用线程,无法忍受可以自行扩展异步日志功能)。在服务器磁盘充足的前提下,一般对于归档日志的文件大小不设置上限,只设置最大归档文件数量,建议数量为14 ~ 30
(也就是2
周到1
个月之间)。下面是一个模板:
/data/log-center/${app}/${filename}.log
/data/log-center/${app}/${filename}.%d{yyyy-MM-dd}.log
14
[%date{ISO8601}] [%level] %logger{80} [%thread] [%X{TRACE_ID}] ${app} - %msg%n
INFO
[%date{ISO8601}] [%level] %logger{80} [%thread] [%X{TRACE_ID}] - %msg%n
DEBUG
1024
0
小结
这篇文章仅仅介绍logback
最新版本的一些基本配置和实践经验,也作为一篇日后随时可以拿起使用的流水账笔记存档。
参考资料:
- The logback manual
(本文完 c-2-d e-a-20220212 这一两个月的基金有点可怕)