Springboot 动态改变Log级别

背景作为程序猿,定位问题是我们的日常工作,而日志是我们定位问题非常重要的依据 。传统方式定位问题时,往往是如下步骤:

  • 将日志级别设低,例如 DEBUG ;
  • 重启应用;
  • 复现问题,观察日志;
那么问题就来了,可不可以动态修改日志级别呢?(无需重启应用,就能立刻刷新)
答案是肯定的!
下面提供几个思路给大家参考 。
使用 LoggingSystem 自行开发修改日志级别的接口不废话,直接上代码
@Resourceprivate 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这个类在底层究竟是如何实现动态修改日志级别感兴趣的话,请评论区留言,我抽时间再写一篇文章来详细说一下 。
然后再说一下这种方式的优缺点吧 。
【Springboot 动态改变Log级别】优点:简单!
缺点:也很明显,只适合单机/生产机器不多的服务 。如果你的服务有上百个节点,用这种方式来修改 。。。
那有朋友会问,有没有适合多机集群的服务的修改方式?
那必须有啊,下面介绍一下思路二 。
使用 Apollo + LoggingSystem这种方式的前提是系统接入了Apollo 。
也不废话,直接上代码吧 。代码里也有注释 。
@Configurationpublic 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;@Resourceprivate LoggingSystem loggingSystem;/*** 支持类配置*/@PostConstructprivate 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配置后的回调方法*/@ApolloConfigChangeListenerprivate void onChange(ConfigChangeEvent changeEvent) {refreshLoggingLevels(changeEvent.changedKeys());}private void refreshLoggingLevels(Set<String> changedKeys) {for (String key : changedKeys) {// key may be : logging.level.com.example.webif (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能实现集群的日志级别动态修改的原理就在这 。是不是也很简单呢?
使用 MQ + LoggingSystem 
如果你们的系统没有接入Apollo的话,那应该如何实现集群的日志级别动态修改呢?


推荐阅读