Skip to content

Spring Boot 日志系统详解 📝

引言:为什么需要日志?

想象一下,你开发了一个电商网站,某天突然有用户反馈"购买失败",但你却不知道具体哪里出了问题。没有日志的系统就像一个黑盒子,出现问题时你只能"盲人摸象"。这就是为什么日志系统如此重要的原因。

IMPORTANT

日志不仅仅是调试工具,它是系统运行状态的"健康档案",帮助我们:

  • 🔍 快速定位问题
  • 📊 分析系统性能
  • 🛡️ 监控安全事件
  • 📈 了解用户行为

Spring Boot 日志系统架构

Spring Boot 采用了一种巧妙的设计哲学:门面模式 + 可插拔实现

TIP

这种设计的好处是:你的业务代码不需要关心底层使用哪种日志框架,Spring Boot 会自动选择最合适的实现。

日志格式解析

让我们来解读 Spring Boot 默认的日志格式:

2025-05-22T20:15:56.181Z  INFO 135387 --- [myapp] [           main] o.s.b.d.f.logexample.MyApplication       : Starting MyApplication using Java 17.0.15

这行日志包含了丰富的信息:

组成部分示例值说明
时间戳2025-05-22T20:15:56.181ZISO 8601 格式,毫秒精度
日志级别INFOERROR > WARN > INFO > DEBUG > TRACE
进程ID135387便于多进程环境下的问题追踪
分隔符---区分元数据和实际消息
应用名称[myapp]来自 spring.application.name
线程名称[main]执行日志的线程
日志器名称o.s.b.d.f.logexample.MyApplication通常是类名(简化显示)
消息内容Starting MyApplication...实际的日志信息

实战配置指南

1. 基础配置

kotlin
@SpringBootApplication
class LoggingDemoApplication

fun main(args: Array<String>) {
    runApplication<LoggingDemoApplication>(*args)
}

@RestController
class LogController {
    
    // 使用 Kotlin 的日志扩展
    private val logger = LoggerFactory.getLogger(LogController::class.java)
    
    @GetMapping("/test-logging")
    fun testLogging(): String {
        logger.trace("这是 TRACE 级别日志") 
        logger.debug("这是 DEBUG 级别日志") 
        logger.info("这是 INFO 级别日志") 
        logger.warn("这是 WARN 级别日志") 
        logger.error("这是 ERROR 级别日志") 
        
        return "日志测试完成,请查看控制台输出"
    }
}
properties
# 应用基本信息
spring.application.name=logging-demo

# 日志级别配置
logging.level.root=INFO
logging.level.com.example.demo=DEBUG
logging.level.org.springframework.web=DEBUG

# 文件输出配置
logging.file.name=logs/application.log
logging.file.path=logs/

# 控制台颜色输出
spring.output.ansi.enabled=always

2. 日志级别管理

不同的日志级别适用于不同的场景:

kotlin
@Service
class UserService {
    private val logger = LoggerFactory.getLogger(UserService::class.java)
    
    fun createUser(user: User): User {
        logger.debug("开始创建用户: {}", user.username) 
        
        try {
            // 业务逻辑
            val savedUser = userRepository.save(user)
            logger.info("用户创建成功: {}", savedUser.id) 
            return savedUser
            
        } catch (e: DataIntegrityViolationException) {
            logger.warn("用户名已存在: {}", user.username) 
            throw UserAlreadyExistsException("用户名已存在")
            
        } catch (e: Exception) {
            logger.error("创建用户失败: {}", user.username, e) 
            throw UserCreationException("用户创建失败")
        }
    }
}

3. 日志分组管理

当你需要同时调整多个相关组件的日志级别时,日志分组非常有用:

properties
# 定义日志组
logging.group.web=org.springframework.core.codec,org.springframework.http,org.springframework.web
logging.group.database=org.springframework.jdbc,org.hibernate.SQL,org.hibernate.type
logging.group.security=org.springframework.security

# 统一设置组的日志级别
logging.level.web=DEBUG
logging.level.database=DEBUG
logging.level.security=WARN
yaml
logging:
  group:
    web: "org.springframework.core.codec,org.springframework.http,org.springframework.web"
    database: "org.springframework.jdbc,org.hibernate.SQL,org.hibernate.type"
    security: "org.springframework.security"
  level:
    web: DEBUG
    database: DEBUG
    security: WARN

4. 文件输出与轮转

对于生产环境,我们通常需要将日志输出到文件,并配置合理的轮转策略:

properties
# 基础文件配置
logging.file.name=logs/app.log

# Logback 轮转策略配置
logging.logback.rollingpolicy.file-name-pattern=logs/app.%d{yyyy-MM-dd}.%i.log.gz
logging.logback.rollingpolicy.max-file-size=100MB
logging.logback.rollingpolicy.max-history=30
logging.logback.rollingpolicy.total-size-cap=1GB
logging.logback.rollingpolicy.clean-history-on-start=true

NOTE

这个配置会:

  • 当文件超过 100MB 时创建新文件
  • 保留最近 30 天的日志
  • 总大小不超过 1GB
  • 启动时清理过期日志

