专注于 JetBrains IDEA 全家桶,永久激活,教程
持续更新 PyCharm,IDEA,WebStorm,PhpStorm,DataGrip,RubyMine,CLion,AppCode 永久激活教程

Java日志:日志级别动态调整

作为开发人员,定位问题是我们的日常工作,而日志是我们定位问题非常重要的依据。传统方式定位问题时,往往是如下步骤:

1、 将日志级别设低,例如 DEBUG ;
2、 重启应用;
3、 复现问题,观察日志;

实际上是可以动态修改日志级别,无需重启应用,立即生效。本文收集了3种动态修改日志级别的文章,分别是

1、 Spring Boot 2动态修改日志级别
2、 阿里在线诊断工具Arthas调整日志等级记录
3、 美团日志级别动态调整——小工具解决大问题

Spirng Boot动态修改日志级别

从 Spring Boot 1.5 开始,Spring Boot Actuator 组件就已提供动态修改日志级别的能力。

示例

1、引入spring-boot-starter-actuator依赖,内容如下:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

2、编写测试代码,如下:

@SpringBootApplication
public class DemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}

@RestController
public class DemoController {

    private static Logger logger = LoggerFactory.getLogger(DemoController.class);

    @GetMapping("/helloworld")
    public String helloworld(){
        logger.debug("welcome to learn spring boot");
        return "welcome to learn spring boot";
    }

}

3、配置文件

management:
  endpoints:
    web:
      exposure:
        include: 'loggers'

Spring Boot 2.x默认只暴露 /health 以及 /info 端点,而日志控制需要用到 /loggers 端点,故而需要设置将其暴露。

测试

/loggers端点提供了查看以及修改日志级别的能力。

1、 查看当前应用各包/类的日志级别 访问 http://localhost:8080/actuator/loggers ,可看到类似如下的结果:

![101\_1.png][101_1.png]

2、 查看指定包/类日志详情 访问 http://localhost:8080/actuator/loggers/com.blockmao.springboot.demo.DemoController ,可看到类似如下的结果:

![101\_2.png][101_2.png]

3、 修改日志级别 默认的日志级别是INFO,所以DemoController的debug日志不会打印。下面来尝试将该类的日志级别设为DEBUG,如下

![101\_3.png][101_3.png]

此时,访问 [http://localhost:8080/helloworld][http_localhost_8080_helloworld] 会看到类似如下的日志:

![101\_4.png][101_4.png]

并且,此时再访问 [http://localhost:8080/actuator/loggers/com.itmuch.logging.TestController][http_localhost_8080_actuator_loggers_com.itmuch.logging.TestController] ,可看到类似如下的结果:

![101\_5.png][101_5.png]

原理

Actuator有约定,/actuator/xxx端点的定义代码在 xxxEndpoint中。找到类org.springframework.boot.actuate.logging.LoggersEndpoint,代码如下:

@Endpoint(id = "loggers")
public class LoggersEndpoint {
    private final LoggingSystem loggingSystem;

    @WriteOperation
    public void configureLogLevel(@Selector String name,
            @Nullable LogLevel configuredLevel) {
        Assert.notNull(name, "Name must not be empty");
        this.loggingSystem.setLogLevel(name, configuredLevel);
    }
    // ...其他省略
}

其中, EndpointWriteOperation@Selector都是Spring Boot 2.0开始提供的新注解。

@Endpoint(id = "loggers")用来描述Spring Boot Actuator 的端点,这样就会产生一个/actuator/loggers的路径,它类似于Spring MVC的@RequestMapping("loggers")

@WriteOperation 表示这是一个写操作,它类似于Spring MVC的 @PostMapping 。Spring Boot Actuator还提供了其他操作,如下表:

Operation HTTP method
@ReadOperation GET
@WriteOperation POST
@DeleteOperation DELETE

@Selector用于筛选@Endpoint注解返回值的子集,它类似于Spring MVC的@PathVariable 。

这样,上面的代码就很好理解了— configureLogLevel 方法:送POST请求后,name就是我们传的包名或者类名,configuredLevel就是我们传的消息体。

org.springframework.boot.logging.LoggingSystem#setLogLevel是抽象方法,具体实现由子类完成。LoggingSystem类结构如下图所示:

101_6.png

LoggingSystem有这么多实现类,Spring Boot怎么知道什么情况下用什么LoggingSystem呢?可在 org.springframework.boot.logging.LoggingSystem 找到类似如下代码:

public abstract class LoggingSystem {
    private static final Map<String, String> SYSTEMS;

    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);
    }

    /**
     * Detect and return the logging system in use. Supports Logback and Java Logging.
     * @param classLoader the classloader
     * @return the logging system
     */
    public static LoggingSystem get(ClassLoader classLoader) {
        String loggingSystem = System.getProperty(SYSTEM_PROPERTY);
        if (StringUtils.hasLength(loggingSystem)) {
            if (NONE.equals(loggingSystem)) {
                return new NoOpLoggingSystem();
            }
            return get(classLoader, loggingSystem);
        }
        return SYSTEMS.entrySet().stream()
                .filter((entry) -> ClassUtils.isPresent(entry.getKey(), classLoader))
                .map((entry) -> get(classLoader, entry.getValue())).findFirst()
                .orElseThrow(() -> new IllegalStateException(
                        "No suitable logging system located"));
    }
  // 省略不相关内容...
}

