1. 日志概述:为什么我们需要关注日志
在开发Spring Boot应用时,日志系统是我们最常接触但可能最容易被忽视的组件之一。记得我刚入行时,曾花费整整三天时间排查一个线上问题,最终发现只是因为一个简单的空指针异常——如果当时正确配置了日志级别,这个问题本可以在五分钟内解决。
1.1 日志的核心价值
日志不仅仅是程序运行时的文字记录,它是一个完整的监控、诊断和审计系统。想象一下医院的病历系统:医生通过病历了解病人的历史状况,我们则通过日志了解应用程序的生命体征。
具体来说,日志的用途包括:
- 问题诊断:当系统抛出异常时,完整的调用栈和上下文信息能帮我们快速定位问题根源
- 行为审计:记录关键操作(如用户登录、数据修改)用于安全审计
- 性能分析:通过记录方法执行时间,找出性能瓶颈
- 业务分析:用户行为日志可以转化为商业智能数据
- 系统监控:配合监控系统实时预警异常状态
实际经验:在电商系统中,我们曾通过分析用户点击日志,发现某个商品页面的转化率异常低,最终定位到是价格显示模块的缓存问题。
1.2 Spring Boot默认日志系统
Spring Boot默认集成了SLF4J日志门面和Logback日志实现。启动应用时看到的那些彩色日志(如下所示)就是Logback的杰作:
code复制2023-08-20 14:22:33.123 INFO 12345 --- [main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http)
与System.out.println相比,这种结构化日志包含了:
- 精确的时间戳(精确到毫秒)
- 日志级别(INFO/WARN/ERROR等)
- 进程ID
- 线程名([main])
- 日志来源类
- 实际日志内容
2. 日志实战:从基础使用到高级配置
2.1 正确获取Logger对象
在Spring Boot中获取Logger的正确姿势是:
java复制import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@RestController
public class ProductController {
// 使用类名作为Logger名称是行业惯例
private static final Logger logger = LoggerFactory.getLogger(ProductController.class);
@GetMapping("/products")
public List<Product> listProducts() {
logger.debug("Fetching product list"); // 调试日志
// ...业务逻辑
}
}
关键注意事项:
- Logger对象应该是static final的,避免重复创建
- 命名通常使用当前类名(LoggerFactory.getLogger(XXX.class))
- 确保导入的是org.slf4j包,而非其他日志框架的Logger
2.2 理解日志级别
Logback定义了5种日志级别(从低到高):
| 级别 | 使用场景 | 示例 |
|---|---|---|
| TRACE | 最详细的跟踪信息,通常用于方法进入/退出等细粒度跟踪 | logger.trace("Entering calculatePrice, params: {}", product); |
| DEBUG | 调试信息,开发阶段使用 | logger.debug("Discount applied: {}%", discountRate); |
| INFO | 重要的业务过程信息 | logger.info("User {} placed order {}", userId, orderId); |
| WARN | 潜在问题,不影响系统运行但需要注意 | logger.warn("Cache miss for key {}", cacheKey); |
| ERROR | 错误事件,影响系统功能 | logger.error("Failed to process payment for order {}", orderId, e); |
配置建议:
- 开发环境:DEBUG级别
- 测试环境:INFO级别
- 生产环境:WARN级别(根据负载情况调整)
在application.properties中配置:
properties复制# 设置root日志级别
logging.level.root=WARN
# 设置特定包下的日志级别
logging.level.com.example.demo=DEBUG
2.3 日志输出最佳实践
格式化日志消息:
java复制// 错误示范 - 字符串拼接
logger.debug("User " + userId + " purchased " + itemCount + " items");
// 正确示范 - 使用占位符
logger.debug("User {} purchased {} items", userId, itemCount);
为什么要用占位符?
- 性能优化:只有当日志级别足够时才会执行字符串拼接
- 可读性:更清晰的日志模板
- 一致性:便于日志分析工具处理
异常日志的正确写法:
java复制try {
// 业务代码
} catch (Exception e) {
// 错误示范 - 只打印异常消息
logger.error("Operation failed: " + e.getMessage());
// 正确示范 - 打印完整堆栈
logger.error("Operation failed for user {}", userId, e);
}
3. 高级配置:让日志系统更强大
3.1 日志持久化配置
生产环境必须将日志持久化到文件。Spring Boot支持两种方式:
方式一:指定日志文件路径
properties复制logging.file.name=app.log
方式二:指定日志目录(自动生成spring.log)
properties复制logging.file.path=/var/log/myapp
推荐配置(带滚动策略):
yaml复制logging:
file:
name: /var/log/myapp/application.log
logback:
rollingpolicy:
max-file-size: 100MB
max-history: 30
file-name-pattern: ${logging.file.name}.%d{yyyy-MM-dd}.%i.gz
这个配置表示:
- 单个日志文件超过100MB时触发滚动
- 保留最近30天的日志
- 压缩历史日志节省空间
3.2 自定义日志格式
默认的日志格式可能不满足所有需求。我们可以自定义控制台和文件的输出格式:
properties复制# 控制台日志格式
logging.pattern.console=%d{yyyy-MM-dd HH:mm:ss.SSS} %clr(%-5level){faint} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n%wEx
# 文件日志格式
logging.pattern.file=%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%t] %-40logger{39} : %m%n%wEx
格式说明符:
%d:日期时间%level:日志级别%t:线程名%logger:日志来源%m:日志消息%n:换行%wEx:异常堆栈
3.3 使用Lombok简化日志代码
添加Lombok依赖后,可以使用@Slf4j注解极大简化代码:
java复制@Slf4j
@RestController
public class OrderController {
@PostMapping("/orders")
public Order createOrder(@RequestBody OrderRequest request) {
log.info("Creating order for user {}", request.getUserId());
// 业务逻辑
}
}
Lombok会自动生成一个名为log的Logger对象,相当于:
java复制private static final Logger log = LoggerFactory.getLogger(OrderController.class);
Lombok日志注解支持:
@Slf4j:使用SLF4J@Log4j2:使用Log4j2@CommonsLog:使用Apache Commons Logging
4. 生产环境日志实践
4.1 日志分级存储策略
在生产环境中,我们通常采用分级存储策略:
- 控制台输出:ERROR级别,便于运维人员及时发现严重错误
- 本地文件:
- application.log:INFO级别,记录常规业务日志
- error.log:ERROR级别,单独记录错误日志
- 日志收集系统:如ELK(Elasticsearch+Logstash+Kibana)集中管理
配置示例:
properties复制# 主日志文件
logging.file.name=/var/log/myapp/application.log
logging.level.root=INFO
# 错误日志文件
logging.logback.rollingpolicy.file-name-pattern=/var/log/myapp/error.%d.log
logging.logback.rollingpolicy.clean-history-on-start=true
logging.logback.rollingpolicy.max-history=30
4.2 敏感信息过滤
日志中绝对不能记录以下敏感信息:
- 用户密码
- 信用卡号等支付信息
- 身份证号
- API密钥
实现方式:
- 使用脱敏工具类
java复制public class LogUtils {
public static String maskSensitive(String input) {
// 实现脱敏逻辑
}
}
log.info("User login: {}", LogUtils.maskSensitive(username));
- 使用Logback的替换功能
xml复制<configuration>
<conversionRule conversionWord="msg"
converterClass="com.example.SensitiveDataConverter"/>
</configuration>
4.3 日志性能优化
高并发场景下,日志可能成为性能瓶颈。以下是一些优化建议:
- 异步日志:使用AsyncAppender减少I/O阻塞
xml复制<appender name="ASYNC_FILE" class="ch.qos.logback.classic.AsyncAppender">
<appender-ref ref="FILE" />
<queueSize>1024</queueSize>
<discardingThreshold>0</discardingThreshold>
</appender>
-
避免过度日志:
- 循环中不要记录DEBUG日志
- 大集合/数组只记录摘要信息
-
使用条件日志:
java复制if (logger.isDebugEnabled()) {
logger.debug("Large object details: {}", expensiveToStringOperation());
}
5. 常见问题排查指南
5.1 日志不输出问题
问题现象:配置了DEBUG级别但看不到日志
排查步骤:
-
检查是否有多个日志框架冲突(如同时存在Logback和Log4j2)
xml复制<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <exclusions> <exclusion> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-logging</artifactId> </exclusion> </exclusions> </dependency> -
检查配置文件加载顺序:
- application.properties > application.yml
- profile-specific配置会覆盖默认配置
-
检查自定义logback.xml是否覆盖了Spring Boot默认配置
5.2 日志文件不滚动问题
问题现象:日志文件持续增大,没有按日期分割
解决方案:
- 确保配置了滚动策略:
properties复制logging.logback.rollingpolicy.max-file-size=100MB
logging.logback.rollingpolicy.max-history=7
-
检查文件权限:
bash复制chmod 644 /var/log/myapp/*.log -
检查磁盘空间:
bash复制df -h /var/log
5.3 日志格式混乱问题
问题现象:日志中出现乱码或格式错乱
解决方案:
- 统一编码配置:
properties复制logging.charset.console=UTF-8
logging.charset.file=UTF-8
- 检查终端是否支持ANSI颜色(对于彩色日志)
- 避免在多线程环境下修改日志格式
在多年的Spring Boot开发中,我发现合理配置日志系统可以节省大量故障排查时间。建议在项目初期就建立完善的日志规范,包括级别使用指南、格式标准和存储策略。记住,好的日志系统应该是:开发时足够详细,生产环境足够高效,出问题时足够有用。