文章目录
- 1. 前言
- 2. 日志框架选型
- 3. SpringBoot默认的日志实现框架(Logback)
- 4. 如何使用日志框架
- 5. 切换日志的具体实现框架(从Logback切换成Log4j2)
- 6. 在SpringBoot项目中如何编写与日志相关的配置
- 7. 补充:Logback配置文件的命名规范
- 8. 记录日志的最佳实践
- 9. 参考文章&参考视频
1. 前言
日志是我们系统出现错误时,最快速有效的定位工具,没有日志给出的错误信息,遇到报错就会一脸懵逼
日志还可以用来记录业务信息,比如记录用户执行的每个操作,不仅可以用于分析改进系统,在遇到非法操作时也能很快找到凶手
对于程序员来说,日志记录是重要的基本功,但很多同学并没有系统学习过日志操作、缺乏使用日志的经验,所以我写下这篇文章,分享自己对日志的一些理解,希望对大家有帮助
日志不是写给机器看的,而是写给未来的你和你的队友看的
2. 日志框架选型
2.1 System.out.println
相信很多同学都是通过 System.out.println
输出信息来调试程序的,但是 System.out.println
存在很严重的问题:
System.out.println
是一个同步方法,每次调用都会导致 I/O 操作,比较耗时,频繁使用甚至会严重影响应用程序的性能,不建议在生产环境使用System.out.println
只能输出简单的信息到标准控制台,无法灵活设置日志级别、格式、输出位置等
我们一般会选择专业的 Java 日志框架或工具库,比如经典的 Apache Log4j 和它的升级版 Log4j 2,还有 Spring Boot 默认集成的 Logback 库
借助专业的 Java 日志框架,我们可以用一行代码快速地完成日志记录,还能灵活调整格式、设置日志级别、将日志写入到文件中、压缩日志等
2.2 SLF4J
SLF4J:Simple Logging Facade for Java
SLF4J 并不是一个具体的日志实现,而是为各种日志框架提供简单统一接口的日志门面(抽象层)
简单来说,我们只需要使用 Slf4j 提供的抽象方法,就能够实现记录日志的功能,日志框架底层的具体实现取决于引入的 jar 包和配置
2.2.1 Log4j(已停止维护,不再介绍)
Log4j(已停止维护,不再介绍)
2.2.2 LogBack&Log4j2
- 从稳定性来说,虽然这些 LogBack 和 Log4j2 都被曝出过漏洞,但 Log4j2 的漏洞更为致命
- 从易用性来说,二者差不多,但 Logback 是 SLF4J 的原生实现、Log4j2 需要进行额外的配置
Spring Boot 默认集成了 Logback,如果没有特殊需求,更推荐初学者选择 Logback
2.3 扩展:日志框架背后的故事
俄罗斯程序员 Ceki 在 2001 年的时候写下了 Log4j 日志框架,慢慢地被很多人熟知
时机成熟之后,作者将 Log4j 捐献给了 Apache 软件基金会,本来应该是一个双赢的结果,但 Apache 软件基金会并没有将 Log4j 管理得很好,于是 Ceki 就不再维护 Log4j,直接开发出了更好用的 Logback,并写了一个日志的门面框架——Slf4j
Logback 的出现让 Log4j 慢慢变得没人使用,于是 Apache 软件基金会慢慢停止更新 Log4j
通过改进 Log4j 的缺点,参考 Logback 的优点,Apache 基金会开发出了 Log4j2
3. SpringBoot默认的日志实现框架(Logback)
我们在 IDEA 中打开项目的 pom.xml 文件,右键打开菜单,查看依赖关系图
按下 CTRL + F
快捷键,搜索 logging 关键字
可以发现,SpringBoot 最基础的场景启动器就包括了日志的场景启动器(CTRL + 鼠标滚轮
可以缩放图表,长按鼠标右键可以移动视图)
日志的场景启动器选择 Logback 作为日志实现框架
4. 如何使用日志框架
日志框架的使用非常简单,一般是先获取到 Logger 日志对象,然后调用 log.xxx(比如 log.debug、log.info、log.error)就能输出日志了
4.1 常规方法
常规方法是通过 LoggerFactory 手动获取 Logger,示例代码如下
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;
@RestController
@RequestMapping
public class LogbackController {
private static final Logger log = LoggerFactory.getLogger(LogbackController.class);
@GetMapping("testLogback")
public void testLogback() {
log.info("testLogback");
}
}
上述代码中,我们通过调用日志工厂并传入当前类,创建了一个 Logger 对象,但由于每个类的类名都不同,我们又经常复制这行代码到不同的类中,就很容易忘记修改类名
所以我们可以使用 this.getClass
动态获取当前类的实例,来创建 Logger 对象(此时的 Logger 对象不能再用 static 修饰)
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;
@RestController
@RequestMapping
public class LogbackController {
private final Logger log = LoggerFactory.getLogger(this.getClass());
@GetMapping("testLogback")
public void testLogback() {
log.error("testLogback");
}
}
给每个类都复制一遍这行代码,就能愉快地打日志了
但这样做还是有点麻烦,大部分同学连复制粘贴都懒得做,怎么办
4.2 使用 Lombok 工具库提供的 @Slf4j 注解
还有更简单的方式,使用 Lombok 工具库提供的 @Slf4j 注解,可以自动为当前类生成一个名为 log 的 SLF4J Logger 对象,简化了 Logger 的定义过程,示例代码如下
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping
@Slf4j
public class LogbackController {
@GetMapping("testLogback")
public void testLogback() {
log.error("testLogback");
}
}
4.3 @Slf4j注解的原理
其实 @Slf4j 注解本质上用的还是常规方法,在 target 目录下生成的字节码中可以找到答案
5. 切换日志的具体实现框架(从Logback切换成Log4j2)
Logback 和 Log4j2 是市面上性能较好的两个日志框架,所以本次演示是从 Logback 切换成 Log4j2
5.1 排除掉与Logback相关的依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<exclusions>
<exclusion>
<artifactId>spring-boot-starter-logging</artifactId>
<groupId>org.springframework.boot</groupId>
</exclusion>
</exclusions>
</dependency>
5.2 添加Log4j2的场景启动器
Spring 官方已经为我们提供了 Log4j2 的场景启动器
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>
6. 在SpringBoot项目中如何编写与日志相关的配置
下面以 Logback 为例,演示在 SpringBoot 项目中如何编写与日志相关的配置
6.1 通过application.yml文件配置(只能编写简单的配置,不推荐使用)
在 application.yml
文件中配置以下内容
# 日志配置
logging:
level:
root: INFO # 设置根日志级别
cn.edu.scau: INFO # 为你的包或类设置特定的日志级别
pattern:
console: "%d{yyyy-MM-dd HH:mm:ss} - %msg%n" # 控制台日志的输出格式
file: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n" # 文件日志的输出格式
file:
path: logs # 日志文件存储路径, 如果不指定,默认为当前目录
name: ${logging.file.path}/app.log # 日志文件名
# config: classpath:logback-spring.xml # 如果需要,可以指定一个Logback配置文件
# 配置Logback的滚动策略
logback:
rollingpolicy:
max-file-size: 10MB # 单个日志文件的最大大小
max-history: 30 # 保留的日志文件的最大数量
clean-history-on-start: false # 应用启动时是否清理旧的日志文件
file-name-pattern: app-%d{yyyy-MM-dd}.%i.log # 日志文件名格式,包括日期和索引
6.2 通过logback.xml配置文件配置(能够编写详细的配置,推荐使用)
logback.xml 配置文件存放在 resources 目录下,用于配置与日志相关的详细信息
6.3 logback.xml文件(logback-spring.xml文件)详解
logback.xml 文件的配置比较复杂,不需要特别记忆,需要用到的时候再查询就可以了
6.3.1 configuration标签
整个配置文件的父标签,无特别含义
6.3.2 include标签
可以引入其它配置文件,例如引入 Spring 提供的 Logback 的默认配置文件
<include resource="org/springframework/boot/logging/logback/defaults.xml"/>
6.3.3 property标签
用于定义变量,定义的变量可以在配置文件的其它地方直接使用
<!-- 定义应用程序名称 -->
<property name="APP_NAME" value="YOUR_APP_NAME"/>
<!-- 定义日志文件的存储路径 -->
<property name="LOG_FILE_PATH" value="logs"/>
<!-- 定义日志文件的文件名,通过拼接日志文件的存储路径、应用程序名称、log扩展名来创建完整的日志文件 -->
<property name="LOG_FILE_NAME" value="${LOG_FILE_PATH}/${APP_NAME}.log"/>
6.3.4 appender标签
通过 appender 标签可以
- 定义日志输出位置:通过配置不同的appender,你可以指定日志信息应该被发送到哪里。例如,你可以配置一个
ConsoleAppender
来在控制台输出日志,或者配置一个FileAppender
来将日志写入文件 - 设置日志格式:每个appender都可以有一个与之关联的
<encoder>
,用于定义日志的格式。这包括日志消息的布局、日期格式等 - 过滤日志事件:Appender可以包含
<filter>
标签,用于根据特定的条件过滤日志事件。例如,你可以设置只记录特定级别的日志或者排除某些特定的日志消息 - 设置日志滚动策略:对于文件输出,appender可以配置滚动策略(如
RollingFileAppender
),这决定了何时和如何创建新的日志文件。例如,基于文件大小(SizeBasedTriggeringPolicy
)或时间(TimeBasedRollingPolicy
)
6.3.4.1 控制台输出配置
<!-- Console 输出配置 -->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<!-- 编码器配置,用于格式化日志输出 -->
<encoder>
<!-- 定义日志消息的输出格式,包括日期、进程ID、线程名、日志级别、日志器名和日志消息 -->
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%processId] [%thread] %-5level %logger{36} - %msg%n</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
6.3.4.2 文件输出配置
<!-- File 输出配置 -->
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- 编码器配置,用于格式化日志输出 -->
<encoder>
<!-- 定义日志消息的输出格式,包括日期、进程ID、线程名、日志级别、日志器名和日志消息 -->
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%processId] [%thread] %-5level %logger{36} - %msg%n</pattern>
<charset>UTF-8</charset>
</encoder>
<!-- 滚动策略配置,基于时间的滚动策略 -->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- 定义滚动日志文件的命名模式,文件名包含日期,并且每天生成一个新的日志文件 -->
<fileNamePattern>${LOG_FILE_NAME}.%d{yyyy-MM-dd}.log</fileNamePattern>
</rollingPolicy>
</appender>
6.3.4.3 异步输出配置
<!-- 异步输出配置 -->
<appender name="ASYNC" class="ch.qos.logback.classic.AsyncAppender">
<!-- 队列大小,定义了在内存中存储日志事件的队列的最大容量 -->
<queueSize>512</queueSize> <!-- 队列大小 -->
<!-- 丢弃阈值,当队列剩余容量小于这个值时,将丢弃低于指定级别的日志事件 -->
<!-- 0 表示不丢弃任何级别的日志事件 -->
<discardingThreshold>0</discardingThreshold> <!-- 丢弃阈值,0 表示不丢弃 -->
<!-- 队列满时是否阻塞主线程,true 表示即使队列满了也不会阻塞主线程 -->
<!-- 这可能会导致日志事件丢失 -->
<neverBlock>true</neverBlock> <!-- 队列满时是否阻塞主线程,true 表示不阻塞 -->
<!-- 指定异步日志输出要引用的appender,此处只能引用一个appender,如果引用了多个appender,只有第一个appender生效 -->
<!-- 异步appender会将日志事件转发给这里指定的appender -->
<appender-ref ref="FILE"/> <!-- 生效的日志目标 -->
</appender>
当 neverBlock
设置为 true
时,即使队列满了,AsyncAppender
也不会阻塞主线程。相反,它会丢弃新的日志事件,并且通常会记录一条警告信息,表明有日志事件因为队列满而被丢弃。以下是这种行为可能导致日志事件丢失的原因:
- 队列满:如果队列满了,而新的日志事件继续产生,
AsyncAppender
会根据neverBlock
的设置来决定是否丢弃这些事件 - 不阻塞主线程:当
neverBlock
设置为true
时,如果队列满了,AsyncAppender
会选择不阻塞主线程,而是直接丢弃新的日志事件 - 性能考虑:在某些情况下,应用程序的性能比日志完整性更重要。例如,在极端负载情况下,为了保持应用程序的响应性,可能会选择丢弃一些日志事件
如果你不希望丢失任何日志事件,应该将 neverBlock
设置为 false
(或者不设置,因为这是默认值)。但是,请注意,这可能会在日志队列满时导致主线程阻塞,从而影响应用程序的整体性能
6.3.5 root标签
<!-- 设置日志级别为INFO,即INFO级别及以上的日志事件才会被处理 -->
<root level="INFO">
<!-- 引用名为CONSOLE的appender,将日志输出到控制台 -->
<appender-ref ref="CONSOLE"/>
<!-- 引用名为FILE的appender,将日志输出到文件 -->
<!--<appender-ref ref="FILE"/>-->
<!-- 引用名为ASYNC的异步appender,提高日志记录性能 -->
<appender-ref ref="ASYNC"/>
</root>
6.3.6 logger标签
logger 标签可以精确到类或者包的日志输出级别,优先级最高
<!-- 配置特定包的日志级别 -->
<!-- 设置cn.edu.scau包下的所有类的日志级别为INFO -->
<logger name="cn.edu.scau" level="info"/>
<!-- 配置特定类的日志级别为WARN -->
<!-- 这将覆盖默认的日志级别设置,只记录WARN及以上级别的日志 -->
<logger name="org.apache.sshd.common.util.SecurityUtils" level="WARN"/>
6.4 完整的logback.xml配置文件
<?xml version="1.0" encoding="UTF-8"?>
<configuration debug="true">
<include resource="org/springframework/boot/logging/logback/defaults.xml"/>
<!-- 定义应用程序名称 -->
<property name="APP_NAME" value="YOUR_APP_NAME"/>
<!-- 定义日志文件的存储路径 -->
<property name="LOG_FILE_PATH" value="logs"/>
<!-- 定义日志文件的文件名,通过拼接日志文件的存储路径、应用程序名称、log扩展名来创建完整的日志文件 -->
<property name="LOG_FILE_NAME" value="${LOG_FILE_PATH}/${APP_NAME}.log"/>
<!-- Console 输出配置 -->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<!-- 编码器配置,用于格式化日志输出 -->
<encoder>
<!-- 定义日志消息的输出格式,包括日期、进程ID、线程名、日志级别、日志器名和日志消息 -->
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [${PID:- }] [%thread] %-5level %logger{36} - %msg%n</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
<!-- File 输出配置 -->
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- 编码器配置,用于格式化日志输出 -->
<encoder>
<!-- 定义日志消息的输出格式,包括日期、进程ID、线程名、日志级别、日志器名和日志消息 -->
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [${PID:- }] [%thread] %-5level %logger{36} - %msg%n</pattern>
<charset>UTF-8</charset>
</encoder>
<!-- 滚动策略配置,基于时间的滚动策略 -->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- 定义滚动日志文件的命名模式,文件名包含日期,并且每天生成一个新的日志文件 -->
<fileNamePattern>${LOG_FILE_NAME}.%d{yyyy-MM-dd}.log</fileNamePattern>
</rollingPolicy>
</appender>
<!-- 异步输出配置 -->
<appender name="ASYNC" class="ch.qos.logback.classic.AsyncAppender">
<!-- 队列大小,定义了在内存中存储日志事件的队列的最大容量 -->
<queueSize>512</queueSize> <!-- 队列大小 -->
<!-- 丢弃阈值,当队列剩余容量小于这个值时,将丢弃低于指定级别的日志事件 -->
<!-- 0 表示不丢弃任何级别的日志事件 -->
<discardingThreshold>0</discardingThreshold> <!-- 丢弃阈值,0 表示不丢弃 -->
<!-- 队列满时是否阻塞主线程,true 表示即使队列满了也不会阻塞主线程 -->
<!-- 这可能会导致日志事件丢失 -->
<neverBlock>true</neverBlock> <!-- 队列满时是否阻塞主线程,true 表示不阻塞 -->
<!-- 指定异步日志输出要引用的appender,此处只能引用一个appender,如果引用了多个appender,只有第一个appender生效 -->
<!-- 异步appender会将日志事件转发给这里指定的appender -->
<appender-ref ref="FILE"/> <!-- 生效的日志目标 -->
</appender>
<!-- 设置日志级别为INFO,即INFO级别及以上的日志事件才会被处理 -->
<root level="INFO">
<!-- 引用名为CONSOLE的appender,将日志输出到控制台 -->
<appender-ref ref="CONSOLE"/>
<!-- 引用名为FILE的appender,将日志输出到文件 -->
<!--<appender-ref ref="FILE"/>-->
<!-- 引用名为ASYNC的异步appender,提高日志记录性能 -->
<appender-ref ref="ASYNC"/>
</root>
<!-- 配置特定包的日志级别 -->
<!-- 设置cn.edu.scau包下的所有类的日志级别为INFO -->
<logger name="cn.edu.scau" level="INFO"/>
<!-- 配置特定类的日志级别为WARN -->
<!-- 这将覆盖默认的日志级别设置,只记录WARN及以上级别的日志 -->
<logger name="org.apache.sshd.common.util.SecurityUtils" level="WARN"/>
</configuration>
6.5 补充:如何禁止控制台输出日志
application.yml 配置文件只能简单地设置日志的输出级别和输出路径,禁止输出到控制台只能在 logback.xml 中进行配置
在 root 标签中排除掉 CONSOLE 标签就可以了
6.6 补充:SizeAndTimeBasedRollingPolicy 策略
SizeAndTimeBasedRollingPolicy 是基于时间和日志文件大小的滚动策略
<!-- 滚动策略配置,基于时间和日志文件大小的滚动策略-->
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<!-- 定义滚动日志文件的命名模式,文件名包含日期,并且每天生成一个新的日志文件 -->
<fileNamePattern>${LOG_FILE_NAME}.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<maxHistory>30</maxHistory>
<maxFileSize>64MB</maxFileSize>
<totalSizeCap>20GB</totalSizeCap>
</rollingPolicy>
如果 LOG_FILE_NAME 变量的值为 test,那么最新的日志都会被记录到 test.log 文件中,历史日志记录的格式如下
如果某一天的日志量很大,最后一位数字会递增
6.7 使用异步日志的注意事项
如果开启了异步输出,就不需要在 root 标签中添加 FILE appender 了,因为 ASYNC appender 已经与 FILE appender 产生关联
如果既开启了异步输出,root 标签中又添加 FILE appender,那么同一行日志在日志文件中将会被记录两次
7. 补充:Logback配置文件的命名规范
LogbackLoggingSystem
在 LogbackLoggingSystem
类的 getStandardConfigLocations 方法中,定义了配置文件的四种命名
AbstractLoggingSystem
查看 AbstractLoggingSystem
类的 getSpringConfigLocations 方法的源码,可以看到,如果是在 Spring 项目中,还可以添加 -spring
后缀
配置文件的命名符合以下八个中的其中一个就行了
建议配置文件的命名使用 logback.xml 或 logback-spring.xml
8. 记录日志的最佳实践
8.1 合理选择日志输出级别
Logback 提供了多个日志输出级别,按照严重性递增的顺序排列如下:
日志级别 | 描述 |
---|---|
TRACE | 提供程序执行过程中的详细信息,通常用于开发阶段,帮助开发者追踪程序的执行流程 |
DEBUG | 提供更详细的诊断信息,用于在开发过程中定位问题,通常不会在生产环境中启用 |
INFO | 记录应用程序的运行状态,包括关键操作的信息,这些日志通常对最终用户或管理员有所帮助 |
WARN | 表明可能发生了潜在错误,程序仍能继续运行,但需要注意这些警告信息 |
ERROR | 记录应用程序运行中发生的错误,这些错误可能会导致程序部分功能失效,但不会导致整个程序崩溃 |
OFF | 完全禁用日志记录,不输出任何日志信息 |
建议在开发环境使用低级别日志(比如 DEBUG),以获取详细的信息
在生产环境中,通常会配置日志级别为 INFO
或 WARN
INFO
级别用于记录应用程序的运行状态和关键操作的信息,可以帮助运维人员了解应用程序的运行情况,同时也为后续的问题排查提供必要的信息WARN
级别用于记录潜在的问题或者需要注意的情况,这些信息可能不会立即影响应用程序的运行,表明可能存在需要调查和解决的问题
8.2 正确地记录日志信息
当要输出的日志内容中存在变量时,建议使用参数化日志,也就是在日志信息中使用占位符( {}
),由日志框架在运行时替换为实际的参数值
比如输出一行用户登录的日志
// 不推荐
log.debug("用户ID:" + userId + " 登录成功。");
// 推荐
log.debug("用户ID:{} 登录成功。", userId);
这样做不仅让日志清晰易读,而且在日志级别低于当前记录级别时,不会执行字符串拼接,从而避免了字符串拼接带来的性能开销以及潜在的 NullPointerException
问题
所以建议在所有日志记录中,使用参数化的方式替代字符串拼接
此外,在输出异常信息时,建议同时记录上下文信息、以及完整的异常堆栈信息,便于排查问题
try {
// 业务逻辑
} catch (Exception e) {
logger.error("处理用户ID:{} 时发生异常:", userId, e);
}
8.3 控制日志的输出量
过多的日志不仅会占用更多的磁盘空间,还会增加系统的 I/O 负担,影响系统性能
因此,除了根据环境设置合适的日志级别外,还要尽量避免在循环中输出日志
在循环中利用 StringBuilder 进行字符串拼接,循环结束后统一输出
StringBuilder logBuilder = new StringBuilder("处理结果:").append(System.lineSeparator());
for (Item item : items) {
try {
processItem(item);
logBuilder.append(String.format("成功[ID=%s]%s", item.getId(), System.lineSeparator()));
} catch (Exception e) {
logBuilder.append(String.format("失败[ID=%s, 原因=%s]%s", item.getId(), e.getMessage(), System.lineSeparator()));
}
}
log.info(logBuilder.toString());
如果参数的计算开销较大,且当前日志级别不需要输出,应该在记录前进行级别检查,从而避免多余的参数计算
if (log.isDebugEnabled()) {
log.debug("复杂对象信息:{}", expensiveToComputeObject());
}
此外,还可以通过更改日志配置文件整体过滤掉特定级别的日志,来防止日志刷屏
<!-- Logback 示例 -->
<appender name="LIMITED" class="ch.qos.logback.classic.AsyncAppender">
<!-- 只允许 INFO 级别及以上的日志通过 -->
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>INFO</level>
</filter>
<!-- 配置其他属性 -->
</appender>
8.4 把控记录日志的时机和日志的内容
注意不要在日志中记录了敏感信息,万一你的日志不小心泄露出去,就相当于泄露了大量用户的信息
很多开发者(尤其是线上经验不丰富的开发者)并没有养成记录日志的习惯,觉得记录日志不重要,等到出了问题无法排查的时候才追悔莫及
-
在系统的关键流程和重要业务节点记录日志,比如用户登录、订单处理、支付等都是关键业务,建议多记录日志
-
对于重要的方法,建议在入口和出口记录重要的参数和返回值,便于快速还原现场、复现问题
-
对于调用链较长的操作,确保在每个环节都有日志,以便追踪到问题所在的环节
8.5 日志管理
随着日志文件的持续增长,会导致磁盘空间耗尽,影响系统正常运行,所以我们需要一些策略来对日志进行管理
首先是设置日志的滚动策略,可以根据文件大小或日期,自动对日志文件进行切分
8.5.1 按文件大小滚动
<!-- 按大小滚动 -->
<rollingPolicy class="ch.qos.logback.core.rolling.SizeBasedRollingPolicy">
<maxFileSize>10MB</maxFileSize>
</rollingPolicy>
如果日志文件大小达到 10MB,Logback 会将当前日志文件重命名为 app.log.1
或其他命名(具体由文件名模式决定),然后创建新的 app.log
文件继续写入日志
8.5.2 按照时间日期滚动
<!-- 滚动策略配置,基于时间的滚动策略 -->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- 定义滚动日志文件的命名模式,文件名包含日期,并且每天生成一个新的日志文件 -->
<fileNamePattern>${LOG_FILE_NAME}.%d{yyyy-MM-dd}.log</fileNamePattern>
</rollingPolicy>
上述配置表示每天创建一个新的日志文件,%d{yyyy-MM-dd} 表示按照日期命名日志文件
还可以通过 maxHistory 属性,限制保留的历史日志文件数量或天数
<maxHistory>30</maxHistory>
这样一来,我们就可以按照天数查看指定的日志,单个日志文件也不会很大,提高了日志检索效率
8.5.3 日志压缩
对于用户较多的企业级项目,日志的增长是飞快的,可以开启日志压缩功能,节省磁盘空间
<!-- 滚动策略配置,基于时间的滚动策略 -->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- 定义滚动日志文件的命名模式,文件名包含日期,并且每天生成一个新的日志文件 -->
<fileNamePattern>${LOG_FILE_NAME}.%d{yyyy-MM-dd}.log.gz</fileNamePattern>
</rollingPolicy>
上述配置表示:每天生成一个新的日志文件,旧的日志文件会被压缩存储
8.6 统一日志格式
统一的日志格式有助于日志的解析、搜索和分析,特别是在分布式系统中
统一的日志格式(整齐清晰,支持按照时间、线程、级别、类名和内容搜索)
2024-11-21 14:30:15.123 [main] INFO com.example.service.UserService - 用户ID:12345 登录成功
2024-11-21 14:30:16.789 [main] ERROR com.example.service.UserService - 用户ID:12345 登录失败,原因:密码错误
2024-11-21 14:30:17.456 [main] DEBUG com.example.dao.UserDao - 执行SQL:[SELECT * FROM users WHERE id=12345]
2024-11-21 14:30:18.654 [main] WARN com.example.config.AppConfig - 配置项 `timeout` 使用默认值:3000ms
2024-11-21 14:30:19.001 [main] INFO com.example.Main - 应用启动成功,耗时:2.34秒
不统一的日志格式(看到后直接原地爆炸)
2024/11/21 14:30 登录成功 用户ID: 12345
2024-11-21 14:30:16 错误 用户12345登录失败!密码不对
DEBUG 执行SQL SELECT * FROM users WHERE id=12345
Timeout = default
应用启动成功
建议每个项目都要明确约定和配置一套日志输出规范,确保日志中包含时间戳、日志级别、线程、类名、方法名、消息等关键信息
<!-- Console 输出配置 -->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<!-- 编码器配置,用于格式化日志输出 -->
<encoder>
<!-- 定义日志消息的输出格式,包括日期、进程ID、线程名、日志级别、日志器名和日志消息 -->
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [${PID:- }] [%thread] %-5level %logger{36} - %msg%n</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
8.7 使用异步日志
对于追求性能的操作,可以使用异步日志,将日志的写入操作放在单独的线程中,减少对主线程的阻塞,从而提升系统性能
配置的关键是配置缓冲队列,要设置合适的队列大小和丢弃策略,防止日志积压或丢失
<!-- 异步输出配置 -->
<appender name="ASYNC" class="ch.qos.logback.classic.AsyncAppender">
<!-- 队列大小,定义了在内存中存储日志事件的队列的最大容量 -->
<queueSize>512</queueSize> <!-- 队列大小 -->
<!-- 丢弃阈值,当队列剩余容量小于这个值时,将丢弃低于指定级别的日志事件 -->
<!-- 0 表示不丢弃任何级别的日志事件 -->
<discardingThreshold>0</discardingThreshold> <!-- 丢弃阈值,0 表示不丢弃 -->
<!-- 队列满时是否阻塞主线程,true 表示即使队列满了也不会阻塞主线程 -->
<!-- 这可能会导致日志事件丢失 -->
<neverBlock>true</neverBlock> <!-- 队列满时是否阻塞主线程,true 表示不阻塞 -->
<!-- 指定异步日志输出要引用的appender,此处只能引用一个appender,如果引用了多个appender,只有第一个appender生效 -->
<!-- 异步appender会将日志事件转发给这里指定的appender -->
<appender-ref ref="FILE"/> <!-- 生效的日志目标 -->
</appender>