由代码不难发现,其实就是构建了一个名为 SYSTEMS 的map,作为各种日志系统的字典;然后在 get 方法中,看应用是否加载了map中的类;如果加载了,就通过反射,初始化LoggingSystem 。例如:Spring Boot发现当前应用加载了ch.qos.logback.core.Appender ,就去实例化 org.springframework.boot.logging.logback.LogbackLoggingSystem

Arthas ognl命令动态修改日志级别

使用ognl命令可以动态修改日志级别,步骤如下:

1、 查找当前类的classLoaderHash

![101\_7.png][101_7.png]

2、 用OGNL获取logger

![101\_8.png][101_8.png]

> 可以发现日志使用的是Logback框架。

3、 单独设置DemoController的logger level

![101\_9.png][101_9.png]

4、 全局设置logger level

![101\_10.png][101_10.png]

如果使用的日志框架是log4j,则使用上述ognl命令则会报错。至于为什么?请阅读Java日志:SLF4J详解

美团日志级别动态调整小工具

1、 初始化:确定所使用的日志框架,获取配置文件中所有的Logger内存实例,并将它们的引用缓存到Map容器中。

    String type = StaticLoggerBinder.getSingleton().getLoggerFactoryClassStr();
    if (LogConstant.LOG4J_LOGGER_FACTORY.equals(type)) {
        logFrameworkType = LogFrameworkType.LOG4J;
        Enumeration enumeration = org.apache.log4j.LogManager.getCurrentLoggers();
        while (enumeration.hasMoreElements()) {
            org.apache.log4j.Logger logger = (org.apache.log4j.Logger) enumeration.nextElement();
            if (logger.getLevel() != null) {
                loggerMap.put(logger.getName(), logger);
            }
        }
        org.apache.log4j.Logger rootLogger = org.apache.log4j.LogManager.getRootLogger();
        loggerMap.put(rootLogger.getName(), rootLogger);
    } else if (LogConstant.LOGBACK_LOGGER_FACTORY.equals(type)) {
        logFrameworkType = LogFrameworkType.LOGBACK;
        ch.qos.logback.classic.LoggerContext loggerContext = (ch.qos.logback.classic.LoggerContext) LoggerFactory.getILoggerFactory();
        for (ch.qos.logback.classic.Logger logger : loggerContext.getLoggerList()) {
            if (logger.getLevel() != null) {
                loggerMap.put(logger.getName(), logger);
            }
        }
        ch.qos.logback.classic.Logger rootLogger = (ch.qos.logback.classic.Logger) LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME);
        loggerMap.put(rootLogger.getName(), rootLogger);
    } else if (LogConstant.LOG4J2_LOGGER_FACTORY.equals(type)) {
        logFrameworkType = LogFrameworkType.LOG4J2;
        org.apache.logging.log4j.core.LoggerContext loggerContext = (org.apache.logging.log4j.core.LoggerContext) org.apache.logging.log4j.LogManager.getContext(false);
        Map<String, org.apache.logging.log4j.core.config.LoggerConfig> map = loggerContext.getConfiguration().getLoggers();
        for (org.apache.logging.log4j.core.config.LoggerConfig loggerConfig : map.values()) {
            String key = loggerConfig.getName();
            if (StringUtils.isBlank(key)) {
                key = "root";
            }
            loggerMap.put(key, loggerConfig);
        }
    } else {
        logFrameworkType = LogFrameworkType.UNKNOWN;
        LOG.error("Log框架无法识别: type={}", type);
    }

