LogBack学习笔记

Logback 学习笔记

配置详解

常用配置

1. 定期刷新配置

生产环境在不重启和重新编译的情况下排查问题

1
2
3
4
<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="60 seconds" debug="false">
<!-- 省略一大波 -->
</configuration>

配置说明

  • scan=true:表示开启自动扫描检查 Logback 配置文件
  • scanPeriod=60 seconds:表示扫描周期为 60 秒
  • debug=false:表示关闭 Logback 框架内部调试日志

2. 异步输出

同步输出日志和异步输出日志对系统的性能指标影响是很大的,线上环境一般都是建议使用异步的方式去输出日志信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="60 seconds" debug="false">
<!-- Step 1: 定义同步日志输出器 -->
<appender name="BIZ" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- 此处省略一大波 -->
</appender>

<!-- Step 2: 定义异步日志输出器 -->
<appender name="ASYNC_BIZ" class="ch.qos.logback.classic.AsyncAppender">
<appender-ref ref="BIZ"/>
<queueSize>1000</queueSize>
</appender>

<!-- Step 3: 配置指定包路径或类的日志输出器 -->
<logger name="cn.micro.demo" additivity="false">
<appender-ref ref="ASYNC_BIZ"/>
</logger>
</configuration>

配置说明

  • RollingFileAppender:用于定义日志的同步输出,然后再异步输出器中进行引用
  • AsyncAppender:用于定义异步输出,引用上述同步输出器。默认使用 BlockingQueue 来排队
    • includeCallerData 是否收集调用方数据,默认为false,此时方法行号,方法名等不能显示
    • queueSize 控制阻塞队列大小,默认256
    • discardingThreshold 控制丢弃日志的条数阈值,防止队列满后阻塞,默认是20% , 但是,设置时它不是百分比,而是具体的条数
    • neverBalock 用于控制队列
  • logger:在 标签中直接引用异步输出器即可

注意事项:

  • 记录异步日志撑爆内存

    queueSize设置的特别大

  • 记录异步日志出现日志丢失

    queueSize设置的特别小(默认值是256,比较小),且discardingThreshold设置为大于0的值(或默认值20%),队列剩余容量小于discardingThreshold的配置就会丢弃<=INFO的日志

  • 记录异步日志出现阻塞

    neverBlock默认为false, 意味着总可能出现阻塞。 比如discardingThreshold为0, 队列满时会阻塞,如果不为0 , 也只会丢弃INFO级别的日志,出现大量错误日志时,还时会阻塞

3. 日志“零丢失”

采用异步输出日志文件是为了解决性能问题,但如果想要极致的性能,同样也是有代价的。Logback 框架默认的异步框架,为了解决队列满后依然会同步阻塞的问题,设置了队列大小被占用超过 80% 时,适当的丢弃部分可舍弃的日志。如一般的 INFO、DEBUG 等级别可以舍弃,只需要保证 ERROR 能打印即可,所以大家在使用的时候,可以适当的进行配置后,也是可以允许部分日志丢失的,只要关键的日志不丢失即可。当然,如果接受不了,那就配置为 0 即可,只是性能会有部分损耗而已。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="60 seconds" debug="false">
<!-- Step 1: 定义日志输出器 -->
<appender name="BIZ" class="ch.qos.logback.core.rolling.RollingFileAppender">
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<!-- Step 2: 默认true表示日志立即输出,否则缓冲空间满后,会出现日志丢失 -->
<immediateFlush>true</immediateFlush>
<!-- 此处省略一大波 -->
</encoder>
<!-- 此处省略一大波 -->
</appender>
<!-- Step 3: 异步输出 -->
<appender name="ASYNC_BIZ" class="ch.qos.logback.classic.AsyncAppender">
<appender-ref ref="BIZ"/>
<queueSize>1000</queueSize>
<!-- Step 4: 定义日志丢失率为0,表示日志不丢失,否则异步队列大小超过80%就会丢弃部分日志 -->
<discardingThreshold>0</discardingThreshold>
</appender>
<!-- Step 5: 配置指定包路径或类的日志输出器 -->
<logger name="cn.micro.demo" additivity="false">
<appender-ref ref="ASYNC_BIZ"/>
</logger>
</configuration>

