SpringBoot3.x日志生产最佳实践原来是这样!

简介: SpringBoot3.x日志生产最佳实践原来是这样!

0 前言

SpringBoot对日志的配置和加载进行了封装,让我们可以很方便地使用一些日志框架,只需要定义对应日志框架的配置文件,如LogBack、Log4j、Log4j2等,代码内部便可以直接使用。


如我们在resources目录下定义了一个logback xml文件,文件内容是logback相关配置,然后就可以直接在代码在使用Logger记录日志啦:


SpringBoot对日志功能的封装:

1 LoggingSystem内部结构

1.1 SpringBoot3.0默认支持的日志类型

  • JDK内置的Log(JavaLoggingSystem)
  • Log4j2(Log4J2LoggingSyststem)
  • Logback(LogbackLoggingSystem)

LoggingSystem是个抽象类,内部

1.2 API

  1. beforeInitialize:日志系统初始化之前需要处理的事。抽象方法

  2. initialize:初始化日志系统。默认不进行任何处理,需子类初始化
  3. cleanUp:日志系统的清除工作。默认不进行任何处理,需子类清除
  4. getShutdownHandler:返回一个Runnable,用于当JVM退出时处理日志系统关闭后需要进行的操作,默认null
  5. setLogLevel:抽象方法,设置对应logger级别

1.3 AbstractLoggingSystem抽象类

继承LoggingSystem抽象类进行扩展,实现beforeInitialize方法,但内部无任何处理。重点在initialize

重写initialize
@Override
public void initialize(LoggingInitializationContext initializationContext,
    String configLocation, LogFile logFile) {
  // 如传递了日志配置文件,则使用指定文件
  if (StringUtils.hasLength(configLocation)) {
    initializeWithSpecificConfig(initializationContext, configLocation, logFile);
    return;
  }
  // 没传递日志配置文件,使用约定方式
  initializeWithConventions(initializationContext, logFile);
}
① 指定日志文件
private void initializeWithSpecificConfig(
    LoggingInitializationContext initializationContext, String configLocation,
    LogFile logFile) {
  // 处理日志配置文件中的占位符
  configLocation = SystemPropertyUtils.resolvePlaceholders(configLocation);
  loadConfiguration(initializationContext, configLocation, logFile);
}
② 约定配置文件
private void initializeWithConventions(
    LoggingInitializationContext initializationContext, LogFile logFile) {
  // 获取自初始化的日志配置文件,该方法会使用getStandardConfigLocations抽象方法得到的文件数组
  // 然后进行遍历,如果文件存在,返回对应的文件目录。注意这里的文件指的是classpath下的文件
  String config = getSelfInitializationConfig();
  // 如果找到对应的日志配置文件并且logFile为null(logFile为null表示只有console会输出)
  if (config != null && logFile == null) {
    // 调用reinitialize方法重新初始化
    // 默认的reinitialize方法不做任何处理,logback,log4j和log4j2覆盖了这个方法,会进行处理
    reinitialize(initializationContext);
    return;
  }
  // 如果没有找到对应的日志配置文件
  if (config == null) {
    // 获取日志配置文件
    // 该方法与getSelfInitializationConfig方法的区别在于getStandardConfigLocations方法得到的文件数组内部遍历的逻辑
    // getSelfInitializationConfig方法直接遍历并判断classpath下是否存在对应的文件
    // getSpringInitializationConfig方法遍历后判断的文件名会在后缀前加上 "-spring" 字符串
    // 比如查找logback.xml文件,getSelfInitializationConfig会直接查找classpath下是否存在logback.xml文件,而getSpringInitializationConfig方法会判断classpath下是否存在logback-spring.xml文件
    config = getSpringInitializationConfig();
  }
  // 如找到对应日志配置文件
  if (config != null) {
    // 调用抽象方法,子类实现
    loadConfiguration(initializationContext, config, logFile);
    return;
  }
  // 还没找到日志配置文件,调用抽象方法加载
  loadDefaults(initializationContext, logFile);
}
protected abstract String[] getStandardConfigLocations();
protected abstract void loadConfiguration(
    LoggingInitializationContext initializationContext, String location,
    LogFile logFile);
protected abstract void loadDefaults(
    LoggingInitializationContext initializationContext, LogFile logFile);

以LogbackLoggingSystem.java为例,看具体的

1.4 初始化过程

根据AbstractLoggingSystem

使用logback日志库时,会查找classpath下是否存在这些文件:

  • logback-test.groovy
  • logback-test.xml
  • logback.groovy
  • logback.xml
  • logback-test-spring.groovy
  • logback-test-spring.xml
  • logback-spring.groovy
  • logback-spring.xml
