前言

关于日志级别,大部分项目可能都设置为info级别,当然也可能有一些追求性能或者说包含很多敏感信息的项目直接将级别设置为warn或者error;这时候如果项目中出现一些未知异常,需要用到很详细的日志信息,此时如果项目中没有动态改变日志级别的机制,排查问题将很棘手。

日志系统

我们常用的一些日志系统包括:​​Log4j2​​、​​Logback​​、​​Java Util Logging​​;我们想动态改变日志的级别,前提是这些日志系统都支持我们直接设置日志等级,当然这些系统提供了很简单的接口;

  • Log4j2
LoggerContext loggerContext = (LoggerContext) LogManager.getContext(false);
LoggerConfig loggerConfig = loggerContext.getConfiguration().getLoggers().get("root");
loggerConfig.setLevel(level);


  • Logback
LoggerContext loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory();
Logger logger = loggerContext.getLogger("root");
((ch.qos.logback.classic.Logger) logger).setLevel(level);


  • Java Util Logging
Logger logger = Logger.getLogger("root");
logger.setLevel(level);


当然除了上面直接设置日志级别的方式,也有可以动态加载配置文件的方式,同样也可以在配置文件中动态改变日志级别,以logback为例:

LoggerContext lc = (LoggerContext) LoggerFactory.getILoggerFactory();
File externalConfigFile = new File("logback.xml");
JoranConfigurator configurator = new JoranConfigurator();
configurator.setContext(lc);
lc.reset();
configurator.doConfigure(externalConfigFileLocation);


上面简单介绍了一下每种日志系统都是如何去设置日志级别的,最关键的是设置完之后,可以实时生效,立马可以看到我们想要的日志;有了这些下面其实就是通过何种方式去改变日志级别的问题了;

如何动态改变级别

如何去动态改变级别,最简单的方式就是对外提供一个接口,给定一个日志级别作为参数实时变更;或者通过配置中心的方式;另外其实像SpringBoot这些主流的框架本身也提供了动态修改的功能;下面可以具体看一下是如何实现的,以logback为例;

自定义接口

自定义一个给定日志级别的接口,外部直接通过调用接口来改变级别:

@RequestMapping(value = "logLevel/{logLevel}")
public String changeLogLevel(@PathVariable("logLevel") String logLevel) {
try {
LoggerContext loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory();
Logger logger = loggerContext.getLogger("root");
((ch.qos.logback.classic.Logger) logger).setLevel(Level.valueOf(logLevel));
} catch (Exception e) {
logger.error("changeLogLevel error", e);
return "fail";
}
return "success";
}


想要改变日志级别直接请求如下地址即可,设置一个debug的级别:

http://[ip]:[port]/logLevel/debug

这种方式虽然比较简单,但是如果节点很多的话,操作起来就很麻烦,当然也可以汇总所有节点路径,一次操作触发所有节点的请求;其实最好的办法应该是类似发布订阅的方式,发布者会给所有订阅者都发送一个更改日志级别的通知,有新的节点只要成为订阅者即可,这种方式其实就是现在主流的配置中心的方式。

配置中心

配置中心的目的其实就是把一些会经常变动的参数集中保存起来,某个系统启动时去配置中心获取相关的参数,同时会对这些参数进行监听,后面在配置中心里面改变参数的值会实时推送给相关系统;这样系统就可以在不重启的情况下就更新了配置;

利用现有的一些中间件我们就能很快实现一个配置中心,比如Zookeeper提供了对某个Node进行监听的功能,MQ和Redis都有发布订阅的功能,所以用来实时推送变更再好不过了;

  • Zookeeper方式

可以直接使用PathChildrenCache用来监听子节点的CHILD_ADDED,CHILD_UPDATED,CHILD_REMOVED事件;这样如果在Zookeeper服务端对节点的值就行更新,客户端会触发以上三个事件:

private void watcherPath(String path) {
PathChildrenCache cache = new PathChildrenCache(client, path, true);
cache.start(StartMode.POST_INITIALIZED_EVENT);
cache.getListenable().addListener(new PathChildrenCacheListener() {
@Override
public void childEvent(CuratorFramework client, PathChildrenCacheEvent event) throws Exception {
switch (event.getType()) {
case CHILD_ADDED:
break;
case CHILD_UPDATED:
String logLevel = new String(event.getData().getData());
//日志级别更新处理
break;
case CHILD_REMOVED:
break;
default:
break;
}
}
});
}


MQ一般都有Queue和Topic方式,Topic方式其实就是订阅发布模式,所有的集群节点可以订阅某个Topic,这样发布端发送更新日志级别的消息,其他订阅节点都能收到:

//日志等级Topic
private final String TOPIC = "LOGLEVEL";
private void watcherPaths() throws JMSException {
Topic topic = session.createTopic(TOPIC);
MessageConsumer consumer = session.createConsumer(topic);
consumer.setMessageListener(new MessageListener() {
@Override
public void onMessage(Message message) {
TextMessage tm = (TextMessage) message;
String logLevel = tm.getText();
//日志级别更新处理
}
});
}


Redis其实除了缓存的功能,也提供了类似MQ的发布订阅的模式;集群节点通过订阅一个channel,发布端通过此channel来发布消息:

private void watcherPaths() throws JMSException {
jedis.subscribe(new JedisPubSub() {
@Override
public void onMessage(String channel, String message) {
String logLevel = message;
//日志级别更新处理
}
},"LOGLEVEL");
}


SpringBoot内置

SpringBoot2.0之后可以通过actuator动态调整日志级别,主要是通过暴露loggers这个endpoint来实现,具体步骤如下:

  • 需要引入actuator
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>


  • 暴露loggers

在application.properties中添加如下配置:

management.endpoints.web.exposure.include=loggers


  • 查看日志级别

启动服务可以通过:

http://[ip]:[port]/actuator/loggers

查看当前项目每个包的日志级别:

{
levels: [
"OFF","ERROR","WARN","INFO","DEBUG","TRACE"
],
loggers: {
ROOT: {
configuredLevel: "INFO",
effectiveLevel: "INFO"
},
...
}


  • 动态修改日志级别

发送POST请求到:

http://[ip]:[port]/actuator/loggers/[包路径]

需要在body中指定configuredLevel参数;

比如修改整个项目日志级别为error:

http://[ip]:[port]/actuator/loggers/root

如何动态改变日志级别_发布订阅

关于SpringBoot内部是如何实现动态改变日志级别的,可以查看其实现核心类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);
}
...
}


具体通过LoggingSystem来对日志系统动态改变级别,上面也介绍了主流使用的日志系统,SpringBoot也都支持这些系统,这是一个抽象类,具体实现类:

  • JavaLoggingSystem
  • Log4J2LoggingSystem
  • LogbackLoggingSystem
  • NoOpLoggingSystem

分别对应了几种日志系统,这几个类内部其实也是调用上面介绍的方法去改变日志级别,当然SpringBoot自动会识别出当前使用的是哪个日志系统,然后使用哪个LoggingSystem;

总结

大部分公司其实更多的还是使用配置中心的方式来动态改变日志级别,这种方式更加灵活,而且配置中心已经成为很多公司的标配组件,不光用来改变日志级别,所有有可能改变的参数都可以使用。