配置说明

  • immediateFlush=true:默认 true 表示日志立即输出,否则缓冲空间满后,会出现日志丢失
  • queueSize:表示日志事件的队列大小,默认为 256。对性能影响较大
  • discardingThreshold:表示日志的丢失条数。默认为队列大小被占用超过 80%,则自动丢弃 TRACE、DEBUGuava 或 INFO 的日志。所以通常设置为 0,表示不丢弃任何日志

源码阅读

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* The default buffer size.
*/
public static final int DEFAULT_QUEUE_SIZE = 256;
int queueSize = DEFAULT_QUEUE_SIZE;

int appenderCount = 0;

static final int UNDEFINED = -1;
int discardingThreshold = UNDEFINED;
boolean neverBlock = false;

... ...

if (discardingThreshold == UNDEFINED)
discardingThreshold = queueSize / 5;

4 MDC 上下文

线上每个系统输出的日志可能都是成篇的,当很多用户在同一段时间内进行各种操作时,我们更希望能单独查看某个用户的单次操作的日志内容,以便于更好的分析出现的问题。因此我们可以在用户进入系统上游的第一时间,就使用 MDC 将请求的唯一表示(甚至是用户唯一表示)put 至日志的上下文中,然后在日志输出的格式中使用 %X{key} 的方式进行打印至每行日志的固定位置,从而实现每个日志都会归属于某个用户的某次操作。接着我们只需要使用用户标识和请求标识进行过滤,即可看到某个用户某一次操作的所有日志。

第一步:在拦截器中进行 put 和 remove 操作

1
2
3
4
5
6
7
8
9
10
11
12
13
@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws IOException, ServletException {
String traceId = request.getHeader("Trace-Id");
try {
MDC.put("Trace-Id", traceId);
filterChain.doFilter(request, response);
} finally {
MDC.remove(X_TRACE_ID);
}
}

第二步:在日志配置文件中进行获取打印

1
2
3
4
5
6
7
8
9
10
11
<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="60 seconds" debug="false">
<!-- Step 1: 定义日志输出器 -->
<appender name="BIZ" class="ch.qos.logback.core.rolling.RollingFileAppender">
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<charset>UTF-8</charset>
<!-- 使用 %X{Trace-Id} 获取MDC上下文中的参数 -->
<pattern>[%d{yyyy-MM-dd HH:mm:ss.SSS}] [%thread] [%-5level] [%X{Trace-Id}] [%logger{36}]: %msg%n</pattern>
</encoder>
</appender>
</configuration>

配置说明

  • %X{Trace-Id}:表示在上下文中获取 key = Trace-Id 的参数值

5 按日期和单文件大小滚动

生产环境,我们希望每天的日志都是独立的文件,方便更好的去查看过去某一天的日志,而不是在单个很庞大的文件中去过滤,因此日志需要按日期滚动。其次,有时候单个文件的日志内容太大,下载至本地后,很难打开,或打开很慢,导致使用起来很不方便,所以我们又希望单个文件的大小不要太大,只要一般的机器能快速打开即可,这个一般我们是建议在 50MB-100MB 之间,太大则依然很慢,很不好排查。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="60 seconds" debug="false">
<!-- Step 1: 定义日志输出器 -->
<appender name="BIZ" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- Step 2: 定义日志滚动触发策略和滚动策略使用 -->
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<!-- Step 3: 使用 %d{yyyy-MM-dd} 实现按日期滚动 -->
<!-- Step 4: 使用 %i 实现按单个文件大小滚动 -->
<fileNamePattern>./logs/bak/biz-%d{yyyy-MM-dd}-%i.log.gz</fileNamePattern>
<!-- Step 5: 使用 <maxFileSize> 指定单个文件大小的最大值 -->
<maxFileSize>80MB</maxFileSize>
</rollingPolicy>
</appender>
</configuration>

