Spring Boot 动态修改 Logger 的日志级别

在 Spring Boot 应用中,默认使用 Logback 来记录日志。可以在 application.yaml 或者是 logback-spring.xml 中配置 Logger 的日志级别。

有以下几个常见的日志级别(从低到高):

  • TRACE(跟踪):最低级别的日志,用于输出详细的调试信息,通常用于追踪代码的执行路径。
  • DEBUG(调试):用于输出调试信息,帮助开发人员调试应用程序。
  • INFO(信息):用于输出一般性的信息,例如应用程序的启动信息、重要事件等。
  • WARN(警告):用于输出警告信息,表示潜在的问题或不符合预期的情况,但不会影响应用程序的正常运行。
  • ERROR(错误):最高级别的日志,用于输出错误信息,表示发生了一个错误或异常情况,可能会影响应用程序的正常运行。

级别越低,包含的信息越详细,级别越高,包含的信息越严重和重要。当设置日志级别时,只有达到或高于该级别的日志才会被记录和输出,而低于该级别的日志将被忽略。如:如果将日志级别设置为 INFO,则会记录和输出 INFOWARNERROR 级别的日志,而 DEBUGTRACE 级别的日志将被忽略。这有助于控制日志的详细程度和输出量,以适应特定的调试或生产环境要求。

Log4j 建议只使用 ERRORWARNINFODEBUG 四个级别。

ROOT Logger 的配置为例

application.yaml 中配置

logging:
  level:
    ROOT: DEBUG

logback-spring.xml 中配置

首先,需要在 application.yaml 中配置 logback-spring.xml 配置文件的位置:

logging:
  config: classpath:logback-spring.xml

然后,在 logback-spring.xml 中配置 ROOT Logger 的日志级别:

<configuration>
    <!-- 继承 Spring 预定义的 Logback 配置 -->
    <include resource="org/springframework/boot/logging/logback/base.xml"/>

    <!-- 忽略其他的日志配置 ... -->

    <!-- ROOT Logger 的日志级别 -->
    <root level="DEBUG">
        <!-- 输出到控制台 -->
        <appender-ref ref="CONSOLE" />
    </root>
</configuration>

在运行时修改 Logger 的日志级别

在使用配置文件的情况下,如果想要修改某个 Logger 的日志级别就要修改配置文件,然后重新启动应用。

好在日志实现库提供了对应的 API 来在运行时动态地修改 Logger 的日志级别,通过这些 API 我们可以很轻松地在应用中管理 Logger。

logback-spring.xml 可以通过配置,定时地检测配置修改,但是这会带来额外的资源消耗。

<configuration scan="true" scanPeriod="30 seconds" > 
 <!-- ... 具体配置 -->
</configuration>

Sl4j 和 Logback

如上所述,Spring Boot 默认使用 Logback 作为日志实现,但是在 Spring Boot 应用中,我们往往使用 sl4j 的 API 来记录日志。

org.slf4j.Logger

所以,Sl4j 和 Logback 是什么关系?

简而言之,SLF4J (Simple Logging Facade for Java)是一个日志门面,而 Logback 是 SLF4J 的一个实现,用于实际记录日志。开发人员可以使用 SLF4J 接口编写日志代码,并选择 Logback 或其他 SLF4J 实现作为底层日志记录器。这种分离的设计允许在不改变应用代码的情况下更换日志实现。

LoggerController

创建 LoggerController, 用于管理系统中的 Logger。

package cn.springdoc.demo.web.controller;

import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

import org.slf4j.LoggerFactory;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.Logger;
import ch.qos.logback.classic.LoggerContext;

/**
 * Logger 管理
 * @author springdoc.cn
 */
@RestController
@RequestMapping("/loggers")
public class LoggerController {

    // Logger
    // 注意,这里使用的是 org.slf4j.Logger 接口
    static final org.slf4j.Logger log = LoggerFactory.getLogger(LoggerController.class);

    /**
     * 查看logger列表
     * @return
     */
    @GetMapping
    public ResponseEntity<List<Map<String, String>>> loggers() {
        
        // 获取到 LoggerContext
        LoggerContext loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory();
        
        // 获取系统中定义的所有 logger
        List<Map<String, String>> loggers = loggerContext.getLoggerList().stream().map(logger -> {
            // 映射为 Map,key 是 logger 名称,value 是其日志级别
            // logger名称 = logger有效级别
            return Collections.singletonMap(logger.getName(), logger.getEffectiveLevel().levelStr);
        }).collect(Collectors.toList());
        
        return ResponseEntity.ok(loggers);
    }

    /**
     * 修改日志级别
     * @param name
     * @param level
     * @return
     */
    @PostMapping
    public ResponseEntity<Object> setLevel(@RequestParam("name") String name,
                            @RequestParam("level") String level) {
        
        // 获取到 LoggerContext
        LoggerContext loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory();
        
        // 根据指定的名称获取 logger
        Logger logger = loggerContext.exists(name);
        if (logger == null) {
            return ResponseEntity.badRequest().body(name + " 日志记录器不存在");
        }
        
        // 解析 level 参数,第二个参数表示当 level 参数非法时的默认值
        Level newLevel = Level.toLevel(level, null);
        
        if (newLevel == null) {
            return ResponseEntity.badRequest().body(level + " 不是合法的日志级别");
        }
        
        // 重写设置 logger 的 level
        logger.setLevel(newLevel);
        
        return ResponseEntity.noContent().build();
    }

