boot+logback+nacos方案:https://blog.csdn.net/z69183787/article/details/108948325
正常情况下使用 springboot + logback + nacos 进行日志级别动态变更是没有问题的,但当和springcloud一起使用时,会发生 logger级别重置的情况,即设置完某个logger的独立级别后,整个logger上下文会重新刷新,并且重置logger 为 Root设置的 level 级别。经过一番源码的探查,目前还未找到问题的原因和解决方案;但与此同时,却发现了另一种实现这个效果的方案:
使用 logging.level 属性进行配置,在nacos变更后,应用会自动生效
nacos配置:
logging:
level:
com.xxx.xxxx.demo.controller: ERROR
com.zzz.zzzz.demo.controller: INFO
关键源码:
Nacos接收到配置变化后进入 NacosContextRefresher 类,核心方法如下:
可以看到 配置修改后,触发了listener,在listener中发布了一个 RefreshEvent
private void registerNacosListener(final String groupKey, final String dataKey) {
String key = NacosPropertySourceRepository.getMapKey(dataKey, groupKey);
Listener listener = listenerMap.computeIfAbsent(key,
lst -> new AbstractSharedListener() {
@Override
public void innerReceive(String dataId, String group,
String configInfo) {
refreshCountIncrement();
nacosRefreshHistory.addRefreshRecord(dataId, group, configInfo);
// todo feature: support single refresh for listening
applicationContext.publishEvent(
new RefreshEvent(this, null, "Refresh Nacos config"));
if (log.isDebugEnabled()) {
log.debug(String.format(
"Refresh Nacos config group=%s,dataId=%s,configInfo=%s",
group, dataId, configInfo));
}
}
});
try {
configService.addListener(dataKey, groupKey, listener);
}
catch (NacosException e) {
log.warn(String.format(
"register fail for nacos listener ,dataId=[%s],group=[%s]", dataKey,
groupKey), e);
}
}
RefreshEvent
public RefreshEvent(Object source, Object event, String eventDesc) {
super(source);
this.event = event;
this.eventDesc = eventDesc;
}
进入 AbstractApplicationContext中的 event广播 multicastEvent
@Override
public void multicastEvent(final ApplicationEvent event, @Nullable ResolvableType eventType) {
ResolvableType type = (eventType != null ? eventType : resolveDefaultEventType(event));
Executor executor = getTaskExecutor();
for (ApplicationListener<?> listener : getApplicationListeners(event, type)) {
if (executor != null) {
executor.execute(() -> invokeListener(listener, event));
}
else {
invokeListener(listener, event);
}
}
}
进入SimpleApplicationEventMulticaster 的 invokeListener方法,进而触发所有满足 eventType 的 listener进行 onApplicationEvent 回调
protected void invokeListener(ApplicationListener<?> listener, ApplicationEvent event) {
ErrorHandler errorHandler = getErrorHandler();
if (errorHandler != null) {
try {
doInvokeListener(listener, event);
}
catch (Throwable err) {
errorHandler.handleError(err);
}
}
else {
doInvokeListener(listener, event);
}
}
private void doInvokeListener(ApplicationListener listener, ApplicationEvent event) {
try {
listener.onApplicationEvent(event);
}
满足 RefreshEvent 的 Listener 是 RefreshEventListener
@Override
public boolean supportsEventType(Class<? extends ApplicationEvent> eventType) {
return ApplicationReadyEvent.class.isAssignableFrom(eventType)
|| RefreshEvent.class.isAssignableFrom(eventType);
}
@Override
public void onApplicationEvent(ApplicationEvent event) {
if (event instanceof ApplicationReadyEvent) {
handle((ApplicationReadyEvent) event);
}
else if (event instanceof RefreshEvent) {
handle((RefreshEvent) event);
}
}
public void handle(ApplicationReadyEvent event) {
this.ready.compareAndSet(false, true);
}
public void handle(RefreshEvent event) {
if (this.ready.get()) { // don't handle events before app is ready
log.debug("Event received " + event.getEventDesc());
Set<String> keys = this.refresh.refresh();
log.info("Refresh keys changed: " + keys);
}
}
Set<String> keys = this.refresh.refresh(); 方法中 调用了 ContextRefresher 中的方法,可以看到发布了一个 EnvironmentChangeEvent 事件
public synchronized Set<String> refresh() {
Set<String> keys = refreshEnvironment();
this.scope.refreshAll();
return keys;
}
public synchronized Set<String> refreshEnvironment() {
Map<String, Object> before = extract(
this.context.getEnvironment().getPropertySources());
addConfigFilesToEnvironment();
Set<String> keys = changes(before,
extract(this.context.getEnvironment().getPropertySources())).keySet();
this.context.publishEvent(new EnvironmentChangeEvent(this.context, keys));
return keys;
}
监听 EnvironmentChangeEvent 事件的监听器是 LoggingRebinder,可以明显的看到
1、获取 yaml配置文件中的 logging.level 中的配置,即上文中 nacos 的配置项(Map<String,String> 结构)
2、然后分别进行设置生效
@Override
public void onApplicationEvent(EnvironmentChangeEvent event) {
if (this.environment == null) {
return;
}
LoggingSystem system = LoggingSystem.get(LoggingSystem.class.getClassLoader());
setLogLevels(system, this.environment);
}
protected void setLogLevels(LoggingSystem system, Environment environment) {
Map<String, String> levels = Binder.get(environment)
.bind("logging.level", STRING_STRING_MAP)
.orElseGet(Collections::emptyMap);
for (Entry<String, String> entry : levels.entrySet()) {
setLogLevel(system, environment, entry.getKey(), entry.getValue().toString());
}
}
private void setLogLevel(LoggingSystem system, Environment environment, String name,
String level) {
try {
if (name.equalsIgnoreCase("root")) {
name = null;
}
level = environment.resolvePlaceholders(level);
system.setLogLevel(name, resolveLogLevel(level));
}
catch (RuntimeException ex) {
this.logger.error("Cannot set level: " + level + " for '" + name + "'");
}
}
最终经过调试,可以完美满足 动态变更 日志级别的需求,但是为何方案一会 重置 日志级别还是没有解决,未完待续。。。
附上工具类,输出所有日志及package的 日志级别
/**
* 查询所有配置的日志级别
*/
@GetMapping("/loggerList")
public List<LogInfoVO> queryLoggerList(){
LoggerContext loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory();
return loggerContext.getLoggerList().stream()
.map(log -> new LogInfoVO(log.getName(), log.getLevel()!=null?log.getLevel().levelStr:"null",log.getEffectiveLevel().levelStr))
.collect(Collectors.toList());
}