跳到主要内容

日志管理

日志是应用程序运行过程中的重要记录,对于问题排查、性能分析和安全审计至关重要。Spring Boot 默认使用 SLF4J + Logback 组合,提供了完善的日志支持。

日志框架概述

日志门面与实现

Spring Boot 的日志体系采用"门面模式":

┌─────────────────────────────────────────────────────────────┐
│ 日志框架架构 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 应用代码 ──> SLF4J(日志门面)──> Logback(日志实现) │
│ │ │
│ ▼ │
│ 统一的日志 API │
│ │
│ 其他日志框架的适配: │
│ - Java Util Logging ──> SLF4J 桥接 │
│ - Commons Logging ──> SLF4J 桥接 │
│ - Log4j ──> SLF4J 桥接 │
│ │
└─────────────────────────────────────────────────────────────┘

为什么需要门面?

组件作用
SLF4J提供统一的日志 API,应用代码只依赖接口
Logback具体的日志实现,高性能、功能丰富

这样做的好处是:应用代码与具体日志实现解耦,可以随时切换日志框架。

日志级别

日志级别从低到高:

级别说明使用场景
TRACE最详细的信息程序追踪调试
DEBUG调试信息开发调试
INFO重要信息关键业务流程
WARN警告信息潜在问题
ERROR错误信息错误但程序可继续
FATAL严重错误程序无法继续(Logback 映射为 ERROR)
OFF关闭日志不输出任何日志

使用日志

创建 Logger

方式一:使用 Lombok 注解(推荐)

import lombok.extern.slf4j.Slf4j;

@Slf4j
@Service
public class UserService {

public User findById(Long id) {
log.info("查询用户,ID: {}", id);
User user = userRepository.findById(id).orElse(null);
if (user == null) {
log.warn("用户不存在,ID: {}", id);
}
return user;
}
}

方式二:手动创建 Logger

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@Service
public class UserService {

private static final Logger log = LoggerFactory.getLogger(UserService.class);

public void doSomething() {
log.info("执行操作");
}
}

日志方法

@Service
@Slf4j
public class OrderService {

public void processOrder(Order order) {
// 基本日志
log.trace("追踪信息:{}", order);
log.debug("调试信息:{}", order);
log.info("处理订单:{}", order.getId());
log.warn("库存不足警告:{}", order.getProductId());
log.error("订单处理失败", new RuntimeException("错误原因"));

// 带异常的日志
try {
// 业务逻辑
} catch (Exception e) {
log.error("处理订单异常,订单ID: {}", order.getId(), e);
}

// 条件日志(性能优化)
if (log.isDebugEnabled()) {
log.debug("详细调试信息: {}", expensiveOperation());
}
}

private String expensiveOperation() {
// 耗时操作
return "result";
}
}

占位符使用

// 推荐:使用占位符(性能更好)
log.info("用户 {} 登录成功,IP: {}", username, ipAddress);

// 不推荐:字符串拼接
log.info("用户 " + username + " 登录成功"); // 即使日志级别不输出也会拼接字符串

// 多个参数
log.info("订单 {} 状态变更:{} -> {}", orderId, oldStatus, newStatus);

// 带异常
log.error("处理失败,订单ID: {}", orderId, exception);

日志配置

基本配置

application.yml 中配置:

# 日志级别
logging:
level:
root: INFO # 根日志级别
com.example: DEBUG # 特定包的日志级别
org.springframework.web: DEBUG # Spring Web 日志
org.hibernate.SQL: DEBUG # Hibernate SQL 日志

# 日志文件配置
file:
name: logs/myapp.log # 日志文件名(完整路径)
# path: logs # 日志目录(与 name 二选一)
max-size: 10MB # 单个文件最大大小
max-history: 30 # 保留的历史文件数
total-size-cap: 1GB # 总日志文件大小上限

# 日志格式
pattern:
console: "%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n"
file: "%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n"

日志级别配置详解

logging:
level:
root: INFO # 全局默认级别
com.example: DEBUG # 项目代码 DEBUG
com.example.mapper: TRACE # Mapper 层显示 SQL 参数
org.springframework: INFO # Spring 框架 INFO
org.springframework.web: DEBUG
org.springframework.security: DEBUG
org.hibernate: INFO
org.hibernate.SQL: DEBUG # 显示 SQL
org.hibernate.type.descriptor.sql: TRACE # 显示 SQL 参数

日志格式说明

%d{yyyy-MM-dd HH:mm:ss.SSS}  - 日期时间
%thread - 线程名
%-5level - 日志级别,左对齐,占5位
%logger{36} - Logger 名称,最长36字符
%msg - 日志消息
%n - 换行
%clr(%5p){cyan} - 彩色输出(控制台)
%X{traceId} - MDC 中的 traceId

