本文已参与「新人创作礼」活动,一起开启掘金创作之路。
spring boot日志应用(二)
常见日志框架
日志框架中其实还有两个更细的分类——日志门面和日志实现。如果你了解设计模式,对于门面这个词你应该不会感到陌生,就是 facade 模式。如果你不了解设计模式那么你可以将日志门面理解为日志的接口框架,是对日志输出定义了一套标准,可以配合相应的日志实现框架一起使用。
日志门面
JCL SLF4J
日志实现
Log4j Log4j2 Logback J.U.L
JCL
JCL(Jakarta commons-logging)是 Apache 出品的一款日志门面框架,于 2005 年面世,到 2014 年已经不再更新了。
SLF4J
SLF4J(Simple Logging Facade For Java)由于 SLF4J 读起来太费劲,所以我更喜欢称呼它为 水立方(Shui Li Fang),是 Ceki 大佬写的一个日志门面框架。
Log4J
Log4J 很多人应该都听过或者用过,非常经典的日志框架。其作者与 SLF4J 也是 Ceki,后来托管给了 Apache,目前也不再更新了。
Log4J2
Log4J2 出品自 Apache。
Logback
目前 Spring Boot 也选择了它作为默认的日志框架。
** J.U.L**
JUL 是 Java 原生的日志,从 1.4 版本开始引入,功能过于简陋,这里拿出来说一下,是因为毕竟是 JDK 中自带的,了解一下就好。
选择
通过上面的简单了解,我们需要在门面和实现中各选一个组合起来使用。直接给出结果吧,我们选择 SLF4J + Logback 的组合,原因如下:
Spring Boot 的默认组合,权威验证
配置
在 Spring Boot 中日志的配置有两种方式,一种是直接在 application.yml 文件中配置;另一种是在外置 logback-spring.xml 文件中进行配置。在修改配置之前,我们先看一下默认情况下的日志输出格式:
2021-11-19 21:54:34.041 INFO 1768 --- [nio-8080-exec-1] c.i.s.log.controller.LogController : log level info
可以看到默认情况下一条日志是由以下几部分组成的:
日期时间 日志级别 进程 ID — 分隔符 [xxx] 线程名 类路径 日志消息
application.yml 方式
接下来我们在 application.yml 文件中进行一些简单的配置:
logging:
pattern:
console: "%d - %m%n"
然后再来看一下输出效果:
2019-11-19 22:25:59,015 - log level info 可以看到我们的配置生效了,现在是按照我们的配置格式打印的日志。
logback-spring.xml 方式
如果你有更多样的配置需求,那么就需要使用外置 XML 文件的配置方式了。我们来看一个详细的配置示例:
<?xml version="1.0" encoding="UTF-8" ?>
<configuration>
<!-- 日志文件存放路径-->
<property name="PATH" value="/var/logs"/>
<!-- 彩色日志依赖的渲染类 -->
<conversionRule conversionWord="clr" converterClass="org.springframework.boot.logging.logback.ColorConverter"/>
<conversionRule conversionWord="wex"
converterClass="org.springframework.boot.logging.logback.WhitespaceThrowableProxyConverter"/>
<conversionRule conversionWord="wEx"
converterClass="org.springframework.boot.logging.logback.ExtendedWhitespaceThrowableProxyConverter"/>
<!-- 彩色日志格式 -->
<property name="CONSOLE_LOG_PATTERN"
value="${CONSOLE_LOG_PATTERN:-%clr(%d{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}}"/>
<!-- 文件日志格式 -->
<property name="FILE_LOG_PATTERN"
value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} -%msg%n"/>
<!-- 控制台输出配置-->
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
<!--日志输出格式-->
<layout class="ch.qos.logback.classic.PatternLayout">
<pattern>
${CONSOLE_LOG_PATTERN}
</pattern>
</layout>
</appender>
<!-- INFO 级别日志文件输出配置-->
<appender name="info" 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>
<!--当天日志文件名-->
<File>${PATH}/info.log</File>
<!--按天分割日志文件-->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!--历史日志文件名规则-->
<fileNamePattern>${PATH}/info.log.%d{yyyy-MM-dd}.%i</fileNamePattern>
<!--按大小分割同一天的日志-->
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>100MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
<!--日志文件保留天数-->
<maxHistory>30</maxHistory>
</rollingPolicy>
<!--日志输出格式-->
<layout class="ch.qos.logback.classic.PatternLayout">
<Pattern>${FILE_LOG_PATTERN}</Pattern>
</layout>
</appender>
<!-- ERROR 级别日志文件输出配置-->
<appender name="error" class="ch.qos.logback.core.rolling.RollingFileAppender">
......
</appender>
<!--日志级别-->
<root level="info">
<appender-ref ref="console"/>
<appender-ref ref="info"/>
<appender-ref ref="error"/>
</root>
</configuration>
配置了日志的输出路径,日志的输出格式,日志的滚动分割规则、保留时间和日志文件大小,还将不同级别的日志分别输出到相应的日志文件中。
日志格式变量介绍
- %level:表示输出日志级别 ;
- %date:表示日志发生时的时间,可缩写为 %d;
- %logger:用于输出 Logger 的类路径,包名+类名,{n}限定了输出长度,如果输出长度不够,尽可能显示类名、压缩包名;
- %thread:当前线程名;
- %M:日志发生时的方法名字;
- %L:日志调用所在代码行,线上运行时不建议使用此参数,因为获取代码行对性能有消耗;
- %m:日志、消息;
- %n:日志换行。
不要直接使用日志实现( Log4j、 Logback 等) 中的 API,应该使用日志门面框架 SLF4J 中的 API,使用门面模式的日志框架,有利于维护和各个类的日志处理方式统一。
使用占位符
不要使用以下这种字符串拼接的方式打印日志:
log.info("username: " + username + " IP: "+ ip + "platform: " + platform);
原因:
可读性差,变量越多会越差 当日志级别为 WARN 或 ERROR 时,该日志不打印,但仍然会进行字符串拼接,浪费资源。 应该使用如下占位符方式:
log.info("username: {} IP: {} platform: {}", username, ip, platform);
完整的堆栈信息
当发生异常时要将完整的堆栈信息打印出来,这样才能更准确的定位问题,而下面这种方式只会打印基本的错信息,比如只会告诉你发生了空指针,但你根本不知道发生在了哪里。
log.error("xxx错误:{}", e.getMessage());
正确的姿势:
log.error("xxx错误:{}", e.getMessage(), e);
日志最少保存两周以上
日志文件至少保存 15 天,因为有些异常具备以“周”为频次发生的特点。
禁止 System.out.println
System.out.println 只会将内容打印到控制台,不会输出到文件中。e.printStackTrace() 也是相同的效果。
正确的使用日志级别
正确合理的使用相应的日志级别打印相应的信息(日志级别的相关介绍参考上一节),如果程序发生了异常却使用 INFO 级别打印日志,那么在排查问题时会增加不必要的障碍。
使用 @Slf4j
推荐使用 Lombok 框架的 @Slf4j 注解开启日志,减少样板代码的编写提升效率,还能避免书写错误导致的问题。
总结
程序中的日志记录的程序运行的轨迹与状态,可以帮助我们很好的对程序进行分析与优化。