作为程序猿,定位问题是我们的日常工作,而日志是我们定位问题非常重要的依据。传统方式定位问题时,往往是如下步骤:
那么问题就来了,可不可以动态修改日志级别呢?(无需重启应用,就能立刻刷新)
答案是肯定的!
下面提供几个思路给大家参考。
不废话,直接上代码
@Resource
private LoggingSystem loggingSystem;
@PostMApping("/changeLogLevel")
public void changeLogLevel(@RequestParam("name") String name, @RequestParam("level") String level) {
LogLevel logLevel = LogLevel.valueOf(level.toUpperCase());
loggingSystem.setLogLevel(name, logLevel);
}
what?这么简单?是的,就是这么简单。
LoggingSystem 这个抽象类就是关键,其实后面所要介绍的几个修改思路(actuator,Apollo,mq)的底层也是基于它进行修改的。
如果大家对LoggingSystem这个类在底层究竟是如何实现动态修改日志级别感兴趣的话,请评论区留言,我抽时间再写一篇文章来详细说一下。
然后再说一下这种方式的优缺点吧。
优点:简单!
缺点:也很明显,只适合单机/生产机器不多的服务。如果你的服务有上百个节点,用这种方式来修改。。。
那有朋友会问,有没有适合多机集群的服务的修改方式?
那必须有啊,下面介绍一下思路二。
这种方式的前提是系统接入了Apollo。
也不废话,直接上代码吧。代码里也有注释。
@Configuration
public class LogLevelRefresher {
private final static Logger log = LoggerFactory.getLogger(com.dylan.config.LoggingLevelRefresher.class);
private static final String PREFIX = "logging.level.";
private static final String ROOT = LoggingSystem.ROOT_LOGGER_NAME;
@Resource
private LoggingSystem loggingSystem;
/**
* 支持类配置
*/
@PostConstruct
private void init() {
//要修改日志级别的key(包路径/类路径)
String keyStr = ConfigCenterService.getAppProperty("log.changeKey", "logging.level.root,logging.level.com.dylan.config");
Set<String> changedKeys = Arrays.stream(keyStr.split(",")).collect(Collectors.toSet());
refreshLoggingLevels(changedKeys);
}
/**
* 修改Apollo配置后的回调方法
*/
@ApolloConfigChangeListener
private void onChange(ConfigChangeEvent changeEvent) {
refreshLoggingLevels(changeEvent.changedKeys());
}
private void refreshLoggingLevels(Set<String> changedKeys) {
for (String key : changedKeys) {
// key may be : logging.level.com.example.web
if (StringUtils.startsWithIgnoreCase(key, PREFIX)) {
String loggerName = PREFIX.equalsIgnoreCase(key) ? ROOT : key.substring(PREFIX.length());
String strLevel = ConfigCenterService.getProperty(key, parentStrLevel(loggerName));
LogLevel level = LogLevel.valueOf(strLevel.toUpperCase());
loggingSystem.setLogLevel(loggerName, level);
//打印一下信息,可以不用
log(loggerName, strLevel);
}
}
}
private String parentStrLevel(String loggerName) {
String parentLoggerName = loggerName.contains(".") ? loggerName.substring(0, loggerName.lastIndexOf(".") : ROOT;
return loggingSystem.getLoggerConfiguration(parentLoggerName).getEffectiveLevel().name();
}
/**
* 获取当前类的Logger对象有效日志级别对应的方法,进行日志输出。举例:
* 如果当前类的EffectiveLevel为WARN,则获取的Method为 `org.slf4j.Logger#warn(JAVA.lang.String, java.lang.Object, java.lang.Object)`
* 目的是为了输出`changed {} log level to:{}`这一行日志
*/
private void log(String loggerName, String strLevel) {
try {
LoggerConfiguration loggerConfiguration = loggingSystem.getLoggerConfiguration(log.getName());
Method method = log.getClass().getMethod(loggerConfiguration.getEffectiveLevel().name().toLowerCase(), String.class, Object.class, Object.class);
method.invoke(log, "changed {} log level to:{}", loggerName, strLevel);
} catch (Exception e) {
log.error("changed {} log level to:{} error", loggerName, strLevel, e);
}
}
}
大家可以看到,Apollo的方式最终也是LoggingSystem 这个类进行修改日志级别的操作。
那可能大家会问,Apollo在这里的作用是什么?
如果大家用过Apollo的话就会发现,在Apollo可视化管理系统中,每个系统都有一个实例列表,里面就是我们具体的应用地址。所以在这里你可以认为Apollo有类似注册中心的作用,在我们应用启动的时候,Apollo后台就会记录下来。
所以Apollo能实现集群的日志级别动态修改的原理就在这。是不是也很简单呢?
如果你们的系统没有接入Apollo的话,那应该如何实现集群的日志级别动态修改呢?
MQ就是其中一个选择。我简单说一下实现思路吧,具体实现也很简单,就留给大家去动手实践啦。
是不是很简单呢?
其实这种方法和方式一是差不多的,只是actuator把接口通过端点Endpoints 的方式暴露出来。
至于什么是端点(Endpoints),我简单介绍一下吧。
Endpoints 是 Actuator 的核心部分,它用来监视应用程序及交互,spring-boot-actuator中已经内置了非常多的Endpoints(health、info、beans、httptrace、shutdown等等),同时也允许我们扩展自己的端点。
Endpoints 分成两类:原生端点和用户自定义端点;自定义端点主要是指扩展性,用户可以根据自己的实际应用,定义一些比较关心的指标,在运行期进行监控。
原生端点是在应用程序里提供的众多 restful api 接口,通过它们可以监控应用程序运行时的内部状况。
原生端点又可以分成三类:
我们这里修改配置文件用到的就是应用配置类的端点。
http://localhost:8080/actuator/loggers
可看到类似如下的结果:
{
"levels": ["OFF", "ERROR", "WARN", "INFO", "DEBUG", "TRACE"],
"loggers": {
"ROOT": {
"configuredLevel": "INFO",
"effectiveLevel": "INFO"
},
"com.itmuch.logging.TestController": {
"configuredLevel": null,
"effectiveLevel": "INFO"
}
}
// ...省略
}
http://localhost:8080/actuator/loggers/com.dylan.logging.TestController
可看到类似如下的结果:
{"configuredLevel":null,"effectiveLevel":"INFO"}
POST方式,json格式的参数
example:http://localhost:8080/actuator/loggers/com.dylan.controller.IncreaseAgentController
actuator修改日志级别
但这种方式和方式一有同样的局限性,就是只适合单机或者开发环境。如果想用这种方式的话可以接入Spring Boot Admin。通过后台的方式进行管理。