logback 简介

logback 官网:https://logback.qos.ch/

logback 由三个模块组成:

  • logback-core
  • logback-classic
  • logback-access

logback-core 是其它模块的基础设施,其它模块基于它构建,logback-core 提供了一些关键的通用机制。

logback-classic 的地位和作用等同于 Log4J,它也被认为是 Log4J 的一个改进版,并且它实现了简单日志门面 SLF4J

logback-access 主要作为一个与 Servlet 容器交互的模块,比如说tomcat或者 jetty,提供一些与 HTTP 访问相关的功能。

配置文件

接下来会介绍关于 logback 配置文件的配置项。

Configuration

整个 logback.xml 配置文件的结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<configuration scan="true" scanPeriod="60 seconds" debug="false">  
<property name="glmapper-name" value="glmapper-demo" />
<contextName>${glmapper-name}</contextName>

<appender>
//xxxx
</appender>

<logger>
//xxxx
</logger>

<root>
//xxxx
</root>

</configuration>
  • scan:当此属性设置为true时,配置文件如果发生改变,将会被重新加载,默认值为true。

  • scanPeriod:设置监测配置文件是否有修改的时间间隔,如果没有给出时间单位,默认单位是毫秒。当scan为true时,此属性生效。默认的时间间隔为1分钟。

  • debug:当此属性设置为true时,将打印出logback内部日志信息,实时查看logback运行状态。默认值为false。

contextName

logger上下文,默认名称为 “default”。可以使用 contextName 标签设置成其他名字,用于区分不同应用程序。

1
<contextName>xxl_job</contextName>

property

用于定义变量标签,name 为变量的名称,value 的值是变量的值。可以用 “${name}” 来使用变量。

1
2
<!-- 日志记录器,日期滚动记录 -->
<property name="LOG_PATH" value="./logs/xxl-job-admin" />

logger

用来设置某一个包或者具体的某一个类的日志打印级别以及指定appender。这里的 level 是向下兼容的,即 DEBUG 级别的也会包含 INFO 级别的日志。

name:用来指定受此logger约束的某一个包或者具体的某一个类。

level:用来设置打印级别(TRACE, DEBUG, INFO, WARN, ERROR, ALLOFF),还有一个值INHERITED或者同义词NULL,代表强制执行上级的级别。如果没有设置此属性,那么当前logger将会继承上级的级别。

addtivity:用来描述是否向上级logger传递打印信息。默认是true

1
2
3
<logger name="org.springframework" level="WARN" additivity="true" />
<logger name="org.mybatis" level="DEBUG" additivity="true" />
<logger name="com.xxl.job.executor" level="DEBUG" additivity="true"/>

root

根logger,也是一种logger,且只有一个level属性。根logger 用于控制 appender 配置的日志等级和输出权限。

1
2
3
4
5
6
7
8
9
<!-- 生产环境下,将此级别配置为适合的级别,以免日志文件太多或影响程序性能 -->
<root level="INFO">
<appender-ref ref="FILEERROR" />
<appender-ref ref="FILEWARN" />
<appender-ref ref="FILEINFO" />
<appender-ref ref="FILEALL" />
<!-- 生产环境将请stdout,testfile去掉 -->
<appender-ref ref="STDOUT" />
</root>

filter

filter其实是appender里面的子元素。它作为过滤器存在,执行一个过滤器会有返回DENY,NEUTRAL,ACCEPT三个枚举值中的一个。可以为appender 添加一个或多个过滤器,可以用任意条件对日志进行过滤。appender 有多个过滤器时,按照配置顺序执行。

  • DENY:日志将立即被抛弃不再经过其他过滤器
  • NEUTRAL:有序列表里的下个过滤器过接着处理日志
  • ACCEPT:日志会被立即处理,不再经过剩余过滤器

filter 还指定了一个 class,class有两个种类:

  • ThresholdFilter:临界值过滤器,过滤掉低于临界值的日志。当日志级别等于或高于临界值,过滤器返回 NEUTRAL;当日志级别低于临界值时,日志会被拒绝。
  • LevelFilter:级别过滤器,根据日志级别进行过滤。如果日志级别等于配置级别,过滤器会根据 onMatch 和 onMismatch 接收或拒绝日志。
1
2
3
4
5
6
7
8
9
10
11
12
13
<!-- 日志记录器,日期滚动记录 -->
<appender name="FILEINFO" class="ch.qos.logback.core.rolling.RollingFileAppender">
......

<!-- 此日志文件只记录info级别的 -->
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>info</level>
<!-- 如果命中就使用这条规则 -->
<onMatch>ACCEPT</onMatch>
<!-- 如果没有命中就禁止这条日志 -->
<onMismatch>DENY</onMismatch>
</filter>
</appender>

appender

appender是一个日志打印的组件,这里组件里面定义了打印过滤的条件、打印输出方式、滚动策略、编码方式、打印格式等等。但是它只是一个配置,这个配置的开关和打印级别由 logger 或者 root 的 appender-ref 指定某个具体的 appender 控制。

appender 的种类