    /**
     * 测试日志输出
     * @return
     */
    @GetMapping("/test")
    public ResponseEntity<Void> test (){
        
        log.debug("Hello springdoc.cn");
        log.info("Hello springdoc.cn");
        log.warn("Hello springdoc.cn");
        log.error("Hello springdoc.cn");
        
        return ResponseEntity.noContent().build();
    }
}

如上,首先在类成员变量为 Controller 定义了一个 log Logger,注意这里使用了 sl4j 全路径 org.slf4j.Logger(避免和 Logback 的 Logger 实现冲突),该 Logger 的名称为类的全路径,即:cn.springdoc.demo.web.controller.LoggerController

loggers() 方法是一个 @GetMapping 端点,它通过 LogbackLoggerContext API 获取到系统中定义的所有 Logger 实例,映射为 Map(Logger 名称为 KEY,Logger 日志级别为 Value)后返回给客户端。

setLevel 方法是一个 @PostMapping 端点,用于动态地修改指定 Logger 的日志级别。它接受 2 个表单参数:

  1. name:Logger 的名称
  2. level:日志级别,它是一个枚举值:ALLTRACEDEBUGINFOWARNERROR

setLevel 方法会根据 name 参数检索系统中的 Logger,如果 Logger 不存在则给客户端返回错误信息。然后解析 level 参数为 ch.qos.logback.classic.Level 对象,同样,如果 level 参数非法的话,也会给客户端返回错误信息。最后,修改 Logger 的日志级别。

最后还有一个 test() 方法,用于测试修改是否生效。该方法中,使用 log 输出了 debuginfowarnerror 级别的 4 条日志,级别由低到高。

测试

首先,在 application.yaml 中配置默认的日志级别为 DEBUG

logging:
  level:
    ROOT: DEBUG

启动应用,首先访问 http://localhost:8080/loggers 获取系统中的所有 Logger:

[
    {
        "ROOT": "DEBUG"
    },
    // .... 省略其他无关的 Logger
    // LoggerController Logger
    {
        "cn.springdoc.demo.web.controller.LoggerController": "DEBUG"
    },
]

返回的 JSON 数组就是系统中的所有 Logger 定义,KEY 就是 Logger 名称,VALUE 则是其日志级别。实际上,整个系统中定义的 Logger 有数百个,这里为了方便演示,删减了其他所有无关的 Logger。

然后,访问 localhost:8080/loggers/test 测试端点,查看服务端控制台的日志输出:

DEBUG 5344 --- [nio-8080-exec-4] c.s.d.web.controller.LoggerController    : Hello springdoc.cn
 INFO 5344 --- [nio-8080-exec-4] c.s.d.web.controller.LoggerController    : Hello springdoc.cn
 WARN 5344 --- [nio-8080-exec-4] c.s.d.web.controller.LoggerController    : Hello springdoc.cn
ERROR 5344 --- [nio-8080-exec-4] c.s.d.web.controller.LoggerController    : Hello springdoc.cn

如上,由于默认配置的日志级别为 DEBUG,所以测试方法中的所有日志都会正常输出。

现在,尝试调用 localhost:8080/loggers 接口,修改 cn.springdoc.demo.web.controller.LoggerController Logger 的日志级别为 INFO

curl -d "name=cn.springdoc.demo.web.controller.LoggerController&level=INFO" -X POST http://localhost:8080/loggers

然后,再次请求 localhost:8080/loggers/test 测试端点,并查看服务端控制台的日志输出:

 INFO 5344 --- [nio-8080-exec-6] c.s.d.web.controller.LoggerController    : Hello springdoc.cn
 WARN 5344 --- [nio-8080-exec-6] c.s.d.web.controller.LoggerController    : Hello springdoc.cn
ERROR 5344 --- [nio-8080-exec-6] c.s.d.web.controller.LoggerController    : Hello springdoc.cn

设置成功,低于 INFO 级别的 DEBUG 日志已经不会被输出到控制台了。

接着,重新修改 cn.springdoc.demo.web.controller.LoggerController Logger 的日志级别为 DEBUG

curl -d "name=cn.springdoc.demo.web.controller.LoggerController&level=DEBUG" -X POST http://localhost:8080/loggers

再次请求测试端点,查看服务端控制台输出:

DEBUG 5344 --- [nio-8080-exec-9] c.s.d.web.controller.LoggerController    : Hello springdoc.cn
 INFO 5344 --- [nio-8080-exec-9] c.s.d.web.controller.LoggerController    : Hello springdoc.cn
 WARN 5344 --- [nio-8080-exec-9] c.s.d.web.controller.LoggerController    : Hello springdoc.cn
ERROR 5344 --- [nio-8080-exec-9] c.s.d.web.controller.LoggerController    : Hello springdoc.cn

一切 OK,Logger 的日志级别修改为 DEBUG 后,测试方法中所有级别的日志记录都正常输出了。