springboot使用jsw打包配置logback日志最佳实践
前言
使用springboot开发的应用,可以打包成一个可运行的jar包发布,但在实际生产环境中我们很少这么做,主要是考虑运维和监控相关的工作,一个单独的jar包不方便。
因此更多的作为真正生产发布我们一般打包成一个jsw包(Java Service Wrapper)发布。
jsw包有自己的日志规范,虽然实际上是在原有的日志框架上套了一个马甲,但是和原应用的日志的规范是冲突的,也就是会出现logback配置的日志文件和jsw配置的日志文件双写的情况,浪费磁盘I/O和存储空间。
为了能使用jsw来发布应用程序,就要想办法解决原应用和wrapper的冲突,使两个日志规范能更好的协作。
笔者自己的实际使用的过程中,主要是用springboot依赖的logback日志框架,因此本文会重点讨论如何在springboot中配置logback和jsw来解决日志的问题。
日志需求
在开发过程中,日志配置经常会根据需要进行调整,运维过程中,也要能够按照实际的生产需求配置日志,因此日志框架整合需要满足下面的需求:
- 支持仅在控制台中输出(开发过程常用)
- 支持禁用控制台输出,只输出到文件(生产环境常用)
- 支持日志文件按日期和大小切割
- 支持日志文件现在总大小
- 支持发布后调节日志级别
日志框架对比
jsw的日志框架分割日志文件的功能不是很强大,常用如下两种方式滚动:
- 按日期滚动
- 按文件大小滚动
截至本文发布时,jsw尚不支持同时按日期和文件大小滚动,对于一天会产生大量日志的应用不适用。
logback的功能非常强大,可以支持按多种方式滚动和分割日志文件,并且可以自己扩展和定制格式化日志,因此我们选择使用logback的规范来配置日志文件记录,禁用jsw的日志输出。
JSW的wrapper.conf配置
明确使用的日志规范之后就要考虑如何满足日志的配置需求了,首先需要禁用jsw的日志文件输出,但是为了保证使用
1
$> app console
命令启动时能在控制台看到输出的日志,需要保留jsw的控制台日志输出,并且为了使得jsw输出的日志格式和logback中配置的完全相同,还需要配置jsw日志只输出日志内容,不做格式化。
最终wrapper.conf配置中的日志部分如下:
1
2
3
4
5
6
7
8
9
wrapper.console.format=M # 控制台只输出日志内容
wrapper.console.loglevel=INFO # 控制台输出日志级别,这个级别是jsw自己的日志的级别,无论logback输出的级别是什么,都不受这个配置影响
wrapper.logfile.loglevel=NONE # 禁用jsw日志,这里配置之后,下面的几个配置都无效了
wrapper.logfile=logs/wrapper.log
wrapper.logfile.format=M
wrapper.logfile.maxsize=1m
wrapper.logfile.maxfiles=1
wrapper.syslog.loglevel=NONE
主要注意的是,jsw配置的日志级别是jsw自己的日志级别,跟logback的日志级别没有关系,只要logback会输出到控制台的日志,都会输出到jsw的console中,因此logback的日志级别可以独立配置,这里一般建议使用info级别,用于启动是出现异常时检查异常原因。
maven插件配置
上面的配置可以通过maven插件配置好,直接打包出来的结果就是按要求配置好的。
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
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>appassembler-maven-plugin</artifactId>
<version>2.0.0</version>
<configuration>
<target>${project.build.directory}</target>
<repositoryLayout>flat</repositoryLayout>
<logsDirectory>logs</logsDirectory>
<useWildcardClassPath>true</useWildcardClassPath>
<configurationSourceDirectory>jsw/conf</configurationSourceDirectory>
<configurationDirectory>conf</configurationDirectory>
<copyConfigurationDirectory>true</copyConfigurationDirectory>
<daemons>
<daemon>
<id>${daemon-name}</id>
<mainClass>${daemon-mainClass}</mainClass>
<jvmSettings>
<initialMemorySize>${daemon-JAVA_Xms}</initialMemorySize>
<maxMemorySize>${daemon-JAVA_Xmx}</maxMemorySize>
<extraArguments>
<!--
Note : if the value is empty the plugin will throw NullPointerException
-->
<extraArgument>-Djava.wrapper=1 ${daemon-JAVA_OPS}</extraArgument>
</extraArguments>
</jvmSettings>
<platforms>
<platform>jsw</platform>
</platforms>
<generatorConfigurations>
<generatorConfiguration>
<generator>jsw</generator>
<includes>
<include>linux-x86-32</include>
<include>linux-x86-64</include>
<include>macosx-universal-32</include>
<include>macosx-universal-64</include>
<include>windows-x86-32</include>
<include>windows-x86-64</include>
</includes>
<configuration>
<property>
<name>configuration.directory.in.classpath.first</name>
<value>conf</value>
</property>
<property>
<name>set.default.REPO_DIR</name>
<value>lib</value>
</property>
<property>
<name>wrapper.console.loglevel</name>
<value>INFO</value>
</property>
<property>
<name>wrapper.console.format</name>
<value>M</value>
</property>
<property>
<name>wrapper.logfile</name>
<value>logs/wrapper.log</value>
</property>
<property>
<name>wrapper.logfile.format</name>
<value>M</value>
</property>
<property>
<name>wrapper.logfile.maxsize</name>
<value>100m</value>
</property>
<property>
<name>wrapper.logfile.loglevel</name>
<value>NONE</value>
</property>
<property>
<name>wrapper.startup.timeout</name>
<value>1800</value>
</property>
<property>
<name>wrapper.ping.timeout</name>
<value>1800</value>
</property>
<property>
<name>wrapper.ntservice.name</name>
<value>${daemon-name}</value>
</property>
<property>
<name>wrapper.ntservice.displayname</name>
<value>${daemon-ntservice-displayname}</value>
</property>
<property>
<name>wrapper.ntservice.description</name>
<value>${daemon-ntservice-description}</value>
</property>
<property>
<name>wrapper.console.title</name>
<value>${daemon-ntservice-displayname}</value>
</property>
<property>
<name>wrapper.java.additional.2</name>
<value>-Dfile.encoding=UTF-8</value>
</property>
<property>
<name>wrapper.java.additional.3</name>
<value>-XX:+UnlockExperimentalVMOptions</value>
</property>
<property>
<name>wrapper.java.additional.4</name>
<value>-XX:+UseCGroupMemoryLimitForHeap</value>
</property>
<property>
<name>wrapper.java.additional.5</name>
<value>-Djava.security.egd=file:/dev/./urandom</value>
</property>
<property>
<name>wrapper.java.additional.6</name>
<value>-Dlogging.filename=${project.build.finalName}</value>
</property>
</configuration>
</generatorConfiguration>
</generatorConfigurations>
</daemon>
</daemons>
</configuration>
<executions>
<execution>
<id>generate-jsw-scripts</id>
<phase>package</phase>
<goals>
<goal>generate-daemons</goal>
</goals>
</execution>
</executions>
</plugin>
插件中的变量属性如下:
1
2
3
4
5
6
7
8
9
10
11
12
<properties>
<java.version>1.8</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<!--jsw daemon -->
<daemon-name>${project.artifactId}</daemon-name>
<daemon-ntservice-displayname>${daemon-name}</daemon-ntservice-displayname>
<daemon-ntservice-description>${daemon-name}</daemon-ntservice-description>
<daemon-mainClass>app.Application</daemon-mainClass><!-- 根据实际的类名配置 -->
<daemon-JAVA_Xms/>
<daemon-JAVA_Xmx/>
<daemon-JAVA_OPS/>
</properties>
注意到插件配置了一个启动参数:
1
2
3
4
<property>
<name>wrapper.java.additional.6</name>
<value>-Dlogging.filename=${project.build.finalName}</value>
</property>
这里是一个启动参数属性,在后文会使用这个属性,主要是配合springboot用于指定日志文件名称的。
到这里日志整合中jsw的配置就完成了。
logback的配置
参考logback配置我们可以很方便配置出满足需求的日志方案,但是为了打包发布后在jsw包中修改配置的话,还需要配合springboot的属性来实现。
也就是我们需要能通过spring的application.yml来改动logback日志配置,由于logback.xml的配置加载在spring启动之前,所以无法使用spring的配置,但是springboot提供了另一种方式加载日志配置文件。
可以在classpath目录下创建logback-spring.xml文件,这个文件logback不会加载,会由springboot进行加载,spring加载的过程会对配置文件进行配置增强,扩展支持了几个属性:
1
2
3
4
5
6
7
<configuration>
<springProperty scope="context" name="CONSOLE_APPENDER"
source="logging.appender.console" defaultValue="true"/>
<springProperty name="test,prod">
<appender-ref ref="ERROR_FILE"/>
</springProperty>
</configuration>
这里<springProperty>标签可以使用spring的application.yml中配置的属性,source即属性的key,并且可以指定defaultVale默认值。
<springProperty>标签类似if判断,只有当对应的profile被激活是配置才生效。
在实际使用中,spring扩展的这两个标签依然不够,如果要写一个通用的配置文件给其他的配置引用,还需要支持更多的判断逻辑,可以增加如下maven依赖:
1
2
3
4
5
<dependency>
<groupId>org.codehaus.janino</groupId>
<artifactId>janino</artifactId>
<version>2.7.8</version>
</dependency>
即可使用if判断逻辑:
1
2
3
4
5
6
<if condition="property("ROLLING_FILE_APPENDER").equals("true")">
<then>
<appender-ref ref="ROLLING_FILE"/>
</then>
<else></else>
</if>
更多内容参考Janino。
这样我们就可以使用spring的application.yml来修改logback中的配置了,可以将日志的大部分配置简化为spring的几个属性来控制。
基础配置
在classpath目录下,创建一个基础配置文件,可以给多个配置引用,这里给出一个示例:
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
<?xml version="1.0" encoding="UTF-8"?>
<included>
<contextName>Logback Base For IAM</contextName>
<springProperty scope="context" name="CONSOLE_APPENDER" source="logging.appender.console" defaultValue="true"/>
<springProperty scope="context" name="ROLLING_FILE_APPENDER" source="logging.appender.rollingfile" defaultValue="true"/>
<springProperty scope="context" name="ERROR_FILE_APPENDER" source="logging.appender.errorfile" defaultValue="false"/>
<springProperty scope="context" name="LOG_HOME" source="logging.path" defaultValue="./logs"/>
<springProperty scope="context" name="LOG_FILE_NAME" source="logging.filename" defaultValue="app"/>
<springProperty scope="context" name="ROOT_LEVEL" source="logging.level.root" defaultValue="INFO"/>
<springProperty scope="context" name="MAX_HISTORY" source="logging.rolling.maxHistory" defaultValue="10"/>
<springProperty scope="context" name="MAX_FILE_SIZE" source="logging.rolling.maxFileSize" defaultValue="1GB"/>
<springProperty scope="context" name="TOTAL_SIZE_CAP" source="logging.rolling.totalSizeCap" defaultValue="0"/>
<!-- 设置log日志存放地址 -->
<property name="LOG_HOME" value="${LOG_HOME:-./logs}"/>
<property name="LOG_FILE_NAME" value="${LOG_FILE_NAME:-app}"/>
<property name="MAX_HISTORY" value="${MAX_HISTORY:-10}"/>
<property name="MAX_FILE_SIZE" value="${MAX_FILE_SIZE:-1GB}"/>
<property name="TOTAL_SIZE_CAP" value="${TOTAL_SIZE_CAP:-0}"/>
<property name="ROOT_LEVEL" value="${ROOT_LEVEL:-INFO}"/>
<property name="CONSOLE_APPENDER" value="${CONSOLE_APPENDER:-false}"/>
<property name="ROLLING_FILE_APPENDER" value="${ROLLING_FILE_APPENDER:-false}"/>
<property name="ERROR_FILE_APPENDER" value="${ERROR_FILE_APPENDER:-false}"/>
<!-- 控制台输出 -->
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%15.15t] - %-5level %logger{5} -%msg%n</pattern>
</encoder>
</appender>
<if condition="property("ROLLING_FILE_APPENDER").equals("true")">
<then>
<!-- 按照每天生成日志文件 -->
<appender name="ROLLING_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>${LOG_HOME}/${LOG_FILE_NAME}.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<!--日志文件保留天数 -->
<maxHistory>${MAX_HISTORY}</maxHistory>
<maxFileSize>${MAX_FILE_SIZE}</maxFileSize>
<totalSizeCap>${TOTAL_SIZE_CAP}</totalSizeCap>
</rollingPolicy>
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>%d{dd HH:mm:ss.SSS} [%15.15t] %-5level %logger{20} -%msg%n</pattern>
</encoder>
</appender>
</then>
<else></else>
</if>
<if condition="property("ERROR_FILE_APPENDER").equals("true")">
<then>
<appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>${LOG_HOME}/error.${LOG_FILE_NAME}.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<maxHistory>${MAX_HISTORY}</maxHistory>
<maxFileSize>${MAX_FILE_SIZE}</maxFileSize>
<totalSizeCap>${TOTAL_SIZE_CAP}</totalSizeCap>
</rollingPolicy>
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>ERROR</level>
</filter>
<encoder>
<pattern>%d{dd HH:mm:ss.SSS} %-4relative [%thread] %-5level %logger{20} - %msg%n</pattern>
</encoder>
</appender>
</then>
<else></else>
</if>
<root level="${ROOT_LEVEL}">
<if condition="property("CONSOLE_APPENDER").equals("true")">
<then>
<appender-ref ref="STDOUT"/>
</then>
<else></else>
</if>
<if condition="property("ROLLING_FILE_APPENDER").equals("true")">
<then>
<appender-ref ref="ROLLING_FILE"/>
</then>
<else></else>
</if>
<if condition="property("ERROR_FILE_APPENDER").equals("true")">
<then>
<appender-ref ref="ERROR_FILE"/>
</then>
<else></else>
</if>
</root>
</included>
这个基础配置几乎是完全通用的,假设我们将这个基础配置放在resources/logging/logback/base.xml中,那么使用如下简单配置即可复用基础配置的功能:
1
2
3
4
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<include resource="logging/logback/base.xml" />
</configuration>
这个配置文件即resources/logback-spring.xml
springboot的配置
按照上一步的配置,现在我们可以在application.yml使用如下属性来简单控制日志的配置:
1
2
3
4
5
6
7
8
9
10
11
12
logging:
path: ./logs # 日志存放的目录
rolling:
maxHistory: 10 # 日志最长保留天数
maxFileSize: 1GB # 日志分割的文件最大值(默认达到1G时分割文件)
totalSizeCap: 0 # 日志文件最大容量,为0表示不限制
level:
root: INFO # 默认日志级别
appender:
console: true # 启用控制台日志输出
rollingfile: true # 启用文件日志输出
errorfile: false # 启用ERROR级别日志单独输出日志文件(文件名为rollingfile的文件名加error.前缀)
特别说明:
一般情况下不建议对日志最大容量进行限制,以防出现问题时无法找到日志,如果在某些磁盘空间比较小的测试环境,日志不是特别重要,可以设置日志容量限制。 注意日志容量限制需要同时配置logging.rolling.maxFileSize和logging.rolling.totalSizeCap两个属性,totalSizeCap的值不能小于maxFileSize。
这样的配置即可达到日志需求中要求的全部需求。