appender 有两个属性 nameclass;name指定appender名称,class指定appender的全限定名。

  • ConsoleAppender:把日志添加到控制台
  • FileAppender:把日志添加到文件
  • RollingFileAppender:滚动记录文件,先将日志记录到指定文件,当符合某个条件时,将日志记录到其他文件。它是FileAppender的子类
1
2
3
<appender name="GLMAPPER-LOGGERONE"
class="ch.qos.logback.core.rolling.RollingFileAppender">
</appender>

append 子标签

1
<append>true</append>

如果是 true,日志被追加到文件结尾,如果是false,清空现存文件,默认是true

file 子标签

file 标签用于指定被写入的文件名,可以是相对也可以是绝对路径,如果上级目录不存在会自动创建,没有默认值。

1
2
3
<file>
${logging.path}/glmapper-spring-boot/glmapper-loggerone.log
</file>

表示当前appender将会将日志写入到${logging.path}/glmapper-spring-boot/glmapper-loggerone.log这个目录下。

rollingPolicy 子标签

这个子标签用来描述滚动策略的。这个只有appenderclassRollingFileAppender时才需要配置。

1
2
3
4
5
6
7
8
9
10
11
<!-- 日志记录器的滚动策略,按日期,按大小记录 -->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- 归档的日志文件的路径,例如今天是2013-12-21日志,当前写的日志文件路径为file节点指定,可以将此文件与file指定文件路径设置为不同路径,从而将当前日志文件或归档日志文件置不同的目录。
而2013-12-21的日志文件在由fileNamePattern指定。%d{yyyy-MM-dd}指定日期格式,%i指定索引 -->
<fileNamePattern>${LOG_PATH}/all/log-all-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<!-- 除按日志记录之外,还配置了日志文件不能超过2M,若超过2M,日志文件会以索引0开始,
命名日志文件,例如log-error-2013-12-21.0.log -->
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>100MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
</rollingPolicy>

TimeBasedRollingPolicy

最常用的滚动策略,它根据时间来制定滚动策略,既负责滚动也负责出发滚动。这个下面又包括了两个属性:

  • FileNamePattern
  • maxHistory
1
2
3
4
5
6
7
8
9
<rollingPolicy 
class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!--日志文件输出的文件名:按天回滚 daily -->
<FileNamePattern>
${logging.path}/glmapper-spring-boot/glmapper-loggerone.log.%d{yyyy-MM-dd}
</FileNamePattern>
<!--日志文件保留天数-->
<MaxHistory>30</MaxHistory>
</rollingPolicy>

上面的这段配置表明每天生成一个日志文件,保存30天的日志文件

FixedWindowRollingPolicy

根据固定窗口算法重命名文件的滚动策略。

encoder 子标签

对记录事件进行格式化。它干了两件事:

  • 把日志信息转换成字节数组
  • 把字节数组写入到输出流
1
2
3
4
5
<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>

目前encoder只有PatternLayoutEncoder一种类型。

不同日志隔离级别打印

根据包、类隔离

1
2
3
4
5
6
7
8
9
10
11
12
<!--此logger约束将.service包下的日志输出到GLMAPPER-SERVICE,错误日志输出到GERROR-APPENDE;GERROR-APPENDE见上面-->
<logger name="com.glmapper.spring.boot.service" level="${logging.level}" additivity="false">
<appender-ref ref="GLMAPPER-SERVICE" />
<appender-ref ref="GERROR-APPENDER" />
</logger>


<!--这里指定到了具体的某一个类-->
<logger name="com.glmapper.spring.boot.task.TestLogTask" level="${logging.level}" additivity="true">
<appender-ref ref="SCHEDULERTASKLOCK-APPENDER" />
<appender-ref ref="ERROR-APPENDER" />
</logger>

根据环境隔离

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
 <springProfile name="dev">
<root level="debug">
<appender-ref ref="STDOUT"/>
<!-- <appender-ref ref="sendErrorMsgAppender"/>-->
</root>
</springProfile>

<springProfile name="sit">
<root level="debug">
<appender-ref ref="STDOUT"/>
<!-- <appender-ref ref="sendErrorMsgAppender"/>-->
</root>
</springProfile>

<springProfile name="uat">
<root level="info">
<appender-ref ref="STDOUT"/>
<!-- <appender-ref ref="sendErrorMsgAppender"/>-->
</root>
</springProfile>

<springProfile name="prod">
<root level="info">
<appender-ref ref="STDOUT"/>
<appender-ref ref="sendErrorMsgAppender"/>
</root>
</springProfile>

logback小实战

使用 logback 配置将日志定义到自定义输出源,可以拿SpringBoot 整合 logback 发送企微通知作为例子。

要实现error级别异常日志异常报警,就是要捕获所有的error级别的日志,然后解析出异常数据,调用企业微信接口发送消息即可。

Logback中的Appender类用来表示日志的输出的目的地。所以我们只需要自定义一个 Appeder,然后在Logback的配置文件中的所有的Logger配置中(或者是所有Error级别的 Logger配置)增加这个自定义的Appeder就可以以拦截所有的(异常)日志。

