前言
记录应用系统曰志主要有三个原因 记录操作轨迹、监控系统运行状况、回溯系统故障。记录操作行为及操作轨迹数据,可以数据化地分析用户偏好,有助于优化业务逻辑,为用户提供个性化的服务。例如,通过 access.log 记录用户的操作频度和跳转链接,有助于分析用户的后续行为。
全面有效的日志系统有助于建立完善的应用监控体系,由此工程师可以实时监控系统运行状况,及时预警,避免故障发生。监控系统运行状况,是指对服务器使用状态,如内存、 CPU 等使用情况,应用运行情况 如响应时间 QPS 等交互状态;应用错误信息,如空指针、 SQL 异常等的监控。例如,在 CPU 使用率大于 60%, 四核服务器中load 大于4时发出报警,提醒工程师及时处理,避免发生故障。
当系统发生线上问题时,完整的现场日志有助于工程师快速定位问题。例如当系统内存溢出时,如果日志系统记录了问题发生现场的堆信息,就可以通过这个曰志分析是什么对象在大量产生并且没有释放内存,回溯系统故障,从而定位问题。
日志规范
推荐日志文件命名方式
推荐的日志文件命名方式为appName_logType_logName.log 其中 logType为日志类型,推荐分类有 stats monitor visit等, logName 为日志描述。这种命名的好处是通过文件名就可以知道曰志文件属于什么应用,什么类型 ,什么目的,也有利于归类查找。例如, mppserver 应用中单独监控时区转换异常的日志文件名定义为mppserver__monitor_timeZoneConvert.log
推荐曰志文件保存时间
代码规约推荐曰志文件至少保存15天,可以根据日志文件的重要程度、文件大小及磁盘空间再自行延长保存时间。
预先判断曰志级别
对DEBUG 、INFO 级别的日志,必须使用条件输出或者使用占位符的方式打印。该约定综合考虑了程序的运行效率和日志打印需求。例如 在某个配置了打印日志级别为WARN 的应用中,如果针对 DEBUG 级别的日志,仅仅在程序中写出
logger.debug(”Processing trade with id:” + id + ” and symbol:"+ symbol);
,那么该日志不会被打印但是会执行字符串拼接操作,如果 symbol 是对象 还会执行 toString() 方法白白浪费了系统资源。如下示例代码为正确的打印日志方式
//使明条件判断形式
if (logger.isDebugEnabled()) {
logger.debug ("Processing trade with id:" + id + "and symlbol:" + symbol) ;
//使用占位符形式
logger.debug ("Processing trade with id: {} and symbol: {}",id, symbol);
避免无效日志打印
生产环境禁止输出 DEBUG 曰志且有选择地输出 INFO日志。使用 INFO、WARN 级别来记录业务行为信息时,一定要控制日志输出量,以免磁盘空间不足。同时要为曰志文件设置合理的生命周期及时清理过期的日志。避免重复打印,务必在日志配置文件中设置 additivity=false
区别对待错误日志
WARN、ERROR 都是与错误有关的日志级别,但不要一发生错误就笼统地输出ERROR 级别日志。 一些业务异常是可以通过引导重试就能恢复正常的,例如用户输入参数错误。在这种情况下,记录日志是为了在用户咨询时可以还原现场,如果输出ERROR 级别就表示一旦出现就需要人为介入,这显然不合理。所以,ERROR只记录系统逻辑错误、异常或者违反重要的业务规则,其他错误都可以归为 WARN级别。
保证记录内容完整
曰志记录的内容包括现场上下文信息与异常堆栈信息,所以打印时需要注意以下两点:
- 记录异常时一定要输出异常堆栈,例如
logger.error("xxx" +e.getMessage(),e)
- 曰志中如果输出对象实例,要确保实例类重写了 toString()方法,否则只会输出对象的 hashCode 没有实际意义。
日志框架分类与选择
日志门面(日志的抽象层) | 日志实现 |
---|---|
JCL(Jakarta Commons Logging)(2014年后不再维护) jboss-logging (不适合企业项目开发使用) SLF4J(Simple Logging Facade for java) |
Log4j JUL(java.util.logging)(java.util.logging)(担心被抢市场,推出的) Log4j2( apache开发的很强大,借了log4j的名,但很多框架未适配上) Logback(Log4j同一个人开发的新框架,做了重大升级) |
日志门面
门面设计模式是面向对象设计模式中的一种,日志框架采用的就是这种模式,类似JDBC 的设计理念。它只提供一套接口规范,自身不负责日志功能的实现。目的是让使用者不需要关注底层具体是哪个日志库来负责日志打印及具体的使用细节等。目前用得最为广泛的曰志门面有两种 slf4j和commons -logging
日志库
负责实现日志相关功能,主流日志库有三个,分别为:log4j、log-jdk(java.util.logging.Logger)、logback。logback是最晚出现的,与log4j同一个作者,是log4j的升级版且本身实现了slf4j的接口。
日志适配器
分为:日志门面适配器(日志库适配slf4j),日志库适配器(slf4j适配日志库)。
-
日志门面适配器
老工程用的日志库没有实现slf4j接口,如log4j;这时候工程里想使用slf4j+log4j的模式,就额外需要一个适配器(slf4j+log4j12)来解决接口不兼容问题
-
日志库适配器
老工程直接使用日志库API完成日志打印,要改成业界标准的门面模式(如slf4j+logback),但是老工程代码打印日志地方太多难以改动,这是就需要一个适配器来完成从旧日志库的API到slf4j的路由,这样在不改动原有代码的情况下也能使用slf4j来统一管理日志(如:log4j-over-slf4j),后续自由替换具体日志库也不成问题。
Spring Boot 采用了 slf4j+logback 的组合形式,Spring Boot也提供对JUL、log4j2、Logback提供了默认配置
SpringBoot默认日志配置
新建springboot项目,引入web启动项,其他默认即可
package com.lzy.logdemo.controller;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @Author: Luzy
* @Date: 2020-07-17 10:16
* @Description:
*/
@RequestMapping
@RestController
public class LogController {
private static final Logger logger = LoggerFactory.getLogger(LogController.class);
@GetMapping("/log")
public String logTest1(String name) {
// 由低到高:trace < debug < info < warn < error
//2. Spring Boot默认设定的是 info 级别日志,(日志默认级别也称为root级别)。可修改默认级别日志:logging.level.root=级别名
//3. 可以进行调整日志级别,设定某个级别后,就只打印设定的这个级别及后面高级别的日志信息。没有指定级别的就用SpringBoot默认规定的级别:root级别
//4. 可修改指定包的日志级别:指定某个包下面的所有日志级别:logging.level.包名=级别名
logger.info("------------info--------------{}",name);
logger.error("------------error--------------{}",name);
logger.debug("------------debug--------------{}",name);
logger.trace("------------trace--------------{}",name);
logger.warn("------------warn--------------{}",name);
return "log test..."+name;
}
}
url输入http://localhost:8080/log?name=lzy,控制台输出
修改日志默认配置
修改日志文件生成路径
logging.file.name | logging.file.path | 示例 | 说明 |
---|---|---|---|
(none) | (none) | 只在控制台输出 | |
指定文件名 | (none) | demo.log | 输出到当前项目根路径下的 demo.log 文件中 |
(none) | 指定目录 | logs/log_lzy | 输出到当前项目所在磁盘根路径下的/logs/log_lzy目录中的 spring.log 文件中 |
指定文件名 | 指定目录 | 当两个同时指定时,采用的是logging.file.name 指定。推荐使用logging.file.name 设置即可,因为它可自定义文件名 |
logging:
file:
name: demo.log
# path: logs/log_lzy
修改日志输出格式
logging:
file:
name: demo.log
pattern:
console: '%clr(%d{yyyy-MM-dd} [%thread] %-5level %logger{50} - %msg%n)'
file: '%d{yyyy-MM-dd HH:mm:ss.SSS} >>> [%thread] >>> %-5level >>> %logger{50} >>> %msg%n'
# path: logs/log_lzy
注意:如上,yml文件中首尾加上单引号可解决识别不了%的问题,properties不需要加
分析日志底层实现
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
spring-boot-starter-web 中引入了 spring-boot-starter 启动器
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<version>2.3.1.RELEASE</version>
<scope>compile</scope>
</dependency>
spring-boot-starter 中引入了 spring-boot-starter-logging 日志启动器
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
<version>2.3.1.RELEASE</version>
<scope>compile</scope>
</dependency>
spring-boot-starter-logging 日志启动器 采用的是 logback 日志框架
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<scope>compile</scope>
</dependency>
总结:SpringBoot中默认日志启动器为 spring-boot-starter-logging ,默认采用的是 logback 日志框架
在 spring-boot-2.3.1.RELEASE.jar! orgspringframeworkootlogginglogbackase.xml 做了日志的默认配置
<included>
<!--日志格式默认规定-->
<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" /> <!--日志级别默认为: info -->
<root level="INFO">
<appender-ref ref="CONSOLE" />
<appender-ref ref="FILE" />
</root>
</included>
日志文件采用方式为:滚动文件追加器
在下面类中会读取上面xml中配置的信息
如果spring boot的日志功能无法满足我们的需求(比如异步日志记录等),我们可以自已定义的日志配置文件
自定义日志配置
自定义Logback日志配置
在类路径下,存放对应日志框架的自定义配置文件即可;SpringBoot就不会使用它默认的日志配置文件了
Logging System | Customization |
---|---|
Logback | logback-spring.xml , logback-spring.groovy , logback.xml , or logback.groovy |
Log4j2 | log4j2-spring.xml or log4j2.xml |
JDK (Java Util Logging) | logging.properties |
logback.xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration debug="false" scan="false" scanPeriod="60 seconds">
<property name="LOG_HOME" value="./logs/logback"/>
<property name="appName" value="lzy-logDemo"/>
<!-- 定义控制台输出 -->
<appender name="stdout" class="ch.qos.logback.core.ConsoleAppender">
<layout class="ch.qos.logback.classic.PatternLayout">
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} - [%thread] - %-5level - %logger{50} - %msg%n</pattern>
</layout>
</appender>
<appender name="appLogAppender" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- 指定日志文件的名称 -->
<file>${LOG_HOME}/${appName}.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${LOG_HOME}/${appName}-%d{yyyy-MM-dd}-%i.log</fileNamePattern>
<MaxHistory>30</MaxHistory>
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<MaxFileSize>10MB</MaxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
</rollingPolicy>
<layout class="ch.qos.logback.classic.PatternLayout">
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [ %thread ] - [ %-5level ] [ %logger{50} : %line ] - %msg%n</pattern>
</layout>
</appender>
<!-- 日志输出级别 -->
<logger name="org.springframework" level="debug" additivity="false"/>
<logger name="com.lzy.logdemo" level="debug"/>
<root level="INFO">
<appender-ref ref="stdout"/>
<appender-ref ref="appLogAppender"/>
</root>
</configuration>
关于logback.xml与logback-spring.xml的说明
logback.xml :是直接就被日志框架加载了。
logback-spring.xml:配置项不会被日志框架直接加载,而是由 SpringBoot 解析日志配置文件
logback.xml加载早于application.properties,所以如果你在logback.xml使用了变量时,而恰好这个变量是写在application.properties时,那么就会获取不到,只要改成logback-spring.xml就可以解决。
官网上说明如下:
因为logback-spring.xml是由 SpringBoot 解析日志配置文件,故可以使用SpringBoot 的 Profifile 特殊配置
logback-spring.xml 使用 Profile 特殊配置
指定运行环境: --spring.profifiles.active=dev
注意
若使用 logback.xml 作为日志配置文件,还指定 Profifile 特殊配置,则会有以下错误,便也证明前面所说logback.xml是直接就被日志框架加载。
更换为log4j2日志实现
修改pom依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions><!-- 去掉springboot默认配置 -->
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency> <!-- 引入log4j2依赖 -->
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>
配置文件
<?xml version="1.0" encoding="UTF-8"?>
<!--Configuration后面的status,这个用于设置log4j2自身内部的信息输出,可以不设置,当设置成trace时,你会看到log4j2内部各种详细输出-->
<!--monitorInterval:Log4j能够自动检测修改配置 文件和重新配置本身,设置间隔秒数-->
<configuration monitorInterval="5">
<!--日志级别以及优先级排序: OFF > FATAL > ERROR > WARN > INFO > DEBUG > TRACE > ALL -->
<!--变量配置-->
<Properties>
<!-- 格式化输出:%date表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度 %msg:日志消息,%n是换行符-->
<!-- %logger{36} 表示 Logger 名字最长36个字符 -->
<property name="LOG_PATTERN" value="%date{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n" />
<!-- 定义日志存储的路径 -->
<property name="FILE_PATH" value="./logs/log4j2" />
<property name="FILE_NAME" value="lzy-log4j2-demo" />
</Properties>
<appenders>
<console name="Console" target="SYSTEM_OUT">
<!--输出日志的格式-->
<PatternLayout pattern="${LOG_PATTERN}"/>
<!--控制台只输出level及其以上级别的信息(onMatch),其他的直接拒绝(onMismatch)-->
<ThresholdFilter level="info" onMatch="ACCEPT" onMismatch="DENY"/>
</console>
<!--文件会打印出所有信息,这个log每次运行程序会自动清空,由append属性决定,适合临时测试用-->
<File name="Filelog" fileName="${FILE_PATH}/test.log" append="false">
<PatternLayout pattern="${LOG_PATTERN}"/>
</File>
<!-- 这个会打印出所有的info及以下级别的信息,每次大小超过size,则这size大小的日志会自动存入按年份-月份建立的文件夹下面并进行压缩,作为存档-->
<RollingFile name="RollingFileInfo" fileName="${FILE_PATH}/info.log" filePattern="${FILE_PATH}/${FILE_NAME}-INFO-%d{yyyy-MM-dd}_%i.log.gz">
<!--控制台只输出level及以上级别的信息(onMatch),其他的直接拒绝(onMismatch)-->
<ThresholdFilter level="info" onMatch="ACCEPT" onMismatch="DENY"/>
<PatternLayout pattern="${LOG_PATTERN}"/>
<Policies>
<!--interval属性用来指定多久滚动一次,默认是1 hour-->
<TimeBasedTriggeringPolicy interval="1"/>
<SizeBasedTriggeringPolicy size="10MB"/>
</Policies>
<!-- DefaultRolloverStrategy属性如不设置,则默认为最多同一文件夹下7个文件开始覆盖-->
<DefaultRolloverStrategy max="15"/>
</RollingFile>
<!-- 这个会打印出所有的warn及以下级别的信息,每次大小超过size,则这size大小的日志会自动存入按年份-月份建立的文件夹下面并进行压缩,作为存档-->
<RollingFile name="RollingFileWarn" fileName="${FILE_PATH}/warn.log" filePattern="${FILE_PATH}/${FILE_NAME}-WARN-%d{yyyy-MM-dd}_%i.log.gz">
<!--控制台只输出level及以上级别的信息(onMatch),其他的直接拒绝(onMismatch)-->
<ThresholdFilter level="warn" onMatch="ACCEPT" onMismatch="DENY"/>
<PatternLayout pattern="${LOG_PATTERN}"/>
<Policies>
<!--interval属性用来指定多久滚动一次,默认是1 hour-->
<TimeBasedTriggeringPolicy interval="1"/>
<SizeBasedTriggeringPolicy size="10MB"/>
</Policies>
<!-- DefaultRolloverStrategy属性如不设置,则默认为最多同一文件夹下7个文件开始覆盖-->
<DefaultRolloverStrategy max="15"/>
</RollingFile>
<!-- 这个会打印出所有的error及以下级别的信息,每次大小超过size,则这size大小的日志会自动存入按年份-月份建立的文件夹下面并进行压缩,作为存档-->
<RollingFile name="RollingFileError" fileName="${FILE_PATH}/error.log" filePattern="${FILE_PATH}/${FILE_NAME}-ERROR-%d{yyyy-MM-dd}_%i.log.gz">
<!--控制台只输出level及以上级别的信息(onMatch),其他的直接拒绝(onMismatch)-->
<ThresholdFilter level="error" onMatch="ACCEPT" onMismatch="DENY"/>
<PatternLayout pattern="${LOG_PATTERN}"/>
<Policies>
<!--interval属性用来指定多久滚动一次,默认是1 hour-->
<TimeBasedTriggeringPolicy interval="1"/>
<SizeBasedTriggeringPolicy size="10MB"/>
</Policies>
<!-- DefaultRolloverStrategy属性如不设置,则默认为最多同一文件夹下7个文件开始覆盖-->
<DefaultRolloverStrategy max="15"/>
</RollingFile>
</appenders>
<!--Logger节点用来单独指定日志的形式,比如要为指定包下的class指定不同的日志级别等。-->
<!--然后定义loggers,只有定义了logger并引入的appender,appender才会生效-->
<loggers>
<!--过滤掉spring和mybatis的一些无用的DEBUG信息-->
<logger name="org.mybatis" level="info" additivity="false">
<AppenderRef ref="Console"/>
</logger>
<!--监控系统信息-->
<!--若是additivity设为false,则 子Logger 只会在自己的appender里输出,而不会在 父Logger 的appender里输出。-->
<Logger name="org.springframework" level="info" additivity="false">
<AppenderRef ref="Console"/>
</Logger>
<root level="info">
<appender-ref ref="Console"/>
<appender-ref ref="Filelog"/>
<appender-ref ref="RollingFileInfo"/>
<appender-ref ref="RollingFileWarn"/>
<appender-ref ref="RollingFileError"/>
</root>
</loggers>
</configuration>
运行结果如下
补充
若日志配置文件名字随意取得,需要在springboot配置文件yml中指明:
logging:
file:
name: demo.log
pattern:
console: '%clr(%d{yyyy-MM-dd} [%thread] %-5level %logger{50} - %msg%n)'
file: '%d{yyyy-MM-dd HH:mm:ss.SSS} >>> [%thread] >>> %-5level >>> %logger{50} >>> %msg%n'
# path: logs/log_lzy
config: classpath:logconfig.xml
至此,SpringBoot整合日志已总结完毕,后续若有补充再更新。
参考资料:
Spring官网 https://docs.spring.io/spring-boot/docs/2.3.1.RELEASE/reference/htmlsingle/#boot-features-logging
《码出高效》第五章节:异常与日志