详解Spring Boot下使用logback 记录多个文件日志

时间:2021-09-28 05:42:34

背景

这两天遇到一个比较有意思的日志问题.

近期对我之前的python代码进行java的重构, 一方面是因为java使用的医疗库非常健全稳定, 可以商用. 另一方面是因为java速度快, 这个库的实现的效率也高, 性能是python版本的好几倍.

但是作为这个项目的唯一作者, 我的癖好也成为这个项目的风格. 这个项目会给很多部署工程师使用. 当然项目的可用性和性能作为第一考虑的因素, 但是作为一个懒人, 对使用软件时候的复杂部署过程和混乱调试信息深恶痛绝. 所以我在项目中使用了高度可配置/易用和多文件日志.

说道多文件日志, 它的优点是每个日志只容纳自身的逻辑, 所以对于一般的入门开发者或者是初级运维工程师查看起来非常方便.

初步尝试

因为spring boot的配置一般来讲是application.properties, 但是同时开发者可以使用yml格式的配置, 二者相比, yml文件更为简洁. 熟读python之禅的我当然是简洁胜于冗余选择了yml.

发现spring-boot可以通过application.yml配置日志. 高兴的配置一番之后发现没法配置多个logger, 弃用! 改用logback-spring.xml(为什么不用logback.xml? 因为-spring这种文件可以获取到spring配置中的变量.下面再说)

第一次实现

我有好几个服务需要打日志. 一般来讲我的日志风格是 *.log 保存 info以上级别日志. *.err.log保存error以上级别日志. 我如果每个文件日志都使用一个appender的话, 配置文件太长了. 而且很难看, 不是我的风格.

google了一下, 发现了这种方案:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
<?xml version="1.0" encoding="utf-8"?>
<configuration>
  # 下面这一行的意思是使用application.yml中的global.log-dir变量
  <springproperty scope="context" name="log_dir" source="global.log-dir" defaultvalue="./log/"/>
  <!-- 追加器开始 -->
  # 这个是一个可以定义变量的appender
  <appender name="sift" class="ch.qos.logback.classic.sift.siftingappender">
    # 使用 loggernamebaseddiscriminator 这个类根据当前logger获取变量
    <discriminator class="com.utils.loggers.loggernamebaseddiscriminator">
      <defaultvalue>general</defaultvalue>
    </discriminator>
    <sift>
      # 根据变量loggername名字生成根据日期滚动的appender
      <appender name="file-${loggername}" class="ch.qos.logback.core.rolling.rollingfileappender">
        <file>
          ${log_dir}/${loggername}.log
        </file>
        <rollingpolicy class="ch.qos.logback.core.rolling.timebasedrollingpolicy">
          <filenamepattern>
            ${log_dir}/${loggername}.%d{yyyy-mm-dd}.log.gz
          </filenamepattern>
          <maxhistory>15</maxhistory>
        </rollingpolicy>
 
        <filter class="ch.qos.logback.classic.filter.thresholdfilter">
          <level>info</level>
        </filter>
 
        <encoder>
          <pattern>%d{hh:mm:ss.sss} %-5level - %msg%n
          </pattern>
        </encoder>
      </appender>
 
      <appender name="file-error-${loggername}" class="ch.qos.logback.core.rolling.rollingfileappender">
        <file>${log_dir}/${loggername}.err.log</file>
        <rollingpolicy class="ch.qos.logback.core.rolling.timebasedrollingpolicy">
          <filenamepattern>
            ${log_dir}/${loggername}.%d{yyyy-mm-dd}.err.log.gz
          </filenamepattern>
          <maxhistory>15</maxhistory>
        </rollingpolicy>
 
        <filter class="ch.qos.logback.classic.filter.thresholdfilter">
          <level>error</level>
        </filter>
 
        <encoder>
          <pattern>%d{hh:mm:ss.sss} %-5level - %msg%n
          </pattern>
        </encoder>
      </appender>
    </sift>
  </appender>
  <!-- 追加器结束 -->
 
  <!-- 日志开始 -->
  <logger name="com.some.service" level="info" additivity="false">
    <appender-ref ref="sift"/>
  </logger>
  <!-- 日志结束 -->
 
</configuration>

下的是对应的 loggernamebaseddiscriminator 类

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package com.utils.loggers;
 
import ch.qos.logback.classic.spi.iloggingevent;
import ch.qos.logback.core.sift.abstractdiscriminator;
 
public class loggernamebaseddiscriminator extends abstractdiscriminator<iloggingevent> {
  private static final string key = "loggername";
  private string defaultvalue;
 
  public string getdefaultvalue() {
    return defaultvalue;
  }
 
  public void setdefaultvalue(string defaultvalue) {
    this.defaultvalue = defaultvalue;
  }
  # 这就是之所以xml里面可以引用loggername变量的原因
  public string getkey() {
    return key;
  }
 