结构化日志:现代应用的最佳实践

为什么需要结构化日志?

传统的文本日志在现代微服务架构中面临挑战:

text
2025-01-01 10:15:00 INFO  用户 john 在 2025-01-01 10:14:58 购买了商品 iPhone15,金额 8999.00,订单号 ORDER123456
2025-01-01 10:15:01 ERROR 支付失败,用户 mary,订单 ORDER123457,原因:余额不足
json
{
  "@timestamp": "2025-01-01T10:15:00.000Z",
  "level": "INFO",
  "message": "用户购买商品成功",
  "user": "john",
  "product": "iPhone15",
  "amount": 8999.00,
  "order_id": "ORDER123456",
  "action": "purchase"
}

配置结构化日志

Spring Boot 支持多种结构化日志格式:

properties
# 启用 Elastic Common Schema 格式
logging.structured.format.console=ecs
logging.structured.format.file=ecs

# 自定义服务信息
logging.structured.ecs.service.name=user-service
logging.structured.ecs.service.version=1.0.0
logging.structured.ecs.service.environment=production
properties
# 启用 Graylog Extended Log Format
logging.structured.format.console=gelf
logging.structured.format.file=gelf

# 自定义主机信息
logging.structured.gelf.host=user-service
logging.structured.gelf.service.version=1.0.0
properties
# 启用 Logstash JSON 格式
logging.structured.format.console=logstash
logging.structured.format.file=logstash

在代码中使用结构化日志

kotlin
@Service
class OrderService {
    private val logger = LoggerFactory.getLogger(OrderService::class.java)
    
    fun processOrder(order: Order) {
        // 使用 SLF4J 的流式 API 添加结构化数据
        logger.atInfo()
            .addKeyValue("user_id", order.userId) 
            .addKeyValue("order_id", order.id) 
            .addKeyValue("amount", order.totalAmount) 
            .addKeyValue("payment_method", order.paymentMethod) 
            .log("订单处理开始")
        
        try {
            // 处理订单逻辑
            paymentService.processPayment(order)
            
            logger.atInfo()
                .addKeyValue("order_id", order.id)
                .addKeyValue("status", "completed")
                .log("订单处理成功")
                
        } catch (e: PaymentException) {
            logger.atError()
                .addKeyValue("order_id", order.id)
                .addKeyValue("error_code", e.errorCode)
                .addKeyValue("error_message", e.message)
                .setCause(e) 
                .log("订单支付失败")
        }
    }
}

自定义结构化日志格式

如果内置格式不满足需求,你可以创建自定义格式:

自定义格式实现示例
kotlin
import ch.qos.logback.classic.spi.ILoggingEvent
import org.springframework.boot.logging.structured.StructuredLogFormatter
import com.fasterxml.jackson.databind.ObjectMapper
import java.time.Instant

class CustomStructuredLogFormatter : StructuredLogFormatter<ILoggingEvent> {
    
    private val objectMapper = ObjectMapper()
    
    override fun format(event: ILoggingEvent): String {
        val logData = mutableMapOf<String, Any>()
        
        // 基础信息
        logData["timestamp"] = Instant.ofEpochMilli(event.timeStamp).toString()
        logData["level"] = event.level.toString()
        logData["logger"] = event.loggerName
        logData["thread"] = event.threadName
        logData["message"] = event.formattedMessage
        
        // 添加 MDC 数据
        event.mdcPropertyMap?.let { mdc ->
            logData["context"] = mdc
        }
        
        // 添加异常信息
        event.throwableProxy?.let { throwable ->
            logData["exception"] = mapOf(
                "class" to throwable.className,
                "message" to throwable.message,
                "stackTrace" to throwable.stackTraceElementProxyArray
                    .take(10) // 只保留前10行堆栈
                    .map { it.toString() }
            )
        }
        
        // 自定义业务字段
        logData["service"] = "my-custom-service"
        logData["version"] = "1.0.0"
        
        return objectMapper.writeValueAsString(logData) + "\n"
    }
}
properties
# 使用自定义格式
logging.structured.format.console=com.example.CustomStructuredLogFormatter

高级配置技巧

1. 环境特定的日志配置

使用 Profile 特定的配置文件:

xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    
    <!-- 开发环境配置 -->
    <springProfile name="dev">
        <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
            <encoder>
                <pattern>%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} %clr(%5p) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n</pattern>
            </encoder>
        </appender>
        <root level="DEBUG">
            <appender-ref ref="CONSOLE"/>
        </root>
    </springProfile>
    
    <!-- 生产环境配置 -->
    <springProfile name="prod">
        <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
            <file>logs/application.log</file>
            <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
                <fileNamePattern>logs/application.%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern>
                <maxFileSize>100MB</maxFileSize>
                <maxHistory>30</maxHistory>
                <totalSizeCap>1GB</totalSizeCap>
            </rollingPolicy>
            <encoder class="org.springframework.boot.logging.logback.StructuredLogEncoder">
                <format>ecs</format>
            </encoder>
        </appender>
        <root level="INFO">
            <appender-ref ref="FILE"/>
        </root>
    </springProfile>
    