2、 获取Logger列表:从本地Map容器取出。

    private String getLoggerList() {
        JSONObject result = new JSONObject();
        result.put("logFramework", logFrameworkType);
        JSONArray loggerList = new JSONArray();
        for (ConcurrentMap.Entry<String, Object> entry : loggerMap.entrySet()) {
            JSONObject loggerJSON = new JSONObject();
            loggerJSON.put("loggerName", entry.getKey());
            if (logFrameworkType == LogFrameworkType.LOG4J) {
                org.apache.log4j.Logger targetLogger = (org.apache.log4j.Logger) entry.getValue();
                loggerJSON.put("logLevel", targetLogger.getLevel().toString());
            } else if (logFrameworkType == LogFrameworkType.LOGBACK) {
                ch.qos.logback.classic.Logger targetLogger = (ch.qos.logback.classic.Logger) entry.getValue();
                loggerJSON.put("logLevel", targetLogger.getLevel().toString());
            } else if (logFrameworkType == LogFrameworkType.LOG4J2) {
                org.apache.logging.log4j.core.config.LoggerConfig targetLogger = (org.apache.logging.log4j.core.config.LoggerConfig) entry.getValue();
                loggerJSON.put("logLevel", targetLogger.getLevel().toString());
            } else {
                loggerJSON.put("logLevel", "Logger的类型未知,无法处理!");
            }
            loggerList.add(loggerJSON);
        }
        result.put("loggerList", loggerList);
        LOG.info("getLoggerList: result={}", result.toString());
        return result.toString();
    }

3、 修改Logger的级别

    private String setLogLevel(JSONArray data) {
        LOG.info("setLogLevel: data={}", data);
        List<LoggerBean> loggerList = parseJsonData(data);
        if (CollectionUtils.isEmpty(loggerList)) {
            return "";
        }
        for (LoggerBean loggerbean : loggerList) {
            Object logger = loggerMap.get(loggerbean.getName());
            if (logger == null) {
                throw new RuntimeException("需要修改日志级别的Logger不存在");
            }
            if (logFrameworkType == LogFrameworkType.LOG4J) {
                org.apache.log4j.Logger targetLogger = (org.apache.log4j.Logger) logger;
                org.apache.log4j.Level targetLevel = org.apache.log4j.Level.toLevel(loggerbean.getLevel());
                targetLogger.setLevel(targetLevel);
            } else if (logFrameworkType == LogFrameworkType.LOGBACK) {
                ch.qos.logback.classic.Logger targetLogger = (ch.qos.logback.classic.Logger) logger;
                ch.qos.logback.classic.Level targetLevel = ch.qos.logback.classic.Level.toLevel(loggerbean.getLevel());
                targetLogger.setLevel(targetLevel);
            } else if (logFrameworkType == LogFrameworkType.LOG4J2) {
                org.apache.logging.log4j.core.config.LoggerConfig loggerConfig = (org.apache.logging.log4j.core.config.LoggerConfig) logger;
                org.apache.logging.log4j.Level targetLevel = org.apache.logging.log4j.Level.toLevel(loggerbean.getLevel());
                loggerConfig.setLevel(targetLevel);
                org.apache.logging.log4j.core.LoggerContext ctx = (org.apache.logging.log4j.core.LoggerContext) org.apache.logging.log4j.LogManager.getContext(false);
                ctx.updateLoggers(); // This causes all Loggers to refetch information from their LoggerConfig.
            } else {
                throw new RuntimeException("Logger的类型未知,无法处理!");
            }
        }
        return "success";
    }

未经允许不得转载:搜云库技术团队 » Java日志:日志级别动态调整

JetBrains 全家桶,激活、破解、教程

提供 JetBrains 全家桶激活码、注册码、破解补丁下载及详细激活教程,支持 IntelliJ IDEA、PyCharm、WebStorm 等工具的永久激活。无论是破解教程,还是最新激活码,均可免费获得,帮助开发者解决常见激活问题,确保轻松破解并快速使用 JetBrains 软件。获取免费的破解补丁和激活码,快速解决激活难题,全面覆盖 2024/2025 版本!

联系我们联系我们