作为开发人员,定位问题是我们的日常工作,而日志是我们定位问题非常重要的依据。传统方式定位问题时,往往是如下步骤:
将日志级别设低,例如 DEBUG ;
重启应用;
复现问题,观察日志;
实际上是可以动态修改日志级别,无需重启应用,立即生效。本文收集了3种动态修改日志级别的文章,分别是
Spirng Boot动态修改日志级别
从 Spring Boot 1.5 开始,Spring Boot Actuator 组件就已提供动态修改日志级别的能力。
示例
1.引入spring-boot-starter-actuator依赖,内容如下:
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-actuator
复制代码
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端点提供了查看以及修改日志级别的能力。
原理
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 SYSTEMS;
static {
Map 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命令可以动态修改日志级别,步骤如下:
查找当前类的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 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 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 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";
}
复制代码