理解SpringBoot 的日志设计
在项目中导入spring-boot-starter.jar依赖,它会传递 导入spring-boot-starter-logging.jar依赖,依赖关系如下图:
spring-boot-starter-logging.jar依赖三个jar包:
- logback-classic.jar:它传递依赖于logback-core.jar和slf4j-api.jar
- log4j-to-slf4j.jar:它传递依赖于log4j-api.jar和slf4j-api.jar
- jul-to-slf4j.jar:它传递依赖于slf4j-api.jar
java的日志框架比较多,常见的包括:SLF4J、Log4j、Log4j2、Logback、common-logging(JCL)、java.util.logging(JUL)、JBoss Logging等,这些日志框架又分:
- 门面类(抽象层):SLF4J、JCL、JBoss Logging
- 日志实现:Log4j、Log4j2、Logback、common-logging(JCL)
SpringBoot默认使用SLF4J+Logback组合,SLF4J作为日志门面(应用程序输出日志时应该面向改API),Logback作为日志实现。
由于SpringBoot要整合大量的第三方框架,这些框架可能使用JCL、Log4j、JUL等。因此SpringBoot提供对应的日志路由,将其他框架生成的日志信息统一路由给SLF4J处理。从上面依赖关系可以看出:
- log4j-to-slf4j.jar:负责将Log4j日志路由到SLF4J
- jul-to-slf4j.jar:负责将JUL日志路由到SLF4J
虽然SpringBoot默认采用Logback作为底层日志实现,但通过配置允许将底层日志实现改为其他框架。SpringBoot允许将Logback依赖排除出去,添加其他日志实现(比如log4j)的依赖。
需要注意的是:当吧SpringBoot应用部署到Web服务器或应用服务器上时,JUL生成的日志不会被路由到SpringBoot应用的日志中,这是为了避免将服务器或者服务器上的其他应用的日志也路由到SpringBoot的日志中,否则会造成日志混乱。
日志级别与格式
代码示例:控制器类
java
@RestController
public class HelloController
{
Logger logger = LoggerFactory.getLogger(this.getClass());
@GetMapping
public Map<String, Object> hello()
{
logger.trace("-------TRACE级别的日志-------");
logger.debug("-------DEBUG级别的日志-------");
logger.info("-------INFO级别的日志-------");
logger.warn("-------WARN级别的日志-------");
logger.error("-------ERROR级别的日志-------");
return Map.of("hello", "Hello");
}
}
日志级别主要分为(级别由低到高):
- all:输出所有日志
- trace
- debug
- info
- warn
- error
- fatal:log4j增加的一种日志级别,代表"致命错误",比error级别更高。(由于SpringBoot不支持此级别,因此会被自动转换为error级别)
- off:关闭所有日志
日志系统有一个规则:当日志的输出方法的级别高于或等于日志的设置级别,该日志才会实际输出。
比如日志级别设为info,当程序使用info()、warn()、error()输出时,日志才会实际输出;使用trace()、debug()输出的日志会被忽略。
因此,日志级别越高,输出日志就越精简,性能开销越小;日志级别越低,输出日志就越详细,性能开销越大。一般项目处于开发、测试、试运行阶段,日志级别设置的低;在项目实际运行阶段,日志级别设置的高。
由上图可知,SpringBoot默认的日志级别是Info。
SpringBoot输出的日志包括如下信息:
- 日期和时间:精确到毫秒
- 日志级别
- 进程ID
- 分隔符:三个减号(---)
- 线程名:方括号里面的内容(在控制台输出时可能会被截断)
- 日志名:通常是完整类名(为了便于阅读,包名经常简写)
- 日志信息
设置日志级别的几种方式:
- 通过debug=true或trace=true等属性(可通过配置文件、命令行参数、系统变量、OS环境变量等方式)改变整个SpringBoot核心的日志级别
- 通过logging.level. = 属性(可通过配置文件、命令行参数、系统变量等方式)设置日志级别。其中 代表日志名,通常是包名或全限定类名,而Level可以是各种日志级别。
需要注意的是,当启用trace或debug模式时,SpringBoot的核心日志(包括嵌入式容器、Hibernate和SpringBoot)被设为对应的级别,但是其他程序组件不会被设为对应级别。
例如:添加命令行参数: --trace 。 启动上面代码运行结果如下:
可见,程序组件本身的日志级别没有改变。
那么要设置程序组件的日志级别,要通过logging.level. = 属性来设置
yaml
logging:
level:
# 将org.crazyit.app包及其子包下所有日志级别设为TRACE
org.crazyit.app: trace
运行结果如下
学会之后解决以下几个问题就很简单了。
- 让Mybatis输出SQL语句(logging.level.<mapper组件所在的包>=debug)
- 输出Redis的详细执行过程(logging.level.io.lettuce.core=debug)
- 输出MongoDB的详细执行过程(logging.level.com.mongodb=debug)
SpringBoot允许通过spring.output.ansi.enabled属性设置是否用不同颜色来区分不同级别日志,该属性支持以下属性值:
- always:总是启用
- detect:自动检查。如果控制台支持ansi颜色特性,则启用。这是默认值。
- never:不启用
如果要改变控制台的日志格式,可通过logging.pattern.console属性进行设置。其默认值是:
%clr(%d{${LOG_DATEFORMAT_PATTERN:-yyyy-MM-dd HH:mm:ss.SSS}}){faint}
%clr(${LOG_LEVEL_PATTERN:-%5p})
%clr(${PID:- }){magenta}
%clr(---){faint}
%clr([%15.15t]){faint}
%clr(%-40.40logger{39}){cyan}
%clr(😃{faint}
%m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}
上面配置由多个"%clr(输出内容){颜色值} "片段组成,每个片段代表一个输出元素,其中{颜色值} 用于指定该片段的颜色。此处颜色值支持如下几个值:
- blue:蓝色
- cyan:青色
- faint:原色
- green:绿色
- magenta:紫红色
- red:红色
- yellow:黄色
如果不指定颜色之后,直接使用"%clr(输出内容)",表面使用默认的颜色。
比如上面设置包含以下片段:
- %clr(${LOG_LEVEL_PATTERN:-%5p}) :表明以日志级别对应的颜色来输出
- %clr(${PID:- }){magenta} :以紫红色输出进程ID
- %clr(---){faint} :以原色来输出三个减号(---)
假如输出日志不要显示日期、时间,设置时去掉"%clr(%d{${LOG_DATEFORMAT_PATTERN:-yyyy-MM-dd HH:mm:ss.SSS}}){faint} "片段即可。
需要注意的是:logging.pattern.console属性仅当使用Logback日志实现时才有效。
输出日志到文件
要将日志输出到文件,设置如下两个属性之一:
- logging.file:设置日志文件
- logging.path:设置日志文件目录。使用默认的spring.log作为文件名。
使用logging.file或logging.path属性输出日志到文件
logging.file.name | logging.file.path | 示例 | 描述 |
---|---|---|---|
无 | 无 | 只输出到控制台 | |
指定文件 | 无 | my.log | 输出到特定文件,文件路径可以是绝对或相对路径 |
无 | 指定目录 | /f:/log | 写入指定路径下的spring.log文件,该路径可以是绝对或相对路径 |
SpringBoot默认只将info、warn、error三个级别的日志输出到文件。
当日志文件达到10MB时,会自动使用新文件。若要改变这个设置,对于Logback日志实现(SpringBoot默认),可直接使用application.properties(或application.yml)设置;对于其他日志实现,需要对应的日志设置文件来设置。比如log4j,需要使用log4j.xml设置。
Logback的日志设置
属性名称 | 描述 |
---|---|
logging.logback.rollingpolicy.file-name-pattern | 设置对日志归档的文件名模版 |
logging.logback.rollingpolicy.clean-history-on-start | 应用启动时是否清除日志文档 |
logging.logback.rollingpolicy.max-file-size | 日志文件归档之前的最大大小 |
logging.logback.rollingpolicy.total-size-cap | 日志归档被删除之前所能容纳的最大大小 |
logging.logback.rollingpolicy.max-history | 设置保留多少天的日志归档(默认7天) |
代码示例:控制器类org.crazyit.app.controller
java
@RestController
@Slf4j
public class HelloController
{
@GetMapping
public Map<String, Object> hello()
{
log.trace("-------TRACE级别的日志-------");
log.debug("-------DEBUG级别的日志-------");
log.info("-------INFO级别的日志-------");
log.warn("-------WARN级别的日志-------");
log.error("-------ERROR级别的日志-------");
return Map.of("hello", "Hello");
}
}
上面的控制器类使用@Slf4j注解修饰,里面的方法可直接使用 log.trace()等方法来输出日志,那这个log对象是从哪来的?
因为本例使用Lombok工具,这个工具专门通过各种注解来生成常用的代码,比如以下常用的注解:
- @Getter:为所有实例变量生成getter方法
- @Setter:为所有非final实例变量生成setter方法
- @ToString:自动生成toString()方法
- @EqualsAndHashCode:自动生成equals()和hashCode()方法
- @AllArgsConstructor:自动生成带所有参数的构造器
- @NoArgsConstructor:自动生成无参构造器
- @Data:自动生成一个数据类,相当于@Getter、@Setter、@ToString、@EqualsAndHashCode、@AllArgsConstructor、@NoArgsConstructor等注解的组合
- @Log、@Log4j、@Log4j2、@Slf4j、@CommonsLog、@JBossLog、@Flogger:为对应的日志实现生成一个日志对象
为了在应用中使用Lombok,需要做一下两件事:
-
添加Lombok依赖:
xml<dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.16</version> </dependency>
-
为IDEA添加Lombok插件,通过主菜单FIle-》Settings-》Plugins-》搜索插件,然后安装即可。
代码示例:application-addition.yml
yaml
logging:
level:
# 将org.crazyit.app包及其子包下所有日志级别设为TRACE
org.crazyit.app: trace
file:
# 指定日志文件的输出目录,在应用的根目录下生成logs文件夹,该文件夹下生成默认文件名为spring.log
# path: logs/
# 指定日志文件,生成在应用的根目录下
name: my.log
日志组
日志组就是将多个包、类组合在一起,起一个名字,以后可直接对改组设置日志级别,对改组设置就相当于同时为改组内的所有包及其子包、类统一设置了日志级别。
代码示例:控制器类,为上面的控制器类再增加这个控制器类org.fkjava.app.controller
java
@RestController
@Slf4j
public class FkController
{
@GetMapping("/fk")
public Map<String, Object> hello()
{
log.trace("-------TRACE级别的日志-------");
log.debug("-------DEBUG级别的日志-------");
log.info("-------INFO级别的日志-------");
log.warn("-------WARN级别的日志-------");
log.error("-------ERROR级别的日志-------");
return Map.of("hello", "Hello");
}
}
代码示例:application-addition.yml
yaml
logging:
group:
# 将org.crazyit.app和org.fkjava.app两个包定义成fkapp组
fkapp: org.crazyit.app, org.fkjava.app
level:
# 将fkapp组对应的包及其子包的所有日志级别设为TRACE
fkapp: trace
关闭控制台日志
如果想改变SpringBoot的底层日志实现(放弃Logback),则需要如下2步:
- 去掉Logback依赖库,添加新日志实现的依赖库
- 在类加载路径的根路径下为新日志提供对应的配置文件
SpringBoot默认从类加载路径的根路径下加载日志框架的配置文件,也可通过logging.config属性来设置新的加载路径。
SpringBoot既可以根据底层依赖库自动选择合适的日志实现,也可通过org.springframework.boot.logging.LoggingSystem属性显式指定日志实现。属性值可以是LoggingSystem实现类的全限定类名(比如Log4J2LoggingSystem, LobbackLoggingSystem, JavaLoggingSystem等类的全限定类名);也可设置为none,也就彻底关闭SpringBoot的日志系统了。
注意:由于日志初始化会在ApplicantContext创建之前完成,因此不能通过SpringBoot的配置文件来配置logging.config、org.springframework.boot.logging.LoggingSystem等日志控制属性,只能通过系统属性来设置。
不同日志系统对应的配置文件
日志系统 | 配置文件 |
---|---|
Logback | logback-spring.xml, logback-spring.groovy, logback.xml或logback.groovy |
Log4j2 | log4j2-spring.xml或log4j2.xml |
JDK(JUL) | logging,properties |
注意:SpringBoot推荐使用带-spring的配置文件,比如Logback日志系统,使用 logback-spring.xml更好。此外尽量避免使用JUL日志系统,因此JUL的类加载机制会导致一些问题。
对日志进行定制的属性
SpringBoot属性 | 系统属性 | 说明 |
---|---|---|
logging.exception-conversion-word | LOG_EXCEPTION_CONVERSION_WORD | 记录异常的转换字 |
logging.file.name | LOG_FILE | 指定日志文件名 |
logging.file.path | LOG_PATH | 指定日志输出路径,使用spring.log作为文件名 |
logging.pattern.console | CONSOLE_LOG_PATTERN | 控制台日志的格式模板 |
logging.pattern.dateformat | LOG_DATEFORMAT_PATTERN | 日期格式模板 |
loggging.charset.console | CONSOLE_LOG_CHARSET | 输出控制台日志的字符集 |
logging.pattern.file | FILE_LOG_PATTERN | 文件日志的格式模板,仅当日志输出到文件时才有效 |
logging.charset.file | FILE_LOG_CHARSET | 文件日志所用的字符集,仅当日志输出到文件时才有效 |
logging.pattern.level | LOG_LEVEL_PATTERN | 指定输出日志级别时使用的格式(默认为%5p) |
PID | PID | 当前进程ID |
SpringBoot为logback提供了一些通用的配置文件,开发者只要导入这些配置文件,即可使用预定义的配置。这些文件位于org/springframework/boot/logging/logback/路径下,其中常用的有:
- defaults.xml:提供了转换规则及各种通用配置
- console-appender.xml:定义一个ConsoleAppender,将日志输出到控制台
- file-appender.xml:定义一个RollingFileAppender,将日志输出到文件
代码示例:logback-spring.xml
xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!-- 导入Logback通用的日志配置 -->
<include resource="org/springframework/boot/logging/logback/defaults.xml" />
<!-- 定义日志文件 -->
<property name="LOG_FILE" value="${LOG_FILE:-${LOG_PATH:-${LOG_TEMP:-${java.io.tmpdir:-/tmp}}/}spring.log}"/>
<!-- 导入输入到文件的日志配置 -->
<include resource="org/springframework/boot/logging/logback/file-appender.xml" />
<!-- 指定将日志输出到文件 -->
<root level="INFO">
<appender-ref ref="FILE" />
</root>
</configuration>
在上面的配置文件中可指定如下占位符:
- ${PID}:当前进程ID
- ${LOG_FILE}:代表是否通过外部配置设置了logging.file.name属性
- ${LOG_PATH}:代表是否通过外部配置设置了logging.file.path属性
- ${LOG_EXCEPTION_CONVERSION_WORD}:代表是否通过外部配置设置了logging.exception-conversion-word属性
上面配置将日志输出到文件,因此必须在application-addition.yml指定logging.file.name或logging.file.path属性。
代码示例:application-addition.yml
yaml
logging:
level:
# 将org.crazyit.app包及其子包下所有日志级别设为TRACE
org.crazyit.app: trace
file:
# 指定日志文件的输出目录,默认文件名为spring.log
# path: logs/
# 指定日志文件
name: my.log
此例子不会在控制台输出日志,仅在当前目录的my.log文件中输出日志。
改用Log4j2日志实现
去除Logback依赖库,添加Log4j2依赖库
xml
<!-- Spring Web依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<!-- 去除spring-boot-starter-logging日志 -->
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>
通过上面配置,项目底层日志框架改成了Log4j2,得益于SpringBoot的日志的抽象机制,上层程序使用的日志没有任何改变。
如果要对Log4j2自定义配置,可通过log4j2.yml或log4j2.json配置。
Logback扩展
可通过logback-spring.xml对Logback配置扩展功能。注意:不能使用logback.xml,因为该文件的加载时机太早,SpringBoot的其他基础功能还没来得及加载。
springProfile标签的name属性可指定相关Profile的日志配置
代码示例:logback-spring.xml
xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!-- 以下配置仅当活动Profile为default、dev和test时有效 -->
<springProfile name="default | dev | test">
<include resource="org/springframework/boot/logging/logback/defaults.xml" />
<property name="LOG_FILE" value="${LOG_FILE:-${LOG_PATH:-${LOG_TEMP:-${java.io.tmpdir:-/tmp}}}/spring.log}"/>
<include resource="org/springframework/boot/logging/logback/console-appender.xml" />
<include resource="org/springframework/boot/logging/logback/file-appender.xml" />
<root level="DEBUG">
<appender-ref ref="CONSOLE" />
<appender-ref ref="FILE" />
</root>
<!-- 指定org.crazyit.app日志的级别是DEBUG -->
<logger name="org.crazyit.app" level="DEBUG"/>
</springProfile>
<!-- 以下配置仅当活动Profile为prod时有效 -->
<springProfile name="prod">
<include resource="org/springframework/boot/logging/logback/defaults.xml" />
<property name="LOG_FILE" value="${LOG_FILE:-${LOG_PATH:-${LOG_TEMP:-${java.io.tmpdir:-/tmp}}}/spring.log}"/>
<include resource="org/springframework/boot/logging/logback/console-appender.xml" />
<include resource="org/springframework/boot/logging/logback/file-appender.xml" />
<root level="INFO">
<appender-ref ref="CONSOLE" />
<appender-ref ref="FILE" />
</root>
<!-- 指定org.crazyit.app日志的级别是INFO -->
<logger name="org.crazyit.app" level="INFO"/>
</springProfile>
</configuration>
此外,logback的配置文件还可读取SpringBoot的配置属性,通过<springProperty.../> 元素来获取,该元素支持以下属性:
- name:为读取到的属性值指定名字
- source:指定读取哪个配置属性。推荐使用"烤串"写法(比如application-dev)
- scope:指定存储该配置属性的作用域
- defaultValue:当配置属性不存在时,指定默认值
比如:
xml
<springProperty scope="context" name="fluentHost" source="myapp.fluentd.host" defaultValue="localhost" />
<appender name="FLUENT" class="ch.qos.logback.more.appenders.DataFluentAppender">
<!-- 使用前面定义的fluentHost属性-->
<remoteHost>${fluentHost}</remoteHost>
</appender>