Spring Boot与日志
日志是应用程序中非常重要的组成部分,用于记录程序运行过程中的信息、警告和错误。Spring Boot为日志提供了优秀的支持和默认配置。
日志框架概述
日志门面与日志实现
在Java生态系统中,日志分为两类:
日志门面(Facade): - SLF4J(Simple Logging Facade for Java) - Commons Logging
日志实现: - Logback - Log4j2 - Log4j - Java Util Logging(JUL)
Spring Boot的日志选择
Spring Boot默认使用: - SLF4J 作为日志门面 - Logback 作为日志实现
这是一个非常好的组合,因为: - SLF4J提供了简洁的API - Logback性能优秀,功能强大 - 两者都是同一个作者开发,兼容性好
Spring Boot日志默认配置
默认日志配置
Spring Boot开箱即用的日志配置包括:
- 控制台输出:默认输出到控制台
- 日志级别:Root级别为INFO
- 日志格式:包含时间戳、级别、PID、线程名、类名和消息
- 彩色输出:在支持的终端中显示彩色日志
默认日志格式
2023-12-01 10:30:45.123 INFO 12345 --- [main] com.example.Application : Starting Application
格式说明:
- 2023-12-01 10:30:45.123
:时间戳
- INFO
:日志级别
- 12345
:进程ID
- [main]
:线程名
- com.example.Application
:类名
- Starting Application
:日志消息
日志级别
日志级别层次
从低到高依次为:
- TRACE:最详细的信息,通常只在开发时使用
- DEBUG:调试信息,用于诊断问题
- INFO:一般信息,程序正常运行的信息
- WARN:警告信息,可能出现问题但不影响运行
- ERROR:错误信息,程序出现错误但可以继续运行
- FATAL:致命错误,程序无法继续运行
在代码中使用日志
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
@Service
public class UserService {
private static final Logger logger = LoggerFactory.getLogger(UserService.class);
public User findById(Long id) {
logger.trace("Entering findById method with id: {}", id);
try {
logger.debug("Searching for user with id: {}", id);
User user = userRepository.findById(id);
if (user != null) {
logger.info("User found: {}", user.getUsername());
return user;
} else {
logger.warn("User not found with id: {}", id);
return null;
}
} catch (Exception e) {
logger.error("Error occurred while finding user with id: {}", id, e);
throw new UserServiceException("Failed to find user", e);
} finally {
logger.trace("Exiting findById method");
}
}
}
使用Lombok简化日志
添加Lombok依赖:
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
使用@Slf4j注解:
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@Slf4j
@Service
public class UserService {
public User findById(Long id) {
log.debug("Searching for user with id: {}", id);
try {
User user = userRepository.findById(id);
log.info("User found: {}", user.getUsername());
return user;
} catch (Exception e) {
log.error("Error finding user with id: {}", id, e);
throw new UserServiceException("Failed to find user", e);
}
}
}
日志配置
application.yml中的日志配置
# 基本日志配置
logging:
# 设置日志级别
level:
root: INFO # 根日志级别
com.example: DEBUG # 包级别配置
com.example.service: TRACE # 具体包的日志级别
org.springframework: WARN # Spring框架日志级别
org.hibernate: ERROR # Hibernate日志级别
# 日志输出配置
file:
name: app.log # 日志文件名
path: /var/logs/ # 日志文件路径
# 日志文件配置
logback:
rollingpolicy:
max-file-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"
环境特定的日志配置
application-dev.yml(开发环境):
logging:
level:
root: DEBUG
com.example: TRACE
pattern:
console: "%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} %clr([%thread]){blue} %clr(%-5level){cyan} %clr(%logger{36}){magenta} - %msg%n"
application-prod.yml(生产环境):
logging:
level:
root: WARN
com.example: INFO
file:
name: /var/logs/app.log
logback:
rollingpolicy:
max-file-size: 50MB
max-history: 60
自定义Logback配置
创建logback-spring.xml
在src/main/resources
目录下创建logback-spring.xml
:
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!-- 引入Spring Boot默认配置 -->
<include resource="org/springframework/boot/logging/logback/defaults.xml"/>
<!-- 定义属性 -->
<property name="LOG_FILE" value="${LOG_FILE:-app}"/>
<property name="LOG_PATH" value="${LOG_PATH:-./logs}"/>
<!-- 控制台输出配置 -->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>
%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint}
%clr([%thread]){blue}
%clr(%-5level){cyan}
%clr(%logger{36}){magenta}
%clr(-){faint} %msg%n
</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
<!-- 文件输出配置 -->
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_PATH}/${LOG_FILE}.log</file>
<encoder>
<pattern>
%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n
</pattern>
<charset>UTF-8</charset>
</encoder>
<!-- 滚动策略 -->
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>${LOG_PATH}/${LOG_FILE}-%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern>
<maxFileSize>10MB</maxFileSize>
<maxHistory>30</maxHistory>
<totalSizeCap>1GB</totalSizeCap>
</rollingPolicy>
</appender>
<!-- 错误日志单独输出 -->
<appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_PATH}/${LOG_FILE}-error.log</file>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>ERROR</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
<encoder>
<pattern>
%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n
</pattern>
<charset>UTF-8</charset>
</encoder>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>${LOG_PATH}/${LOG_FILE}-error-%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern>
<maxFileSize>10MB</maxFileSize>
<maxHistory>30</maxHistory>
</rollingPolicy>
</appender>
<!-- 异步日志配置 -->
<appender name="ASYNC_FILE" class="ch.qos.logback.classic.AsyncAppender">
<appender-ref ref="FILE"/>
<queueSize>1024</queueSize>
<discardingThreshold>0</discardingThreshold>
<includeCallerData>true</includeCallerData>
</appender>
<!-- 环境特定配置 -->
<springProfile name="dev">
<logger name="com.example" level="DEBUG"/>
<root level="INFO">
<appender-ref ref="CONSOLE"/>
</root>
</springProfile>
<springProfile name="prod">
<logger name="com.example" level="INFO"/>
<logger name="org.springframework" level="WARN"/>
<root level="WARN">
<appender-ref ref="ASYNC_FILE"/>
<appender-ref ref="ERROR_FILE"/>
</root>
</springProfile>
<!-- 默认配置 -->
<springProfile name="!dev & !prod">
<root level="INFO">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="FILE"/>
</root>
</springProfile>
</configuration>
高级日志配置
JSON格式日志
适用于日志收集系统(如ELK Stack):
<!-- JSON格式输出 -->
<appender name="JSON_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_PATH}/${LOG_FILE}.json</file>
<encoder class="net.logstash.logback.encoder.LogstashEncoder">
<includeContext>true</includeContext>
<includeMdc>true</includeMdc>
<customFields>{"service":"my-app","version":"1.0.0"}</customFields>
</encoder>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>${LOG_PATH}/${LOG_FILE}-%d{yyyy-MM-dd}.%i.json.gz</fileNamePattern>
<maxFileSize>10MB</maxFileSize>
<maxHistory>30</maxHistory>
</rollingPolicy>
</appender>
需要添加依赖:
<dependency>
<groupId>net.logstash.logback</groupId>
<artifactId>logstash-logback-encoder</artifactId>
<version>7.2</version>
</dependency>
MDC(Mapped Diagnostic Context)
MDC用于在日志中添加上下文信息:
@Component
public class LoggingFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
// 添加请求ID到MDC
String requestId = UUID.randomUUID().toString();
MDC.put("requestId", requestId);
MDC.put("requestURI", httpRequest.getRequestURI());
MDC.put("userAgent", httpRequest.getHeader("User-Agent"));
try {
chain.doFilter(request, response);
} finally {
// 清理MDC
MDC.clear();
}
}
}
在logback配置中使用MDC:
<encoder>
<pattern>
%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level [%X{requestId}] %logger{36} - %msg%n
</pattern>
</encoder>
条件日志配置
@Component
public class ConditionalLoggingExample {
private static final Logger logger = LoggerFactory.getLogger(ConditionalLoggingExample.class);
public void processData(List<String> data) {
// 只有在DEBUG级别启用时才执行复杂的日志构建
if (logger.isDebugEnabled()) {
logger.debug("Processing data: {}", data.stream()
.collect(Collectors.joining(", ", "[", "]")));
}
// 对于简单的日志,直接使用占位符
logger.info("Processing {} items", data.size());
}
}
性能优化
异步日志
异步日志可以提高应用性能:
<!-- 异步Appender配置 -->
<appender name="ASYNC_CONSOLE" class="ch.qos.logback.classic.AsyncAppender">
<appender-ref ref="CONSOLE"/>
<queueSize>1024</queueSize>
<discardingThreshold>0</discardingThreshold>
<includeCallerData>false</includeCallerData>
<neverBlock>true</neverBlock>
</appender>
日志级别优化
生产环境建议配置:
logging:
level:
root: WARN
com.yourcompany: INFO
org.springframework: ERROR
org.hibernate: ERROR
com.zaxxer.hikari: WARN
日志监控和分析
集成Micrometer
@RestController
public class UserController {
private static final Logger logger = LoggerFactory.getLogger(UserController.class);
private final Counter errorCounter;
public UserController(MeterRegistry meterRegistry) {
this.errorCounter = Counter.builder("user.errors")
.description("User operation errors")
.register(meterRegistry);
}
@GetMapping("/users/{id}")
public User getUser(@PathVariable Long id) {
try {
logger.info("Fetching user with id: {}", id);
return userService.findById(id);
} catch (Exception e) {
logger.error("Error fetching user with id: {}", id, e);
errorCounter.increment();
throw e;
}
}
}
健康检查端点
management:
endpoints:
web:
exposure:
include: health,info,loggers,metrics
endpoint:
loggers:
enabled: true
可以通过以下端点管理日志:
- GET /actuator/loggers
- 查看所有logger配置
- GET /actuator/loggers/{name}
- 查看特定logger配置
- POST /actuator/loggers/{name}
- 动态修改日志级别
动态修改日志级别:
curl -X POST 'http://localhost:8080/actuator/loggers/com.example.service' \
-H 'Content-Type: application/json' \
-d '{"configuredLevel": "DEBUG"}'
最佳实践
1. 日志内容
@Service
public class OrderService {
private static final Logger logger = LoggerFactory.getLogger(OrderService.class);
public Order createOrder(OrderRequest request) {
// 记录关键业务操作
logger.info("Creating order for user: {}, amount: {}",
request.getUserId(), request.getAmount());
try {
Order order = processOrder(request);
// 记录成功操作
logger.info("Order created successfully: orderId={}, amount={}",
order.getId(), order.getAmount());
return order;
} catch (InsufficientFundsException e) {
// 记录业务异常(WARN级别)
logger.warn("Insufficient funds for user: {}, required: {}, available: {}",
request.getUserId(), request.getAmount(), e.getAvailableAmount());
throw e;
} catch (Exception e) {
// 记录系统异常(ERROR级别)
logger.error("Failed to create order for user: {}", request.getUserId(), e);
throw new OrderCreationException("Order creation failed", e);
}
}
}
2. 避免敏感信息泄露
@Data
public class User {
private Long id;
private String username;
@JsonIgnore
private String password; // 密码不会出现在日志中
@Override
public String toString() {
return "User{id=" + id + ", username='" + username + "'}";
}
}
3. 结构化日志
public class StructuredLogging {
private static final Logger logger = LoggerFactory.getLogger(StructuredLogging.class);
public void logUserAction(String action, Long userId, String details) {
// 使用结构化的方式记录日志
MDC.put("action", action);
MDC.put("userId", String.valueOf(userId));
logger.info("User action completed: {}", details);
MDC.clear();
}
}
4. 日志轮转配置
logging:
logback:
rollingpolicy:
max-file-size: 50MB # 单文件大小
max-history: 30 # 保留天数
total-size-cap: 2GB # 总大小限制
clean-history-on-start: true # 启动时清理
小结
Spring Boot的日志系统具有以下特点:
- 开箱即用:默认配置适合大多数场景
- 灵活配置:支持多种配置方式和格式
- 性能优化:支持异步日志和条件日志
- 生产就绪:提供日志轮转、监控等企业级特性
- 可观测性:与监控系统良好集成
正确使用日志可以帮助我们: - 快速定位和解决问题 - 监控应用运行状态 - 分析业务数据和用户行为 - 满足合规和审计要求
记住:好的日志是应用可维护性的重要保障!