</configuration>
properties
# 开发环境 - 详细日志
logging.level.root=DEBUG
logging.level.com.example=TRACE
spring.output.ansi.enabled=always
properties
# 生产环境 - 精简日志
logging.level.root=WARN
logging.level.com.example=INFO
logging.structured.format.file=ecs
logging.file.name=logs/app.log

2. 性能监控日志

创建专门的性能监控切面:

kotlin
@Aspect
@Component
class PerformanceLoggingAspect {
    
    private val logger = LoggerFactory.getLogger("PERFORMANCE")
    
    @Around("@annotation(Timed)")
    fun logExecutionTime(joinPoint: ProceedingJoinPoint): Any? {
        val startTime = System.currentTimeMillis()
        val methodName = "${joinPoint.signature.declaringTypeName}.${joinPoint.signature.name}"
        
        return try {
            val result = joinPoint.proceed()
            val executionTime = System.currentTimeMillis() - startTime
            
            logger.atInfo()
                .addKeyValue("method", methodName) 
                .addKeyValue("execution_time_ms", executionTime) 
                .addKeyValue("status", "success") 
                .log("方法执行完成")
                
            result
        } catch (e: Exception) {
            val executionTime = System.currentTimeMillis() - startTime
            
            logger.atError()
                .addKeyValue("method", methodName)
                .addKeyValue("execution_time_ms", executionTime)
                .addKeyValue("status", "error")
                .addKeyValue("error", e.message)
                .setCause(e) 
                .log("方法执行失败")
                
            throw e
        }
    }
}

// 使用注解
@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
annotation class Timed

@Service
class UserService {
    
    @Timed
    fun createUser(user: User): User {
        // 业务逻辑
        return userRepository.save(user)
    }
}

日志最佳实践

1. 日志级别使用指南

日志级别选择原则

  • ERROR: 系统错误,需要立即处理
  • WARN: 潜在问题,需要关注但不影响核心功能
  • INFO: 重要的业务流程信息
  • DEBUG: 详细的调试信息,仅在开发/测试环境使用
  • TRACE: 最详细的跟踪信息,通常用于框架内部

2. 避免常见的日志陷阱

kotlin
class BadLoggingExample {
    private val logger = LoggerFactory.getLogger(BadLoggingExample::class.java)
    
    fun processUser(user: User) {
        // 错误1:日志级别判断缺失,影响性能
        logger.debug("Processing user: " + user.toString()) 
        
        // 错误2:敏感信息泄露
        logger.info("User password: ${user.password}") 
        
        // 错误3:异常信息丢失
        try {
            // 业务逻辑
        } catch (e: Exception) {
            logger.error("Error occurred") 
        }
        
        // 错误4:过度日志记录
        logger.debug("Step 1 completed") 
        logger.debug("Step 2 completed") 
        logger.debug("Step 3 completed") 
    }
}
kotlin
class GoodLoggingExample {
    private val logger = LoggerFactory.getLogger(GoodLoggingExample::class.java)
    
    fun processUser(user: User) {
        // 正确1:使用参数化日志,避免不必要的字符串拼接
        logger.debug("Processing user: {}", user.id) 
        
        // 正确2:脱敏处理敏感信息
        logger.info("User login: {}", user.username.maskSensitive()) 
        
        // 正确3:完整的异常信息记录
        try {
            // 业务逻辑
        } catch (e: Exception) {
            logger.error("Failed to process user: {}", user.id, e) 
        }
        
        // 正确4:合理的日志粒度
        logger.info("User processing completed: {}", user.id) 
    }
    
    // 敏感信息脱敏扩展函数
    private fun String.maskSensitive(): String {
        return if (length <= 4) "****" 
               else "${take(2)}${"*".repeat(length - 4)}${takeLast(2)}"
    }
}

3. 日志监控与告警

结合结构化日志实现智能监控:

kotlin
@Component
class AlertingService {
    private val logger = LoggerFactory.getLogger("ALERT")
    
    fun sendCriticalAlert(message: String, context: Map<String, Any>) {
        logger.atError()
            .addKeyValue("alert_type", "critical") 
            .addKeyValue("alert_source", "application") 
            .addKeyValue("timestamp", Instant.now().toString()) 
            .apply { 
                context.forEach { (key, value) -> 
                    addKeyValue(key, value) 
                }
            }
            .log(message)
    }
}

总结

Spring Boot 的日志系统为我们提供了强大而灵活的日志管理能力。通过合理的配置和使用,我们可以:

  • 🎯 快速定位问题:通过结构化日志和合理的日志级别
  • 📊 监控系统健康:通过性能日志和告警机制
  • 🔧 优化系统性能:通过日志分析发现瓶颈
  • 🛡️ 增强系统安全:通过审计日志追踪操作

IMPORTANT

记住:好的日志不仅仅是记录信息,更是系统可观测性的基础。合理的日志策略能让你的应用在复杂的生产环境中游刃有余!

🎉 现在你已经掌握了 Spring Boot 日志系统的核心知识,快去实践中应用这些技巧吧!