公司的项目不会有 e.printStackTrace(); 这种代码的存在。因为这打印出来的错误信息没有日期、等级等等,分析起来不方便。再比如分布式系统中需要使用 trackID 来追踪问题,我们难道要每次打日志的时候都将 trackID 记录一下吗。像这种公共的东西,我们可以抽出一个框架来处理这些问题
为什么要用 SLF4J
在阿里开发手册有这么一条:
【强制】应用中不可直接使用日志系统(Log4j、Logback)中的 API,而应依赖使用日志框架 SLF4J 中的 API,使用门面模式的日志框架,有利于维护和各个类的日志处理方式统一
slf4j ,即 Simple Logging Facade for Java,简单门面日志。它是对所有日志框架制定的一种规范、标准、接口,并不是一个框架的具体的实现,它只服务于各种各样的日志系统
slf4j 提供了统一的记录日志的接口,对不同日志系统的具体实现进行了抽象化,只要按照其提供的方法记录即可,最终日志的格式、记录级别、输出方式等通过绑定具体的日志系统来实现。在项目中使用了 slf4j 记录日志,并且绑定了 log4j(pom.xml 中配置了相应的 jar 包依赖),则日志会以 log4j 的风格输出;后期需要改为以 logback 的风格输出日志,只需要将 jar 包 log4j 替换成 logback 即可,根本不需要修改日志文件的代码
建议统一使用 Slf4j 作为日志门面,logback 作为日志实现
日志级别
日志常用的五个级别,根据严重程度由低到高,依次为:debug(调试 ) < info(消息) < warn(警告) < error(错误) < fatal(严重错误)。通常可以根据实际所需要的颗粒度的大小选择其中的几个,当前常用 debug,info,warn,error 4个级别
在项目的 yaml 文件中可以配置日志级别
logging:
level:
com.xyz.spring: trace
logging.level 用来指定具体的包中应用程序日志的输出级别。上面的配置表示 com.xyz.spring 包下的所有日志输出级别为 trace,该级别会将操作数据库的 sql 打印出来,开发时设置成 trace 方便定位问题,在生产环境上,将这个日志级别再设置成 error 级别即可
Springboot 中日志的配置
Springboot 启动器中已经集成了 slf4j,就是 spring-boot-starter-web,因此无需手动导包,如果需要,可以使用以下配置
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.30</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.30</version>
</dependency>
application.yml 的日志配置
logging:
config: logback.xml
level:
com.xyz: trace
config 表示读取日志配置的地点,将 locback.xml 放在 pom 文件同级包下即可。level 表示在 XXX 包下的日志输出级别,配置文件读取步骤:
- 尝试在 classpath 下查找文件 logback-test.xml
- 如果 logback-test.xml 文件不存在,尝试在 classpath 下查找 logback.xml
- 如果两个文件都不存在,LogBack 用 BasicConfiguration 自动对自己进行最小化配置,这样不需要添加任何配置就可以输出日志信息
以下的 xml 文件只需要更改部分项目地址就能直接使用
<?xml version="1.0" encoding="UTF-8"?>
<configuration debug="false">
<!--定义日志文件的存储地址 勿在 LogBack 的配置中使用相对路径-->
<property name="LOG_HOME" value="D:IdeaProjects/SpringBoot-Item/springboot-slf4j/log"/>
<!-- 定义日志格式 -->
<property name="LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%-5level] [%thread] [%-30.30logger{30}] %msg%n"/>
<!-- 控制台输出 -->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符-->
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
</encoder>
</appender>
<!-- 按照每天生成日志文件 -->
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!--日志文件输出的文件名-->
<FileNamePattern>${LOG_HOME}/SpringBoot-Slf4j_%d{yyyy-MM-dd}.log</FileNamePattern>
<!--日志文件保留天数-->
<MaxHistory>30</MaxHistory>
</rollingPolicy>
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符-->
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
</encoder>
<!--日志文件最大的大小-->
<triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
<MaxFileSize>10MB</MaxFileSize>
</triggeringPolicy>
</appender>
<!-- 日志输出级别 -->
<logger name="org.springframework" level="INFO"/>
<logger name="com" level="INFO"/>
<root level="INFO">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="FILE"/>
</root>
</configuration>
在使用日志的时候,在该类中加入一行代码:
private Logger logger = LoggerFactory.getLogger(UserController.class);
或者在类之前加上 lombak 的注解
@Slf4j
日志的最佳实践
不要随便打日志,你可能觉得奇怪,打个日志难道会出什么故障码。但是线上确确实实因为日志出现过问题,下面来列举几个:
- 线上机器 OOM。经过排查发现是因为堆外内存溢出,再排查发现是因为打了很多大 POJO 组成的 list,转化成 json 直接打印了
- 报警不断。然鹅大部分的报警都是业务异常,比如用户无权限、访问 db 查不到数据等等,这些简单的问题不应该被我们监控到,因此日志打的不好会造成报警噪音特别大
那么我们应当遵守什么规则来打日志呢,以下是一些最佳实践:
1,系统/应用启动和参数变更。当系统启动时,可以将相关的参数信息进行打印,以便出现问题时,更准确地查询原因;参数信息可能并不存储在本地,需要通过配置中心获取,而参数信息有变更时,也需要将变更后的内容输出在日志中
2,关键操作节点与关键方法入参出参。最典型的就是在关键位置添加日志,记录用户进行的某个操作,记录关键方法的话,我们可以使用 AOP 写个注解,还能随便监控一下方法耗时
3,异常。如果是通过 try-catch 处理,需要注意日志编写的位置。如果需要将异常在本层抛出,则不需要进行日志记录,否则会出现日志重复的问题,如果打了日志,记得将异常转化成合理返回值。同时,应当注意将业务异常日志打成 info,因为很多监控系统会收集 error 的日志次数然后给你报警,但是业务异常算异常吗,至少它不应该由我们来处理
4,尽可能不要在日志中打太多数据,尤其不要把一个 pogo list 转化成 json 打进去,一般的日志实现是会 OOM 的。虽然打印的日志日志没有使用虚拟机空间,为了减少数据复制次数使用零拷贝技术放在了堆外空间,但是堆外空间也可能出现装不下的问题。我们可以将一个 pojo 拆成多个重要信息打印