完整格式示例

logging:
pattern:
console: "%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){green} [%clr(%thread){blue}] %clr(%-5level){yellow} %clr(%logger{36}){cyan} - %msg%n"

控制台彩色输出

spring:
output:
ansi:
enabled: always # 总是启用彩色输出
# enabled: detect # 自动检测(默认)
# enabled: never # 禁用

颜色对应关系

日志级别颜色
ERROR红色
WARN黄色
INFO绿色
DEBUG绿色
TRACE绿色

日志文件管理

文件滚动策略

Spring Boot 默认提供滚动日志文件支持:

logging:
file:
name: logs/myapp.log
max-size: 10MB # 单文件最大 10MB
max-history: 30 # 保留 30 天
total-size-cap: 1GB # 总大小上限 1GB
clean-history-on-start: false # 启动时是否清理历史文件

文件命名规则

  • 当前日志:myapp.log
  • 历史日志:myapp.2024-01-15.0.logmyapp.2024-01-15.1.log

按日期滚动

logging:
file:
name: logs/myapp.log
logback:
rollingpolicy:
file-name-pattern: logs/myapp.%d{yyyy-MM-dd}.%i.log
max-history: 30
max-file-size: 10MB

Logback 高级配置

创建 logback-spring.xml

src/main/resources/ 目录下创建 logback-spring.xml

<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="30 seconds">

<!-- 从 Spring 配置中读取属性 -->
<springProperty scope="context" name="appName" source="spring.application.name" defaultValue="myapp"/>
<springProperty scope="context" name="logPath" source="logging.file.path" defaultValue="logs"/>

<!-- 定义变量 -->
<property name="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">
<encoder>
<pattern>${LOG_PATTERN}</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>

<!-- 文件输出 -->
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${logPath}/${appName}.log</file>
<encoder>
<pattern>${LOG_PATTERN}</pattern>
<charset>UTF-8</charset>
</encoder>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>${logPath}/${appName}.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<maxFileSize>10MB</maxFileSize>
<maxHistory>30</maxHistory>
<totalSizeCap>1GB</totalSizeCap>
</rollingPolicy>
</appender>

<!-- 错误日志单独输出 -->
<appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${logPath}/${appName}-error.log</file>
<encoder>
<pattern>${LOG_PATTERN}</pattern>
</encoder>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>${logPath}/${appName}-error.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<maxFileSize>10MB</maxFileSize>
<maxHistory>30</maxHistory>
</rollingPolicy>
<!-- 只记录 ERROR 级别 -->
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>ERROR</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
</appender>

<!-- 异步输出(提升性能) -->
<appender name="ASYNC_FILE" class="ch.qos.logback.classic.AsyncAppender">
<discardingThreshold>0</discardingThreshold>
<queueSize>512</queueSize>
<appender-ref ref="FILE"/>
</appender>

<!-- 开发环境配置 -->
<springProfile name="dev">
<root level="INFO">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="FILE"/>
</root>
<logger name="com.example" level="DEBUG"/>
</springProfile>

<!-- 生产环境配置 -->
<springProfile name="prod">
<root level="INFO">
<appender-ref ref="ASYNC_FILE"/>
<appender-ref ref="ERROR_FILE"/>
</root>
<logger name="com.example" level="INFO"/>
</springProfile>

</configuration>

Profile 特定配置

使用 <springProfile> 标签针对不同环境配置:

<!-- 开发环境:输出到控制台 -->
<springProfile name="dev">
<root level="DEBUG">
<appender-ref ref="CONSOLE"/>
</root>
</springProfile>

<!-- 生产环境:输出到文件 -->
<springProfile name="prod">
<root level="INFO">
<appender-ref ref="FILE"/>
<appender-ref ref="ERROR_FILE"/>
</root>
</springProfile>

<!-- 多环境匹配 -->
<springProfile name="dev | test">
<!-- dev 或 test 环境生效 -->
</springProfile>

<!-- 排除环境 -->
<springProfile name="!prod">
<!-- 非 prod 环境生效 -->
</springProfile>

日志分组

Spring Boot 支持将多个 Logger 组合在一起统一配置:

logging:
group:
# 自定义分组
service: com.example.user,com.example.order,com.example.product
web: org.springframework.web,org.springframework.boot.web

level:
service: DEBUG # 整个分组设置为 DEBUG
web: INFO

内置分组

分组名包含的 Logger
weborg.springframework.web
sqlorg.springframework.jdbc, org.hibernate.SQL

MDC(Mapped Diagnostic Context)

MDC 用于在日志中添加上下文信息,如请求 ID、用户 ID 等。