@Override
protected String[] getStandardConfigLocations() {
  return new String[] { "logback-test.groovy", "logback-test.xml", "logback.groovy",
      "logback.xml" };
}
@Override
protected void loadConfiguration(LoggingInitializationContext initializationContext,
    String location, LogFile logFile) {
  // 调用父类Slf4JLoggingSystem的方法
  super.loadConfiguration(initializationContext, location, logFile);
  // 获取slf4j内部的LoggerContext
  LoggerContext loggerContext = getLoggerContext();
  // logback环境的一些配置配置处理
  stopAndReset(loggerContext);
  try {
    configureByResourceUrl(initializationContext, loggerContext,
        ResourceUtils.getURL(location));
  }
  catch (Exception ex) {
    throw new IllegalStateException(
        "Could not initialize Logback logging from " + location, ex);
  }
  List<Status> statuses = loggerContext.getStatusManager().getCopyOfStatusList();
  StringBuilder errors = new StringBuilder();
  for (Status status : statuses) {
    if (status.getLevel() == Status.ERROR) {
      errors.append(errors.length() > 0 ? "\n" : "");
      errors.append(status.toString());
    }
  }
  if (errors.length() > 0) {
    throw new IllegalStateException(
        "Logback configuration error " + "detected: \n" + errors);
  }
}
// 没找到日志配置文件的话使用loadDefaults方法加载
@Override
protected void loadDefaults(LoggingInitializationContext initializationContext,
    LogFile logFile) {
  // 获取slf4j内部的LoggerContext
  LoggerContext context = getLoggerContext();
  stopAndReset(context);
  LogbackConfigurator configurator = new LogbackConfigurator(context);
  context.putProperty("LOG_LEVEL_PATTERN",
      initializationContext.getEnvironment().resolvePlaceholders(
          "${logging.pattern.level:${LOG_LEVEL_PATTERN:%5p}}"));
  // 构造默认的console Appender。如果logFile不为空,还会构造file Appender
  new DefaultLogbackConfiguration(initializationContext, logFile)
      .apply(configurator);
  context.setPackagingDataEnabled(true);
}
// logback的清除工作
@Override
public void cleanUp() {
  super.cleanUp();
  getLoggerContext().getStatusManager().clear();
}
// 动态设置logger的level
@Override
public void setLogLevel(String loggerName, LogLevel level) {
  getLogger(loggerName).setLevel(LEVELS.get(level));
}
// 清除后的一些工作
// ShutdownHandler会调用LoggerContext的stop方法
@Override
public Runnable getShutdownHandler() {
  return new ShutdownHandler();
}

2 LoggingSystem的初始化

LoggingApplicationListener是ApplicationListener接口的实现类,会被 SpringBoot 使用工厂加载机制加载。

2.1 spring.factories

spring-boot-3.0.0.jar/META-INF/spring.factories:

注意到LoggingApplicationListener,和 SpringBoot 启动流程关联点:

2.2 SpringApplication.java

@SuppressWarnings({ "unchecked", "rawtypes" })
public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
  this.resourceLoader = resourceLoader;
  Assert.notNull(primarySources, "PrimarySources must not be null");
  this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
  this.webApplicationType = deduceWebApplicationType();
  setInitializers((Collection) getSpringFactoriesInstances(
      ApplicationContextInitializer.class));
  // 使用工厂加载机制找到这些Listener
  setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
  this.mainApplicationClass = deduceMainApplicationClass();
}

2.3 LoggingApplicationListener.java

SpringApplication#run执行时触发该事件

@Override
public void onApplicationEvent(ApplicationEvent event) {
  if (event instanceof ApplicationStartedEvent) {
    // 先得到LoggingSystem,再调用beforeInitialize
    onApplicationStartedEvent((ApplicationStartedEvent) event);
  }
  ...
}
private void onApplicationStartedEvent(ApplicationStartedEvent event) {
  // get会从下面那段static代码块得到Map中遍历
  // 如对应的key(key是某个类的全名)在classloader中存在,构造该key对应的value对应的LoggingSystem
  this.loggingSystem = LoggingSystem
      .get(event.getSpringApplication().getClassLoader());
  this.loggingSystem.beforeInitialize();
}
static {
  Map<String, String> systems = new LinkedHashMap<>();
  systems.put("ch.qos.logback.core.Appender",
      "org.springframework.boot.logging.logback.LogbackLoggingSystem");
  systems.put("org.apache.logging.log4j.core.impl.Log4jContextFactory",
      "org.springframework.boot.logging.log4j2.Log4J2LoggingSystem");
  systems.put("java.util.logging.LogManager",
      "org.springframework.boot.logging.java.JavaLoggingSystem");
  SYSTEMS = Collections.unmodifiableMap(systems);
}

