跳转至

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开箱即用的日志配置包括:

  1. 控制台输出:默认输出到控制台
  2. 日志级别:Root级别为INFO
  3. 日志格式:包含时间戳、级别、PID、线程名、类名和消息
  4. 彩色输出:在支持的终端中显示彩色日志

默认日志格式

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:日志消息

日志级别

日志级别层次

从低到高依次为:

  1. TRACE:最详细的信息,通常只在开发时使用
  2. DEBUG:调试信息,用于诊断问题
  3. INFO:一般信息,程序正常运行的信息
  4. WARN:警告信息,可能出现问题但不影响运行
  5. ERROR:错误信息,程序出现错误但可以继续运行
  6. 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的日志系统具有以下特点:

  1. 开箱即用:默认配置适合大多数场景
  2. 灵活配置:支持多种配置方式和格式
  3. 性能优化:支持异步日志和条件日志
  4. 生产就绪:提供日志轮转、监控等企业级特性
  5. 可观测性:与监控系统良好集成

正确使用日志可以帮助我们: - 快速定位和解决问题 - 监控应用运行状态 - 分析业务数据和用户行为 - 满足合规和审计要求

记住:好的日志是应用可维护性的重要保障!