  public void setkey() {
    throw new unsupportedoperationexception("key not settable. using " + key);
  }
 
  public string getdiscriminatingvalue(iloggingevent e) {
    string loggername = e.getloggername();
 
    if (loggername == null)
      return defaultvalue;
    else {
      string[] split = loggername.split("\\.");
      return split[split.length - 1];
    }
  }
}

最开始我的日志里面没有报错信息, 正常的生成info日志. 但是后来发现事情好像不是想象的那样

问题出现

后来我把程序改成多线程. 发现所有涉及到多线程的服务日志里面都没信息了. google半天, 发现几个令我震惊的真相:

  • 真相1: 所有滚动appender都不支持异步追加 (其实也不是, 但是那种方式需要写死日志文件名, 不推荐, 不讲)
  • 真相2: siftingappender 内部最多嵌套一个appender. 所以理论上我的error的日志里面应该永远不会有内容.

问题解决

对于之前的两个问题, 分而治之.

不支持异步

再次谷歌(到这里读者基本上发现了我搬砖的本质), 发现有个appender名字叫asyncappender, 这玩意是一个其他appender的wrapper. 说白了, 就是你打日志的命令是异步的, 放到队列里面, 而它真正的打日志的动作是一个单独的同步线程. 这就牛逼了, 使用这玩意收集我所有日志, 然后再转发给siftingappender 进行分发即可.

siftingappender 内部最多嵌套一个appender

这个好办, 把info的appender和error的appender拆开放到两个siftingappender里面就行了, 不过这样的话, 前面提到的的asyncappender 也要写两个.

最后, logback-spring.xml文件如下

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <springproperty scope="context" name="log_dir" source="global.log-dir" defaultvalue="./log/"/>
 
  <!-- 追加器开始 -->
  <appender name="sift" class="ch.qos.logback.classic.sift.siftingappender">
    <discriminator class="com.utils.loggers.loggernamebaseddiscriminator">
    </discriminator>
    <sift>
      <appender name="file-${loggername}"
           class="ch.qos.logback.core.rolling.rollingfileappender">
        <file>${log_dir}/${loggername}.log</file>
        <rollingpolicy
            class="ch.qos.logback.core.rolling.timebasedrollingpolicy">
          <filenamepattern>
            ${log_dir}/${loggername}.%d{yyyy-mm-dd}.log
          </filenamepattern>
          <maxhistory>15</maxhistory>
        </rollingpolicy>
 
        <filter class="ch.qos.logback.classic.filter.thresholdfilter">
          <level>info</level>
        </filter>
 
        <encoder>
          <pattern>%d{hh:mm:ss.sss} %-5level - %msg%n
          </pattern>
        </encoder>
      </appender>
    </sift>
  </appender>
  <appender name="sift-err" class="ch.qos.logback.classic.sift.siftingappender">
    <discriminator class="com.infervision.utils.loggers.loggernamebaseddiscriminator">
    </discriminator>
    <sift>
      <appender name="file-error-${loggername}" class="ch.qos.logback.core.rolling.rollingfileappender">
        <file>${log_dir}/${loggername}.err.log</file>
        <rollingpolicy class="ch.qos.logback.core.rolling.timebasedrollingpolicy">
          <filenamepattern>
            ${log_dir}/${loggername}.%d{yyyy-mm-dd}.err.log
          </filenamepattern>
          <maxhistory>15</maxhistory>
          <totalsizecap>50mb</totalsizecap>
        </rollingpolicy>
 
        <filter class="ch.qos.logback.classic.filter.thresholdfilter">
          <level>error</level>
        </filter>
 
        <encoder>
          <pattern>%d{hh:mm:ss.sss} %-5level - %msg%n
          </pattern>
        </encoder>
      </appender>
    </sift>
  </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 ="sift"/>
  </appender>
 
  <!-- 异步输出 -->
  <appender name ="async-err" class= "ch.qos.logback.classic.asyncappender">
    <!-- 不丢失日志.默认的,如果队列的80%已满,则会丢弃tract、debug、info级别的日志 -->
    <discardingthreshold >0</discardingthreshold>
    <!-- 更改默认的队列的深度,该值会影响性能.默认值为256 -->
    <queuesize>512</queuesize>
    <!-- 添加附加的appender,最多只能添加一个 -->
    <appender-ref ref ="sift-err"/>
  </appender>
  <!-- 追加器结束 -->
 
  <!-- 日志开始 -->
 
  <logger name="com.some.service" level="info" additivity="false">
    <appender-ref ref="async"/>
  </logger>
  <!-- 日志结束 -->
 
</configuration>

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持服务器之家。

原文链接:https://www.jianshu.com/p/9493855ce4cc