spring-boot-starter模块内部会引用spring-boot-starter-logging模块,这starter-logging模块内部会引入logback相关依赖。这依赖会导致LoggingSystem的静态方法get获取LoggingSystem时得到LogbackLoggingSystem。


因此springboot程序使用logback作默认日志。前提都是以LogbackLoggingSystem作为日志系统。

2.4 FAQ

① 项目无任何日志配置

执行到AbstractLoggingSystem#initialize时,日志配置文件为null:

最后只能调loadDefaults进行加载,LogbackLoggingSystem#loadDefaults方法,由于logFile为null,所以最终只构造个ConsoleAppender。

所以项目没有任何日志配置时,默认就是在控制台打印了项目启动信息。

② 项目无任何logback配置,只有yaml中配置logging.file/path

logging.file和logging.path的配置在LogFile这个日志文件类中生效。

比如yaml配置如下(只定义了logging.file):

logging:
  file: /tmp/temp.log

这时FileAppender对应file是/tmp/spring.log文件。

LogFile.java

@Override
public String toString() {
  // 如果配置了logging.file,直接使用该文件
  if (StringUtils.hasLength(this.file)) {
    return this.file;
  }
  // 否则使用logging.path目录,在该目录下创建spring.log日志文件
  String path = this.path;
  if (!path.endsWith("/")) {
    path = path + "/";
  }
  return StringUtils.applyRelativePath(path, "spring.log");
}

所以若配置了logging.path/file,生效的只有logging.file配置。

③ resources下有logback.xml配置

相当于classpath下存在logback.xml文件。LogbackLoggingSystem#getStandardConfigLocations返回如下:

  1. logback-test.groovy或logback-test-spring.groovy
  2. logback-test.xml或logback-test-spring.xml
  3. logback.groovy或logback-spring.groovy
  4. logback.xml或logback-spring.xml

在resources目录下定义logback-spring.xml文件,内容如下:

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <!--<pattern>%d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger[%line] &#45;&#45; %msg%n</pattern>-->
            <pattern>%d{YYYY-MM-dd} [%thread] %-5level %logger[%line] -- %msg%n</pattern>
        </encoder>
    </appender>
    <root level="INFO">
        <appender-ref ref="STDOUT"/>
    </root>
</configuration>

这时logging.file配置失效,这是因为没有调用loadDefaults方法(loadDefaults方法内部会把LogFile构造成FileAppender),而是调用了loadConfiguration方法,该方法会根据logback.xml文件中的配置去构造Appender。

④ resources下有my-logback.xml配置

由于LogbackLoggingSystem中没有对my-logback.xml路径的解析,所有不会被识别,但是可以在yaml中配置logging.config配置:

logging:
  config: classpath:my-logback.xml

这样配置就能识别my-logback.xml文件。

3 NoOpLoggingSystem

SpringBoot内部的NoOpLoggingSystem,这个日志系统内部什么都不做,构造过程:

public static LoggingSystem get(ClassLoader classLoader) {
  // SYSTEM_PROPERTY静态变量是LoggingSystem的类全名
  String loggingSystem = System.getProperty(SYSTEM_PROPERTY);
  if (StringUtils.hasLength(loggingSystem)) {
    if (NONE.equals(loggingSystem)) { // None静态变量是值是none
      return new NoOpLoggingSystem();
    }
    return get(classLoader, loggingSystem);
  }
  for (Map.Entry<String, String> entry : SYSTEMS.entrySet()) {
    if (ClassUtils.isPresent(entry.getKey(), classLoader)) {
      return get(classLoader, entry.getValue());
    }
  }
  throw new IllegalStateException("No suitable logging system located");
}

加上启动参数:

-Dorg.springframework.boot.logging.LoggingSystem=none

即可构造NoOpLoggingSystem。

