一、Slf4j 详解
工厂函数
要使用Slf4j,需要先创建一个org.slf4j.Logger实例,可以使用它的工厂函数org.slf4j.LoggerFactory.getLogger(),参数可以是字符串或Class:
- 如果是字符串,这个字符串会作为返回Logger实例的名字;
- 如果是Class,会调用它的getName()获取Class的全路径,作为Logger实例的名字;
public class ExampleService {
// 传 Class,一般都是传当前的 Class
private static final Logger log = LoggerFactory.getLogger(ExampleService.class);
// 上边那一行相当于:
private static final Logger log = LoggerFactory.getLogger("com.example.service.ExampleService");
// 你也可以指定任意字符串
private static final Logger log = LoggerFactory.getLogger("service");
}
这个字符串格式的「实例名字」可以称之为LoggerName,用于在日志实现层区分如何打印日志(见下一篇【3.1 Conversion Word】节)
Lombok
Lombok也提供了针对各种日志系统的支持,比如你只需要@lombok.extern.slf4j.Slf4j注解就可以得到一个静态的log字段,不用再手动调用工厂函数。默认的LoggerName 即是被注解的Class;同时也支持字符串格式的topic字段指定LoggerName。
@Slf4j
public class ExampleService {
// 注解 @Slf4j 会帮你生成下边这行代码
// private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(ExampleService.class);
}
@Slf4j(topic = "service")
public class ExampleService {
// 注解 @Slf4j(topic = "service") 会帮你自动生成下边这行代码
// private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger("service");
}
除了Slf4j,Lombok几乎支持目前市面上所有的日志方案,从接口到实现都没放过。具体明细可以参考Lombok的官方文档@Log (and friends)[1]。
日志级别
通过org.slf4j.event.Level我们可以看到一共有五个等级,按优先级从低到高依次为:
-
TRACE:一般用于记录调用链路,比如方法进入时打印xxx start;
-
DEBUG:个人觉得它和 trace 等级可以合并,如果一定要区分,可以用来打印方法的出入参;
-
INFO:默认级别,一般用于记录代码执行时的关键信息;
-
WARN:当代码执行遇到预期外场景,但它不影响后续执行时,可以使用;
-
ERROR:出现异常,以及代码无法兜底时使用;
多说一句,Logback额外还有两个级别ALL/OFF表示完全开启/关闭日志输出,我们记日志时并不涉及。
日志的实现层会决定哪个等级的日志可以输出,这也是我们打日志时需要区分等级的原因,在保证重要的日志不丢失的同时,仅在有需要时才打印用于Debug的日志。
@Slf4j
public class ExampleService {
@Resource
private RpcService rpcService;
public String querySomething(String request) {
// 使用 trace 标识这个方法调用情况
log.trace("querySomething start");
// 使用 debug 记录出入参
log.debug("querySomething request={}", request);
String response = null;
try {
RpcResult rpcResult = rpcService.call(a);
if (rpcResult.isSuccess()) {
response = rpcResult.getData();
// 使用 info 标识重要节点
log.info("querySomething rpcService.call succeed, request={}, rpcResult={}", request, rpcResult);
} else {
// 使用 warn 标识程序调用有预期外错误,但这个错误在可控范围内
log.warn("querySomething rpcService.call failed, request={}, rpcResult={}", request, rpcResult);
}
} catch (Exception e) {
// 使用 error 记录程序的异常信息
log.error("querySomething rpcService.call abnormal, request={}, exception={}", request, e.getMessage(), e);
}
// 使用 debug 记录出入参
log.debug("querySomething response={}", response);
// 使用 trace 标识这个方法调用情况
log.trace("querySomething end");
return response;
}
}
打印接口
通过org.slf4j.Logger我们可以看到有非常多的日志打印接口,不过定义的格式都类似,以info为例,一共有两大类:
public boolean info(…);
public boolean isInfoEnabled(…);
info 方法
这个方法有大量的重载,不过使用逻辑是一致的,为了便于说明,我们直接上图:
isInfoEnabled 方法
通过isInfoEnabled方法可以获取当前Logger实例是否开启了对应的日志级别,比如我们可能见过类似这样的代码:
if (log.isInfoEnabled()) {
log.info(...)
}
但其实日志实现层本身就会判断当前Logger实例的输出等级,低于此等级的日志并不会输出,所以一般并不太需要这样的判断。但如果你的输出需要额外消耗资源,那么先判断一下会比较好,比如:
if (log.isInfoEnabled()) {
// 有远程调用
String resource = rpcService.call();
log.info("resource={}", resource)
// 要解析大对象
Object result = ....; // 一个大对象
log.info("result={}", JSON.toJSONString(result));
}
Marker
在前边介绍接口时,我们只提到了log.info()中填字符串模板及参数的情况,细心的朋友应该发现,还有一些接口多了一个org.slf4j.Marker类型的入参,比如:
log.info(Marker, …)
我们可以通过工厂函数创建 Marker 并使用,比如:
Marker marker = MarkerFactory.getMarker("foobar");
log.info(marker, "test a={}", 1);
这个 Marker 是一个标记,它会传递给日志实现层,由实现层决定 Marker 的处理方式,比如:
- 将Marker通过%marker打印出来;
- 使用MarkerFilter[3]过滤出(或过滤掉)带有某个Marker的日志,比如把需要Sunfire监控的日志都过滤出来写到一个单独的日志文件;
MDC
MDC的全称是Mapped Diagnostic Context,直译为映射调试上下文,说人话就是用来存储扩展字段的地方,而且它是线程安全的。比如OpenTelemetry[4]的traceId就会被存到MDC中(见下一篇【五、MDC 中的 traceId】节)。
而且MDC的使用也很简单,就像是一个Map<String, String>实例,常用的方法put/get/remove/clear都有,又到了举粟子🌰时间
// 和 Map<String, String> 相似的接口定义
MDC.put("key", "value");
String value = MDC.get("key");
MDC.remove("key");
MDC.clear();
// 获取 MDC 中的所有内容
Map<String, String> context = MDC.getCopyOfContextMap();
Fluent API (链式调用)
Fluent API也可以直译为「流式 API」, Slf4j从2.0.x开始支持[5],它很像Lombok中@Builder提供的能力,即通过链式调用分别设置各个属性,最后再调用.log()(就像调用.build()那样)完成整个调用。
Marker marker = MarkerFactory.getMarker("foobar");
Exception e = new RuntimeException();
// == 以下几个示例的最终效果是完全一致的 ==
// 这是传统的调用方式
log.info(market, "request a={}, b={}", 1, 2, e);
// Fluent API 例1
log.atInfo() // 表示这是 INFO 级别。你猜对了,还有 atTrace/atDebug/atWarn/atError
.addMarker(marker)
.log("request a={}, b={}", 1, 2, e); // 与传统 API 很像
// Fluent API 例2
log.atInfo()
.addMarker(marker)
.setCause(e)
.setMessage("request a={}, b={}") // 传字符串模板
.setMessage(() -> "request a={}, b={}") // setMessage 支持传入 Supplier
.addArgument(1) // 添加与字符串模板中占们符所对应的值
.addArgument(() -> 2) // addArgument 支持传入 Supplier
.log(); // 大火收汁
// == addKeyValue 的输出格式依赖日志实现层的配置,默认格式与上边示例不同 ==
// Fluent API 例3
log.atInfo()
.setMessage("request") // 注意这里没有占位符
.setKeyValue("a", 1) // 通过 setKeyValue 添加关心的变量
.setKeyValue("b", () -> 2) // value 支持传入 Supplier
.log();
// 通过 setKeyValue 设置的值默认会放在 message 前边,比如上边这个例子,默认会输出:
// a=1 b=2 request
总结一下:
- 所有add前缀的方法,都支持设置多个,比如- -addMarker/addArgument/addKeyValue。所以在Fluent API中是支持给一条日志添加多个Marker的,而传统API不可以。
- 所有set前缀的方法,对应的值都只有一个,比如setMessage/setCause,虽然你可以多次调用,但只有最后一次会生效。
在上边的示例中传统API看起来更简洁。但如果日志中占位符很多,那用Fluent API,特别是使用其中的addKeyValue就很有优势。
顺便说一下,相比Slf4j,更晚推出的Log4j 2在传统API中也支持通过传入Supplier惰性求值,就像这样:
log.info("request a={}", () -> a);
二、Logback
2.1 一、配置入口
Logback支持XML、Groovy的配置方式,以XML来说,它会默认查找resources目录下的logback-test.xml(用于测试)/logback.xml文件。
而如果你使用的Spring Boot,那么你还可以使用logback-spring.xml文件进行配置。这两者的区别是:
- logback-spring.xml是由 Spring Boot 找到,插入自己的上下文信息[1]并做进一步处理后再传递给Logback的,你可以在其中使用区分环境配置,也可以使用拿到Spring上下文信息(比如spring.application.name)。
- logback.xml是由Logback自己找到的,自然不会有Spring Boot相关的能力。
2.2 配置文件介绍
下来我们以logback-spring.xml为例进行介绍。一个Logback配置文件主要有以下几个标签:
-
confinuration:最外层的父标签,其中有几个属性配置,但项目中较少使用,就不啰嗦了;
-
property:定义变量;
-
appender:负责日志输出(一般是写到文件),我们可以通过它设置输出方案;
-
logger:用来设置某个LoggerName的打印级别;
-
root:logger的兜底配置,从而我们不必配置每个LoggerName;
-
conversionRule:定义转换规则,参考【四、Java API】;
2.2.1 springProperty 和 property
前文提到<springProperty>用来插入Spring上下文,那就是 Logback 自己定义变量的标签。直接看示例:
<springProperty scope="context" name="APP_NAME" source="spring.application.name"/>
<property name="LOG_PATH" value="${user.home}/${APP_NAME}/logs"/>
<property name="APP_LOG_FILE" value="${LOG_PATH}/application.log"/>
<property name="APP_LOG_PATTERN"
value="%date{yyyy-MM-dd HH:mm:ss.SSS}|%-5level|%X{trace_id}|%thread|%logger{20}|%message%n%exception"/>
我们首先用<springProperty>插入APP_NAME这个变量来表示应用名,随后用它拼出LOG_PATH变量。示例中还用到了${user.home}这个Logback内建支持的上下文变量[2]。APP_LOG_FILE是log文件路径;APP_LOG_PATTERN是日志格式(请参考【三、占位符】节)。
2.2.2 appender
这一节涉及到的知识点很多,但一码胜千言,先直接给出示例:
<appender name="APPLICATION" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${APP_LOG_FILE}</file>
<encoder>
<pattern>${APP_LOG_PATTERN}</pattern>
</encoder>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>${APP_LOG_FILE}.%d{yyyy-MM-dd}.%i</fileNamePattern>
<maxHistory>30</maxHistory>
<maxFileSize>200MB</maxFileSize>
<totalSizeCap>10GB</totalSizeCap>
</rollingPolicy>
</appender>
<appender name="APPLICATION-async" class="ch.qos.logback.classic.AsyncAppender">
<queueSize>256</queueSize>
<discardingThreshold>0</discardingThreshold>
<neverBlock>true</neverBlock>
<appender-ref ref="APPLICATION"/>
</appender>
示例中涉及的变量在上一节已经提到,这里不啰嗦了。需要关注的是以下几个点:
-
ch.qos.logback.core.rolling.RollingFileAppender负责将日志滚动打印,避免单文件体积过大。具体的滚动策略都在中指定,各配置项都还算好理解。
-
ch.qos.logback.classic.AsyncAppender负责将日志异步打印,避免大量打印日志时阻塞线程。
除了上边这两个,还有如ch.qos.logback.core.ConsoleAppender是用来将日志输出到控制台,如果你用到了,建议参考【下一篇九、不要将日志输出至Console】节。
更多Appender和RollingPolicy相关的介绍,可以参考官方文档Chapter 4: Appenders[3]。
2.2.4 logger和root
<logger> :用来设置某个LoggerName的打印级别。比如:
<logger level="INFO" additivity="false" name="com.foo.bar">
<appender-ref ref="APPLICATION-async"/>
</logger>
<root level="INFO">
<appender-ref ref="APPLICATION-async"/>
</root>
<logger>:某模块路径的打印级别配置
<logger name="com.javastarboy.worknotice" level="info" />
上面的配置指定所有LoggerName为com.foo.bar的日志以INFO级别进行打印(TRACE和DEBUG级别将不会输出),此配置绑定的输出器(appender)为APPLICATION-async。
其中LoggerName会以 . 为分隔符逐级向上匹配,比如实际LoggerName为com.foo.bar.service.ExampleService,那么它的查找过程依次为:
com.foo.bar.service.ExampleService
com.foo.bar.service
com.foo.bar(此时命中了我们示例中的<logger>,另外因为配置了additivity=“false” 所以停止继续向下查找)
com.foo
com
<root>
而 <root>就是兜底配置了,当LoggerName没匹配到任何一项 时,就会使用 <root>,所以它是没有additivity和name属性的。
一般实际业务场景中,所有<logger>都建议加上additivity=“false” ,否则日志就会因查找到多个<logger>(或<root>)而打印多份。
2.2.5 日志异步打印
ch.qos.logback.classic.AsyncAppender是logback的一个异步打印实现类,指明使用该类打印日志即开启了异步打印功能。
异步打印原理
https://blog.csdn.net/qq496013218/article/details/76603779
当Logging Event进入AsyncAppender后,AsyncAppender会调用appender方法,append方法中在将event填入Buffer(这里选用的数据结构为BlockingQueue)中前,会先判断当前buffer的容量以及丢弃日志特性是否开启,当消费能力不如生产能力时,AsyncAppender会超出Buffer容量的Logging Event的级别,进行丢弃,作为消费速度一旦跟不上生产速度,中转buffer的溢出处理的一种方案。AsyncAppender有个线程类Worker,它是一个简单的线程类,是AsyncAppender的后台线程,所要做的工作是:从buffer中取出event交给对应的appender进行后面的日志推送。
从上面的描述中可以看出,AsyncAppender并不处理日志,只是将日志缓冲到一个BlockingQueue里面去,并在内部创建一个工作线程从队列头部获取日志,之后将获取的日志循环记录到附加的其他appender上去,从而达到不阻塞主线程的效果。因此AsynAppender仅仅充当事件转发器,必须引用另一个appender来做事。
在使用AsyncAppender的时候,有些选项还是要注意的。由于使用了BlockingQueue来缓存日志,因此就会出现队列满的情况。正如上面原理中所说的,在这种情况下,AsyncAppender会做出一些处理:默认情况下,如果队列80%已满,AsyncAppender将丢弃TRACE、DEBUG和INFO级别的event,从这点就可以看出,该策略有一个惊人的对event丢失的代价性能的影响。另外其他的一些选项信息,也会对性能产生影响
默认情况下,event queue配置最大容量为256个events。如果队列被填满,应用程序线程被阻止记录新的events,直到工作线程有机会来转发一个或多个events。因此队列深度需要根据业务场景进行相应的测试,做出相应的更改,以达到较好的性能。
<appender name="FILE" class= "ch.qos.logback.core.rolling.RollingFileAppender">
<!-- 按天来回滚,如果需要按小时来回滚,则设置为{yyyy-MM-dd_HH} -->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>/opt/log/test.%d{yyyy-MM-dd}.log</fileNamePattern>
<!-- 如果按天来回滚,则最大保存时间为1天,1天之前的都将被清理掉 -->
<maxHistory>30</maxHistory>
<!-- 日志输出格式 -->
<layout class="ch.qos.logback.classic.PatternLayout">
<Pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} -%msg%n</Pattern>
</layout>
</appender>
<!-- 异步输出 -->
<appender name ="ASYNC" class= "ch.qos.logback.classic.AsyncAppender">
<!-- 不丢失日志.默认的,如果队列的80%已满,则会丢弃TRACT、DEBUG、INFO级别的日志 -->
<discardingThreshold >0</discardingThreshold>
<!-- 更改默认的队列的深度,该值会影响性能.默认值为256 -->
<queueSize>512</queueSize>
<!-- 添加附加的appender,最多只能添加一个 -->
<appender-ref ref ="FILE"/>
</appender>
<root level ="trace">
<appender-ref ref ="ASYNC"/>
</root>
示例二
<appender name="rollingFile" class="ch.qos.logback.core.rolling.RollingFileAppender">
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>logs/${application.name}.%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>30</maxHistory>
</rollingPolicy>
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] [traceId=%X{traceId} spanId=%X{spanId}] %-5level %logger{36} - %msg%n
</pattern>
</encoder>
</appender>
<appender name="AsyncAppender" class="ch.qos.logback.classic.AsyncAppender">
<!-- 不丢失日志.默认的,如果队列的80%已满,则会丢弃TRACT、DEBUG、INFO级别的日志 -->
<discardingThreshold >0</discardingThreshold>
<!-- 更改默认的队列的深度,该值会影响性能.默认值为256 -->
<queueSize>512</queueSize>
<!-- 添加附加的appender,最多只能添加一个 -->
<appender-ref ref ="rollingFile"/>
</appender>
2.2.5 springProfile
Spring还提供了<springProfile>标签,用来根据Spring Profiles[4](即 spring.profiles.active的值)动态调整日志信息,比如我们希望线上环境使用INFO级别,而预发、日常使用TRACE级别:
<springProfile name="production">
<root level="INFO">
<appender-ref ref="APPLICATION-async"/>
</root>
</springProfile>
<springProfile name="staging,testing">
<root level="TRACE">
<appender-ref ref="APPLICATION-async"/>
</root>
</springProfile>
三、占位符
3.1 Conversion Word
Logback提供了大量有用的占位符给大家使用,官方文档在Conversion Word[5]。
比如一些常用的占位符(大部分占位符都有缩写形式,比如%logger可以简写为%c,我不在这里一一列举了,具体可以查看上边给出的官方文档):
占位符 | 说明 |
---|---|
%logger | 输出LoggerName(参考【第三篇:1.1 工厂函数】节) |
%message | 输出你实际要打印的日志信息(参考【第三篇:3.1 info方法】节) |
%exception | 输出异常堆栈,对应通过Slf4j传入的异常(参考【第三篇:3.1 info方法】节) |
%level | 输出日志级别,即TRACE/DEBUG/INFO/WARN/ERROR/FATAL。注意这里有别于Slf4j(参考【第三篇:二、日志级别】),多了个FATAL级别,这是为了适配Log4j而存在的。 |
%xException | 输出异常堆栈,同时包含每行堆栈所归属的JAR包名 |
%marker | 输出通过Marker(参考【第三篇:四、Marker】节)传入的字符串 |
%mdc | 输出MDC(参考【第三篇:五、MDC】节)中对应的值 |
%kvp | 输出通过addKeyValue(参考【第三篇:六、Fluent API (链式调用)】节)传入的KV对 |
%date | 输出时间,可以添加符合ISO 8601[6]的参数指定输出格式,比如我们常用的yyyy-MM-dd HH:mm:ss |
%thread | 输出打印日志方法所在的线程名 |
%n | 输出一个换行符 |
%nopex | 忽略传入的堆栈,不打印。请参考【第五篇:七、将堆栈合并为一行】节Logback会判断你的日志pattern,如果没有输出堆栈,会默认追加%exception,以保证传入的异常信息不会丢。这个占位符就是明确要求不做追加。 |
另外,针对%logger额外做一下补充。%logger的参数中可以传一个正整数,用于指定输出长度,当LoggerName长度超过限制时,Logback会以 . 为分隔智能缩短。假设LoggerName是com.example.foo.bar.ExampleService(这个字符串长度为34),那么:
配置 | 输出结果 | 说明 |
---|---|---|
[%logger] | [com.example.foo.bar.ExampleService] | 原样输出 |
[%logger{32}] | [c.example.foo.bar.ExampleService] | 实际长度32,与限制值一致 |
[%logger{30}] | [c.e.foo.bar.ExampleService] | 实际长度26,比限制值小。因为每一级 package要么保留原样,要么只取第一个字符 |
[%logger{10}] | [c.e.f.b.ExampleService] | 实际长度20,比限制值大。因为每一级 package至少会保留一个字符,且最后一级不会被缩短 |
[%logger{0}] | [ExampleService] | 0比较特殊,表示只保留最后一级,且不会被缩短 |
3.2 Format modifiers
从前边我们可以看到,占位符的基本使用方式是:%占位符{参数},但其实还有一个用于控制格式的可选配置,可以放在%与占位符之间,叫作Format modifiers[7]。一个完整的格式配置包含五个部分,比如:-10.-20,我们分别解释:
-
-:第一个-表示不足最小长度时在右侧填充空格,即输出内容左对齐(默认是在左侧填充空格,即右对齐);
-
10:第一个数字表示输出最小长度,不足的补空格;
-
.:与后边两项配置的分隔符,本身没有含义;
-
-:第二个-表示超过最大长度时先裁剪右侧,即保留左侧字符(默认先裁剪左侧,即保留右侧字符);
-
20:第二个数字表示输出最大长度,超出部分会做裁剪;
举个几个例子:
配置 | 文本 | 输出结果 | 说明 |
---|---|---|---|
[%5level] | INFO | [ INFO] | 最小5个字符,右对齐 |
[%-5level] | INFO | [INFO ] | 最小5个字符,左对齐 |
[%.-1level] | INFO | [I] | 最大1个字符,优先保留左侧字符 |
[%-5,-10logger] | com.foo.bar.Service | [com.foo.ba] | 最大10个字符,优先保留左侧字符 |
[%-5,10logger] | com.foo.bar.Service | [ar.Service] | 最大10个字符,优先保留右侧字符 |
四、日志示例
4.1 日志配置实例
<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="2 seconds">
<timestamp key="TIMESTAMP" datePattern="yyyy-MM-dd"/>
<springProperty scope="context" name="application.name" source="spring.application.name" defaultValue="xxxx-xxxx"/>
<property name="LOGPATH" value="log"/>
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] [traceId=%X{traceId} spanId=%X{spanId}] %-5level %logger{36} - %msg%n
</pattern>
</encoder>
</appender>
<appender name="rollingFile" class="ch.qos.logback.core.rolling.RollingFileAppender">
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>logs/${application.name}.%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>30</maxHistory>
</rollingPolicy>
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] [traceId=%X{traceId} spanId=%X{spanId}] %-5level %logger{36} - %msg%n
</pattern>
</encoder>
</appender>
<appender name="errorFile" class="ch.qos.logback.core.rolling.RollingFileAppender">
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>ERROR</level>
</filter>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>logs/${application.name}_error.%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>30</maxHistory>
</rollingPolicy>
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] [traceId=%X{traceId} spanId=%X{spanId}] %-5level %logger{36} - %msg%n
</pattern>
</encoder>
</appender>
<!-- project default level -->
<logger name="avicit" level="info"/>
<root level="info">
<appender-ref ref="console"/>
<appender-ref ref="rollingFile"/>
<appender-ref ref="errorFile"/>
</root>
</configuration>
4.2 logback 常用配置详解 <appender>
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%-4relative [%thread] %-5level %logger{35} - %msg %n</pattern>
</encoder>
</appender>
<root level="DEBUG">
<appender-ref ref="STDOUT" />
</root>
</configuration>
4.2.1 FileAppender: 把日志添加到文件
有以下子节点:
<file>:被写入的文件名,可以是相对目录,也可以是绝对目录,如果上级目录不存在会自动创建,没有默认值。
<append>:如果是 true,日志被追加到文件结尾,如果是 false,清空现存文件,默认是true。
<encoder>:对记录事件进行格式化。(具体参数稍后讲解 )
<prudent>:如果是 true,日志会被安全的写入文件,即使其他的FileAppender也在向此文件做写入操作,效率低,默认是 false。
例如:
<configuration>
<appender name="FILE" class="ch.qos.logback.core.FileAppender">
<file>testFile.log</file>
<append>true</append>
<encoder>
<pattern>%-4relative [%thread] %-5level %logger{35} - %msg%n</pattern>
</encoder>
</appender>
<root level="DEBUG">
<appender-ref ref="FILE" />
</root>
</configuration>
4.2.2 .RollingFileAppender
滚动记录文件,先将日志记录到指定文件,当符合某个条件时,将日志记录到其他文件。有以下子节点:
<file>:被写入的文件名,可以是相对目录,也可以是绝对目录,如果上级目录不存在会自动创建,没有默认值。
<append>:如果是 true,日志被追加到文件结尾,如果是 false,清空现存文件,默认是true。
<encoder>:对记录事件进行格式化。(具体参数稍后讲解 )
<rollingPolicy>:当发生滚动时,决定 RollingFileAppender 的行为,涉及文件移动和重命名。
<triggeringPolicy >: 告知 RollingFileAppender 合适激活滚动。
<prudent>:当为true时,不支持FixedWindowRollingPolicy。支持TimeBasedRollingPolicy,但是有两个限制,1不支持也不允许文件压缩,2不能设置file属性,必须留空。
<appender name="rollingFile" class="ch.qos.logback.core.rolling.RollingFileAppender">
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>logs/${application.name}.%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>30</maxHistory>
</rollingPolicy>
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] [traceId=%X{traceId} spanId=%X{spanId}] %-5level %logger{36} - %msg%n
</pattern>
</encoder>
</appender>
4.2.2.1 rollingPolicy 日志记录器的滚动策略
TimeBasedRollingPolicy 最常用的滚动策略
最常用的滚动策略,它根据时间来制定滚动策略,既负责滚动也负责出发滚动。有以下子节点:
-
<fileNamePattern>:
必要节点,包含文件名及“%d”转换符, “%d”可以包含一个java.text.SimpleDateFormat指定的时间格式,如:%d{yyyy-MM}。如果直接使用 %d,默认格式是 yyyy-MM-dd。RollingFileAppender 的file字节点可有可无,通过设置file,可以为活动文件和归档文件指定不同位置,当前日志总是记录到file指定的文件(活动文件),活动文件的名字不会改变;如果没设置file,活动文件的名字会根据fileNamePattern 的值,每隔一段时间改变一次。“/”或者“\”会被当做目录分隔符。 -
<maxHistory>:
可选节点,控制保留的归档文件的最大数量,超出数量就删除旧文件。假设设置每个月滚动,且是6,则只保存最近6个月的文件,删除之前的旧文件。注意,删除旧文件是,那些为了归档而创建的目录也会被删除。
例如:每天生成一个日志文件,保存30天的日志文件。
<configuration>
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>logFile.%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>30</maxHistory>
</rollingPolicy>
<encoder>
<pattern>%-4relative [%thread] %-5level %logger{35} - %msg%n</pattern>
</encoder>
</appender>
<root level="DEBUG">
<appender-ref ref="FILE" />
</root>
</configuration>
MaxHistory 属性可以按“文件数量、小时、天、月、年”等策略实现文件保留。但是很多人都遇到过此配置不生效问题,网上都只介绍了 cleanHistoryOnStart ,而我是另一种情况,这里汇总一下。
<!-- 日志级别从低到高分为TRACE < DEBUG < INFO < WARN < ERROR < FATAL,如果设置为WARN,则低于WARN的信息都不会输出 -->
<contextName>logback</contextName>
<!-- name的值是变量的名称,value的值时变量定义的值。通过定义的值会被插入到logger上下文中。定义变量后,可以使“${}”来使用变量。 -->
<property name="log.path" value="../logs/worknotice"/>
<!-- 时间滚动输出 level为 INFO 日志 -->
<appender name="INFO_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- 正在记录的日志文件的路径及文件名 -->
<file>${log.path}/log_info.log</file>
<!--日志文件输出格式-->
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
<charset>UTF-8</charset>
</encoder>
<!-- 日志记录器的滚动策略,按日期,按大小记录 -->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- 每天日志归档路径以及格式,编码器 -->
<fileNamePattern>${log.path}/info/log-info-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<!--每个文件最多100MB-->
<maxFileSize>100MB</maxFileSize>
<!--日志文件保留天数-->
<maxHistory>15</maxHistory>
<!--每个文件最多100MB,保留15天的历史记录,但最多20GB-->
<totalSizeCap>20GB</totalSizeCap>
<!--重启清理日志文件-->
<cleanHistoryOnStart>true</cleanHistoryOnStart>
</rollingPolicy>
<!-- 此日志文件只记录info级别的 -->
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>info</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
</appender>
参考:
https://blog.csdn.net/u012723183/article/details/107685109
FixedWindowRollingPolicy: 根据固定窗口算法重命名文件的滚动策略
有以下子节点:
-
<minIndex>:窗口索引最小值
-
<maxIndex>:窗口索引最大值,当用户指定的窗口过大时,会自动将窗口设置为12。
-
<fileNamePattern >:
必须包含“%i”例如,假设最小值和最大值分别为1和2,命名模式为 mylog%i.log,会产生归档文件mylog1.log和mylog2.log。还可以指定文件压缩选项,例如,mylog%i.log.gz 或者 没有log%i.log.zip
4.2.2.2 triggeringPolicy
SizeBasedTriggeringPolicy
**: 查看当前活动文件的大小,如果超过指定大小会告知RollingFileAppender 触发当前活动文件滚动。只有一个节点:
-
<maxFileSize>:这是活动文件的大小,默认值是10MB。
例如:按照固定窗口模式生成日志文件,当文件大于20MB时,生成新的日志文件。窗口大小是1到3,当保存了3个归档文件后,将覆盖最早的日志。
<configuration>
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>test.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.FixedWindowRollingPolicy">
<fileNamePattern>tests.%i.log.zip</fileNamePattern>
<minIndex>1</minIndex>
<maxIndex>3</maxIndex>
</rollingPolicy>
<triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
<maxFileSize>5MB</maxFileSize>
</triggeringPolicy>
<encoder>
<pattern>%-4relative [%thread] %-5level %logger{35} - %msg%n</pattern>
</encoder>
</appender>
<root level="DEBUG">
<appender-ref ref="FILE" />
</root>
</configuration>
另外还有SocketAppender、SMTPAppender、DBAppender、SyslogAppender、SiftingAppender,并不常用,这些就不在这里讲解了,大家可以参考官方文档。当然大家可以编写自己的Appender。
4.2.3 filter:
过滤此日志文件只记录xx级别的日志
<appender name="errorFile" class="ch.qos.logback.core.rolling.RollingFileAppender">
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>ERROR</level>
</filter>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>logs/${application.name}_error.%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>30</maxHistory>
</rollingPolicy>
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] [traceId=%X{traceId} spanId=%X{spanId}] %-5level %logger{36} - %msg%n
</pattern>
</encoder>
</appender>
4.2.3 <encoder>:
负责两件事,一是把日志信息转换成字节数组,二是把字节数组写入到输出流。
目前PatternLayoutEncoder 是唯一有用的且默认的encoder ,有一个<pattern>节点,用来设置日志的输入格式。使用“%”加“转换符”方式,如果要输出“%”,则必须用“\”对“%”进行转义。
<encoder>
<pattern>%-4relative [%thread] %-5level %logger{35} - %msg%n</pattern>
</encoder
<pattern>里面的转换符说明:
格式修饰符,与转换符共同使用:
可选的格式修饰符位于“%”和转换符之间。
第一个可选修饰符是左对齐 标志,符号是减号“-”;接着是可选的最小宽度 修饰符,用十进制数表示。如果字符小于最小宽度,则左填充或右填充,默认是左填充(即右对齐),填充符为空格。如果字符大于最小宽度,字符永远不会被截断。最大宽度 修饰符,符号是点号"."后面加十进制数。如果字符大于最大宽度,则从前面截断。点符号“.”后面加减号“-”在加数字,表示从尾部截断。
例如:%-4relative 表示,将输出从程序启动到创建日志记录的时间 进行左对齐 且最小宽度为4。
五、Java API
除了使用XML配置文件外,Logback还提供了大量的Java API[8]以支持更复杂的业务诉求。我们通过三个非常实用的场景来简单介绍一下。
- 场景一:使用log.info(“obj={}”, obj) 时,如何将obj统一转JSON String后输出;
- 场景二:日志中涉及到的手机号、身份证号,如何脱敏后再记录日志;
- 场景三:Logback配置基于XML,如何不改代码不发布,也可以动态修改日志级别;
其中前两个问题都可以通过MessageConverter[9]实现,因为篇幅原因,具体介绍可关注后续文章。
第三个问题可以借助LoggerContext[10]及Logger[11],同样因为篇幅原因,具体介绍可关注后续文章。
六、MDC 中的 traceId
单独拿出一节讲这个,是因为我发现有很多同学会手动记录traceId,比如:
log.info("traceId={}, blah blah blah", Span.current().getSpanContext().getTraceId());
其实OpenTelemetry[12]已经自动将traceId加到了MDC,对应的Key是trace_id,使用%mdc{trace_id}(参考【第三篇五、MDC】节)即可打印出traceId。比如我们在【2.1 springProperty和property】一节的示例中,就使用了这个Key。
七、后记
以上只是简单介绍了Logback的常用功能,如需进一步了解可以参考官方文档Logback documentation[13]。特别是其中很多例子是结合Slf4j一起介绍的,非常易懂。