使用 MDC

import org.slf4j.MDC;

@Slf4j
@RestController
public class UserController {

@GetMapping("/users/{id}")
public User getUser(@PathVariable Long id) {
// 添加 MDC 信息
MDC.put("userId", String.valueOf(id));
MDC.put("requestId", UUID.randomUUID().toString());

try {
log.info("查询用户信息");
return userService.findById(id);
} finally {
// 清理 MDC
MDC.clear();
}
}
}

在日志格式中使用 MDC

<property name="LOG_PATTERN" 
value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%X{requestId}] [%thread] %-5level %logger{36} - %msg%n"/>

拦截器自动添加 MDC

@Component
public class LogInterceptor implements HandlerInterceptor {

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
String requestId = request.getHeader("X-Request-Id");
if (requestId == null) {
requestId = UUID.randomUUID().toString();
}
MDC.put("requestId", requestId);
MDC.put("uri", request.getRequestURI());
return true;
}

@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
Object handler, Exception ex) {
MDC.clear();
}
}

结构化日志

Spring Boot 3.x 支持结构化日志(JSON 格式):

启用 JSON 日志

logging:
structured:
format:
console: ecs # Elastic Common Schema
file: ecs

支持的格式

格式说明
ecsElastic Common Schema
gelfGraylog Extended Log Format
logstashLogstash JSON format

JSON 日志输出示例

{
"@timestamp": "2024-01-15T10:30:00.123Z",
"log": {
"level": "INFO",
"logger": "com.example.UserService"
},
"message": "用户登录成功",
"service": {
"name": "myapp",
"version": "1.0.0"
},
"process": {
"pid": 12345,
"thread": {
"name": "http-nio-8080-exec-1"
}
}
}

日志最佳实践

1. 合理使用日志级别

// ERROR:影响业务正常运行的错误
log.error("订单支付失败,订单ID: {}", orderId, e);

// WARN:潜在问题,不影响系统运行
log.warn("库存不足,商品ID: {}, 剩余: {}", productId, stock);

// INFO:关键业务节点
log.info("用户登录成功,用户名: {}", username);

// DEBUG:调试信息(生产环境关闭)
log.debug("查询条件: {}", condition);

// TRACE:详细追踪(仅开发环境)
log.trace("方法入口,参数: {}", params);

2. 避免敏感信息

// 错误:暴露敏感信息
log.info("用户登录,密码: {}", password);

// 正确:脱敏或排除
log.info("用户登录,用户名: {}", username);

3. 使用占位符而非拼接

// 不推荐:字符串拼接
log.info("处理订单: " + orderId + ", 状态: " + status);

// 推荐:占位符
log.info("处理订单: {}, 状态: {}", orderId, status);

4. 条件日志

// 避免不必要的字符串操作
if (log.isDebugEnabled()) {
log.debug("详细数据: {}", objectMapper.writeValueAsString(data));
}

5. 异常日志包含堆栈

// 正确:包含异常堆栈
log.error("处理订单异常,订单ID: {}", orderId, e);

// 错误:丢失堆栈信息
log.error("处理订单异常: " + e.getMessage());

6. 异步日志提升性能

<appender name="ASYNC_FILE" class="ch.qos.logback.classic.AsyncAppender">
<queueSize>512</queueSize>
<discardingThreshold>0</discardingThreshold>
<appender-ref ref="FILE"/>
</appender>

常见问题

1. 日志文件不生成

检查配置:

logging:
file:
name: logs/myapp.log # 确保路径正确

2. 中文乱码

<encoder>
<pattern>...</pattern>
<charset>UTF-8</charset>
</encoder>

3. 日志级别不生效

确保配置的包路径正确:

logging:
level:
com.example: DEBUG # 包路径要与代码包路径一致

4. 日志文件过大

配置滚动策略:

logging:
file:
max-size: 10MB
max-history: 30
total-size-cap: 1GB

小结

本章我们学习了:

  1. 日志框架:SLF4J + Logback 的架构
  2. 日志级别:TRACE、DEBUG、INFO、WARN、ERROR 的使用
  3. 基本配置:日志级别、格式、文件配置
  4. Logback 高级配置:自定义 appender、滚动策略
  5. MDC:上下文信息传递
  6. 结构化日志:JSON 格式输出
  7. 最佳实践:合理使用级别、避免敏感信息、性能优化

练习

  1. 配置日志输出到文件,并设置滚动策略
  2. 创建 logback-spring.xml 实现不同环境不同配置
  3. 使用 MDC 添加请求 ID 到日志
  4. 实现错误日志单独输出到文件
  5. 配置异步日志提升性能

参考资源