相关实践学习
通过日志服务实现云资源OSS的安全审计
本实验介绍如何通过日志服务实现云资源OSS的安全审计。
目录
相关文章
|
6月前
|
Prometheus 监控 Java
日志收集和Spring 微服务监控的最佳实践
在微服务架构中,日志记录与监控对系统稳定性、问题排查和性能优化至关重要。本文介绍了在 Spring 微服务中实现高效日志记录与监控的最佳实践,涵盖日志级别选择、结构化日志、集中记录、服务ID跟踪、上下文信息添加、日志轮转,以及使用 Spring Boot Actuator、Micrometer、Prometheus、Grafana、ELK 堆栈等工具进行监控与可视化。通过这些方法,可提升系统的可观测性与运维效率。
615 1
日志收集和Spring 微服务监控的最佳实践
|
6月前
|
负载均衡 监控 安全
5 个 IIS 日志记录最佳实践
IIS日志记录是监控Web服务器性能与安全的关键。本文介绍启用日志、应用池配置、负载均衡、敏感数据防护、日志集中管理及保留策略等五大最佳实践,助力高效分析与合规审计。
386 1
|
12月前
|
存储 Java 文件存储
微服务——SpringBoot使用归纳——Spring Boot使用slf4j进行日志记录—— logback.xml 配置文件解析
本文解析了 `logback.xml` 配置文件的详细内容,包括日志输出格式、存储路径、控制台输出及日志级别等关键配置。通过定义 `LOG_PATTERN` 和 `FILE_PATH`,设置日志格式与存储路径;利用 `&lt;appender&gt;` 节点配置控制台和文件输出,支持日志滚动策略(如文件大小限制和保存时长);最后通过 `&lt;logger&gt;` 和 `&lt;root&gt;` 定义日志级别与输出方式。此配置适用于精细化管理日志输出,满足不同场景需求。
2871 1
|
6月前
|
缓存 Java 应用服务中间件
Spring Boot配置优化:Tomcat+数据库+缓存+日志,全场景教程
本文详解Spring Boot十大核心配置优化技巧,涵盖Tomcat连接池、数据库连接池、Jackson时区、日志管理、缓存策略、异步线程池等关键配置,结合代码示例与通俗解释,助你轻松掌握高并发场景下的性能调优方法,适用于实际项目落地。
1112 5
|
12月前
|
Java 微服务 Spring
微服务——SpringBoot使用归纳——Spring Boot使用slf4j进行日志记录——使用Logger在项目中打印日志
本文介绍了如何在项目中使用Logger打印日志。通过SLF4J和Logback,可设置不同日志级别(如DEBUG、INFO、WARN、ERROR)并支持占位符输出动态信息。示例代码展示了日志在控制器中的应用,说明了日志配置对问题排查的重要性。附课程源码下载链接供实践参考。
1288 0
|
12月前
|
SQL Java 数据库连接
微服务——SpringBoot使用归纳——Spring Boot使用slf4j进行日志记录—— application.yml 中对日志的配置
在 Spring Boot 项目中,`application.yml` 文件用于配置日志。通过 `logging.config` 指定日志配置文件(如 `logback.xml`),实现日志详细设置。`logging.level` 可定义包的日志输出级别,例如将 `com.itcodai.course03.dao` 包设为 `trace` 级别,便于开发时查看 SQL 操作。日志级别从高到低为 ERROR、WARN、INFO、DEBUG,生产环境建议调整为较高级别以减少日志量。本课程采用 yml 格式,因其层次清晰,但需注意格式要求。
1122 0
|
12月前
|
Java API 开发者
微服务——SpringBoot使用归纳——Spring Boot使用slf4j进行日志记录——slf4j 介绍
在软件开发中,`System.out.println()`常被用于打印信息,但大量使用会增加资源消耗。实际项目推荐使用slf4j结合logback输出日志,效率更高。Slf4j(Simple Logging Facade for Java)是一个日志门面,允许开发者通过统一方式记录日志,无需关心具体日志系统。它支持灵活切换日志实现(如log4j或logback),且具备简洁占位符和日志级别判断等优势。阿里巴巴《Java开发手册》强制要求使用slf4j,以保证日志处理方式的统一性和维护性。使用时只需通过`LoggerFactory`创建日志实例即可。
779 0
|
10月前
|
监控 容灾 算法
阿里云 SLS 多云日志接入最佳实践:链路、成本与高可用性优化
本文探讨了如何高效、经济且可靠地将海外应用与基础设施日志统一采集至阿里云日志服务(SLS),解决全球化业务扩展中的关键挑战。重点介绍了高性能日志采集Agent(iLogtail/LoongCollector)在海外场景的应用,推荐使用LoongCollector以获得更优的稳定性和网络容错能力。同时分析了多种网络接入方案,包括公网直连、全球加速优化、阿里云内网及专线/CEN/VPN接入等,并提供了成本优化策略和多目标发送配置指导,帮助企业构建稳定、低成本、高可用的全球日志系统。
1012 55
|
8月前
|
机器学习/深度学习 XML Java
【spring boot logback】日志logback格式解析
在 Spring Boot 中,Logback 是默认的日志框架,它支持灵活的日志格式配置。通过配置 logback.xml 文件,可以定义日志的输出格式、日志级别、日志文件路径等。
1447 5
|
11月前
|
缓存 安全 Java
深入解析HTTP请求方法:Spring Boot实战与最佳实践
这篇博客结合了HTTP规范、Spring Boot实现和实际工程经验,通过代码示例、对比表格和架构图等方式,系统性地讲解了不同HTTP方法的应用场景和最佳实践。
1004 5