配置说明

  • SizeAndTimeBasedRollingPolicy:表示定义日志滚动的触发策略和滚动策略
  • fileNamePattern:表示根据日期和序号进行定义日志文件名称,从而实现滚动策略
  • maxFileSize:表示定义单个日志文件的最大值,超过最大值则触发滚动,上述的序号 i 则自动加 1

6 按保留天数和总容量滚动

上述虽然支持了日志按天滚动和按单个文件大小滚动。但上了生产之后,过一段时间就会发现,线上服务器的硬盘满了,排查问题后发现,是日志文件占了太多导致的,所以需要运维人员去删除,可运维人员也不能一直盯着每台服务,超过就去删除吧?于是我们希望,每个日志文件设置一个最大保留天数,超过则自动删除超过的文件。但这样是不够的,因为有可能天数还没超过,总大小有超过了,所以我们又需要设置监控总的日志大小,总大小超过,也希望去自动删除最老的日志文件。这样就完全可以不需要运维人员一直监控着了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="60 seconds" debug="false">
<!-- Step 1: 定义日志输出器 -->
<appender name="BIZ" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- Step 2: 定义日志滚动触发策略和滚动策略使用 -->
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>./logs/bak/biz-%d{yyyy-MM-dd}-%i.log.gz</fileNamePattern>
<maxFileSize>80MB</maxFileSize>
<!-- Step 3: 使用 maxHistory 定义日志保留的最大天数 -->
<maxHistory>30</maxHistory>
<!-- Step 4: 使用 totalSizeCap 定义日志保留的总容量大小 -->
<totalSizeCap>5GB</totalSizeCap>
</rollingPolicy>
</appender>
</configuration>

配置说明

  • SizeAndTimeBasedRollingPolicy:表示定义日志滚动的触发策略和滚动策略
  • maxHistory:表示日志最多保留的天数,超过则自动删除日志文件
  • totalSizeCap:表示所有日志总文件大小,超过则自动删除最老的日志文件

7 读取 application.yml 配置

在 SpringBoot 项目中,我们通常会从 application.yml 配置文件中读取应用的名称和环境变量等重要参数。则可通过 来获取其配置参数信息,并定义为日志的上下文参数信息,然后可使用 ${key} 方式来获取定义的参数值内容。

1
2
3
4
5
6
7
8
9
10
11
12
<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="60 seconds" debug="false">
<!-- Step 1:通过springProperty标签直接读取application.yml中的配置-->
<springProperty name="app_name" scope="context" source="spring.application.name"/>
<springProperty name="app_env" scope="context" source="spring.profiles.active"/>

<!-- Step 2: 定义日志输出器 -->
<appender name="BIZ" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- Step 3: 使用${app_name}来读取定义的应用名称作为文件名称 -->
<file>./logs/${app_name}-biz.log</file>
</appender>
</configuration>

配置说明

  • <springProperty>:使用 source 来获取配置文件中的参数值,然后定义为日志上下文的参数
  • ${app_name}:表示从日志上下文中获取 key = app_name 的参数值内容

8 分文件记录日志

线上环境,如果把所有日志都输出至一个文件,则可能会出现大篇幅的刷新日志,非常不便于排查问题时定位问题。因此,一般情况线上日志我们都会分文件记录日志内容,如启动类日志文件、业务日志文件、定时任务日志文件和交易日志文件等等。可以根据实际情况需要来分文件打印,但也不要太过细致,合理分类记录即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="60 seconds" debug="false">
<!-- Step 1:定义日志输出器 -->
<!-- 1.1:业务日志 ${app_name}-biz.log -->
<appender name="BIZ" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>./log/${app_name}-biz.log</file>
<!-- 此处省略一大波 -->
</appender>
<!-- 1.2:默认日志 ${app_name}-stdout.log -->
<appender name="STDOUT" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>./log/${app_name}-stdout.log</file>
<!-- 此处省略一大波 -->
</appender>

<!-- Step 2:日志输出 -->
<!-- 2.1:配置指定包路径或类的日志输出器 -->
<logger name="cn.micro.demo" additivity="false">
<appender-ref ref="BIZ"/>
</logger>
<!-- 2.2:默认日志 -->
<root level="INFO">
<appender-ref ref="STDOUT"/>
</root>
</configuration>

###