作为开发人员,定位问题是我们的日常工作,而日志是我们定位问题非常重要的依据。传统方式定位问题时,往往是如下步骤:
实际上是可以动态修改日志级别,无需重启应用,立即生效。本文收集了3种动态修改日志级别的文章,分别是
从 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
端点提供了查看以及修改日志级别的能力。
查看当前应用各包/类的日志级别 访问 http://localhost:8080/actuator/loggers ,可看到类似如下的结果:
查看指定包/类日志详情 访问 http://localhost:8080/actuator/loggers/com.blockmao.springboot.demo.DemoController ,可看到类似如下的结果:
修改日志级别 默认的日志级别是INFO,所以 DemoController
的debug日志不会打印。下面来尝试将该类的日志级别设为 DEBUG
,如下
此时,访问 http://localhost:8080/helloworld 会看到类似如下的日志:
并且,此时再访问 http://localhost:8080/actuator/loggers/com.itmuch.logging.TestController ,可看到类似如下的结果:
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); } // ...其他省略 } 复制代码
其中, Endpoint
、 WriteOperation
、 @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
类结构如下图所示:
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
。
使用 ognl
命令可以动态修改日志级别,步骤如下:
查找当前类的classLoaderHash
用OGNL获取logger
可以发现日志使用的是Logback框架。
单独设置DemoController的logger level
全局设置logger level
如果使用的日志框架是 log4j
,则使用上述 ognl
命令则会报错。至于为什么?请阅读Java日志:SLF4J详解
初始化:确定所使用的日志框架,获取配置文件中所有的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); } 复制代码
获取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(); } 复制代码
修改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"; } 复制代码