首先定义一个企微发送消息的Appender。

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
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
public class SendErrorMsgAppender extends UnsynchronizedAppenderBase<LoggingEvent> {

private String pattern;

private PatternLayout layout;

private Level nowLevel = Level.ERROR;

protected Logger logger = LoggerFactory.getLogger(this.getClass());

@Override
protected void append(LoggingEvent eventObject) {
if (eventObject == null || !eventObject.getLevel().isGreaterOrEqual(nowLevel)) {
return;
}
try {
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();

String url = "", userAccount = "", bodyString = "";
if (requestAttributes != null) {
HttpServletRequest request = requestAttributes.getRequest();

url = request.getRequestURI();
userAccount = getUserinfo(request);
bodyString = ServletUtil.getBody(request);


if (StringUtils.isNotBlank(bodyString)) {
bodyString = JSON.toJSONString(JSON.parse(bodyString));
}

logger.info("url:{}", url);
} else if (StringUtils.isBlank(eventObject.getFormattedMessage())) {
// 没有地址的错误日志不发送直接存储到日志文件即可,避免企业微信页面展示过多的错误信息
logger.info("lockKey:{}", eventObject.getLoggerName());
return;
}
Environment bean = SpringUtil.getBean(Environment.class);

String active = bean.getProperty("spring.profiles.active");
// dev本地调试异常错误不推送企微
if (!"prod".equals(active)) {
return;
}

String serverName = "jr-ai-open-api";
String errorMessage = layout.doLayout(eventObject);
String webHook = bean.getProperty("qyWeChat.webHook");

if (StringUtils.isNotBlank(webHook)) {
toWechat(webHook, serverName, active, url, userAccount, bodyString, errorMessage);
}
} catch (Exception e) {
e.printStackTrace();
}
}

@Override
public void start() {
PatternLayout patternLayout = new PatternLayout();
patternLayout.setContext(context);
patternLayout.setPattern(getPattern());
patternLayout.start();
this.layout = patternLayout;
super.start();
}

public void toWechat(String robotUrl, String projectName, String environment, String requestUrl, String requestAccount, String requestBody, String errorLog) throws Exception {
String markdownContent = buildMarkdownContent(projectName, environment, requestUrl, requestAccount, requestBody, errorLog);
String markdownMsg = "{\"msgtype\": \"markdown\", \"markdown\": {\"content\": " + JSON.toJSONString(markdownContent) + "}}";
try (CloseableHttpClient httpclient = HttpClients.createDefault()) {
HttpPost httppost = new HttpPost(robotUrl);
httppost.addHeader("Content-Type", "application/json; charset=utf-8");
httppost.setEntity(new StringEntity(markdownMsg, "utf-8"));
httpclient.execute(httppost);
} catch (Exception e) {
e.printStackTrace();
}
}

private String buildMarkdownContent(String projectName, String environment, String requestUrl, String requestAccount, String requestBody, String errorLog) {
return "<font color=\"red\"> 【ERROR 通知】 </font> \n" +
"> <font color=\"comment\"> 触发项目:</font> <font color=\"info\"> " + projectName + "</font> \n" +
"> <font color=\"comment\"> 触发环境:</font> <font color=\"info\"> " + environment + "</font> \n" +
"> <font color=\"comment\"> 触发时间:</font> " + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")) + "\n" +
"> <font color=\"comment\"> 请求URL:</font> " + requestUrl + "\n" +
"> <font color=\"comment\"> 请求账号:</font> " + requestAccount + "\n" +
"> <font color=\"comment\"> 请求Body:</font> \n```json\n" + requestBody + "\n```\n\n" +
"<font color='red'>【Exception 详情】</font> \n```json\n" + errorLog + "\n```\n";
}


public String getPattern() {
return pattern;
}

public void setPattern(String pattern) {
this.pattern = pattern;
}

public PatternLayout getLayout() {
return layout;
}

public void setLayout(PatternLayout layout) {
this.layout = layout;
}

public Level getNowLevel() {
return nowLevel;
}

public void setNowLevel(Level nowLevel) {
this.nowLevel = nowLevel;
}
}

在 logback.xml 中定义 SendErrorMsgAppender 的输出源,并指定在生产环境下才输出日志。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<appender name="sendErrorMsgAppender" class="com.junrunrenli.proxy.exception.SendErrorMsgAppender">
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [%X{traceId}] [%X{loginName}] %-5level %logger{50} [line:%L]: %ex{10} -%msg%n</pattern>
<filter class="ch.qos.logback.core.filter.EvaluatorFilter">
<!-- 过滤指定类型日志 -->
<evaluator>
<expression>return message.contains("Broken pipe");</expression>
</evaluator>
<OnMatch>DENY</OnMatch>
<OnMismatch>ACCEPT</OnMismatch>
</filter>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>ERROR</level>
<OnMatch>ACCEPT</OnMatch>
<OnMismatch>DENY</OnMismatch>
</filter>
</appender>

<springProfile name="prod">
<root level="info">
<appender-ref ref="STDOUT"/>
<appender-ref ref="sendErrorMsgAppender"/>
</root>
</springProfile>

这样配置以后,项目中所有使用log.error()方法打印的日志(即error级别日志)都会通过企业微信发出消息报警。