From 7e30e6102deaccaf1a5458228d3ed01d611b0739 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E6=B5=B7=E6=B3=A2?= Date: Thu, 29 Apr 2021 18:11:06 +0800 Subject: [PATCH 1/3] LoggingRebinder supports updating the logger level of the logger group and refreshing the logger group definition when refreshed through refresh endpoint --- .../RefreshAutoConfiguration.java | 10 +- .../PropertySourceBootstrapConfiguration.java | 4 +- .../cloud/logging/LoggingRebinder.java | 156 +++++++++++++++++- .../cloud/logging/LoggingRebinderTests.java | 32 +++- 4 files changed, 190 insertions(+), 12 deletions(-) diff --git a/spring-cloud-context/src/main/java/org/springframework/cloud/autoconfigure/RefreshAutoConfiguration.java b/spring-cloud-context/src/main/java/org/springframework/cloud/autoconfigure/RefreshAutoConfiguration.java index 82f51c738..4b3cb9f82 100644 --- a/spring-cloud-context/src/main/java/org/springframework/cloud/autoconfigure/RefreshAutoConfiguration.java +++ b/spring-cloud-context/src/main/java/org/springframework/cloud/autoconfigure/RefreshAutoConfiguration.java @@ -26,6 +26,7 @@ import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.ListableBeanFactory; +import org.springframework.beans.factory.ObjectProvider; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.BeanDefinitionHolder; @@ -41,6 +42,8 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.context.properties.bind.Bindable; import org.springframework.boot.context.properties.bind.Binder; +import org.springframework.boot.logging.LoggerGroups; +import org.springframework.boot.logging.LoggingSystem; import org.springframework.cloud.context.refresh.ConfigDataContextRefresher; import org.springframework.cloud.context.refresh.ContextRefresher; import org.springframework.cloud.context.refresh.LegacyContextRefresher; @@ -98,8 +101,11 @@ public static RefreshScope refreshScope() { @Bean @ConditionalOnMissingBean - public static LoggingRebinder loggingRebinder() { - return new LoggingRebinder(); + public static LoggingRebinder loggingRebinder(ObjectProvider loggingSystem, + ObjectProvider loggerGroups) { + return new LoggingRebinder( + loggingSystem.getIfAvailable(() -> LoggingSystem.get(LoggingSystem.class.getClassLoader())), + loggerGroups.getIfAvailable(LoggerGroups::new)); } @Bean diff --git a/spring-cloud-context/src/main/java/org/springframework/cloud/bootstrap/config/PropertySourceBootstrapConfiguration.java b/spring-cloud-context/src/main/java/org/springframework/cloud/bootstrap/config/PropertySourceBootstrapConfiguration.java index 1822e5bc2..2b3e611c8 100644 --- a/spring-cloud-context/src/main/java/org/springframework/cloud/bootstrap/config/PropertySourceBootstrapConfiguration.java +++ b/spring-cloud-context/src/main/java/org/springframework/cloud/bootstrap/config/PropertySourceBootstrapConfiguration.java @@ -56,6 +56,7 @@ /** * @author Dave Syer + * @author Haibo Wang * */ @Configuration(proxyBeanMethods = false) @@ -149,7 +150,8 @@ private void reinitializeLoggingSystem(ConfigurableEnvironment environment, Stri } private void setLogLevels(ConfigurableApplicationContext applicationContext, ConfigurableEnvironment environment) { - LoggingRebinder rebinder = new LoggingRebinder(); + LoggingSystem system = LoggingSystem.get(LoggingSystem.class.getClassLoader()); + LoggingRebinder rebinder = new LoggingRebinder(system, null); rebinder.setEnvironment(environment); // We can't fire the event in the ApplicationContext here (too early), but we can // create our own listener and poke it (it doesn't need the key changes) diff --git a/spring-cloud-context/src/main/java/org/springframework/cloud/logging/LoggingRebinder.java b/spring-cloud-context/src/main/java/org/springframework/cloud/logging/LoggingRebinder.java index 6250a0bb1..ab99ec4e5 100644 --- a/spring-cloud-context/src/main/java/org/springframework/cloud/logging/LoggingRebinder.java +++ b/spring-cloud-context/src/main/java/org/springframework/cloud/logging/LoggingRebinder.java @@ -16,7 +16,11 @@ package org.springframework.cloud.logging; +import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; +import java.util.HashMap; +import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Map.Entry; @@ -27,11 +31,17 @@ import org.springframework.boot.context.properties.bind.Bindable; import org.springframework.boot.context.properties.bind.Binder; import org.springframework.boot.logging.LogLevel; +import org.springframework.boot.logging.LoggerGroup; +import org.springframework.boot.logging.LoggerGroups; import org.springframework.boot.logging.LoggingSystem; import org.springframework.cloud.context.environment.EnvironmentChangeEvent; import org.springframework.context.ApplicationListener; import org.springframework.context.EnvironmentAware; +import org.springframework.core.ResolvableType; import org.springframework.core.env.Environment; +import org.springframework.util.Assert; +import org.springframework.util.MultiValueMap; +import org.springframework.util.StringUtils; /** * Listener that looks for {@link EnvironmentChangeEvent} and rebinds logger levels if any @@ -39,16 +49,34 @@ * * @author Dave Syer * @author Olga Maciaszek-Sharma + * @author Haibo Wang * */ public class LoggingRebinder implements ApplicationListener, EnvironmentAware { + private static Bindable>> STRING_STRINGS_MAP = Bindable + .of(ResolvableType.forClassWithGenerics(MultiValueMap.class, String.class, String.class).asMap()); + private static final Bindable> STRING_STRING_MAP = Bindable.mapOf(String.class, String.class); private final Log logger = LogFactory.getLog(getClass()); private Environment environment; + final private static List SPRING_LOGGING_GROUP_NAMES = Arrays.asList("web", "sql"); + + private ArrayList rootLoggersList = new ArrayList<>(); + + final private LoggingSystem loggingSystem; + + final private LoggerGroups loggerGroups; + + public LoggingRebinder(LoggingSystem loggingSystem, LoggerGroups loggerGroups) { + Assert.notNull(loggingSystem, "LoggingSystem must not be null"); + this.loggingSystem = loggingSystem; + this.loggerGroups = loggerGroups; + } + @Override public void setEnvironment(Environment environment) { this.environment = environment; @@ -59,31 +87,143 @@ public void onApplicationEvent(EnvironmentChangeEvent event) { if (this.environment == null) { return; } - LoggingSystem system = LoggingSystem.get(LoggingSystem.class.getClassLoader()); - setLogLevels(system, this.environment); + + if (this.loggerGroups != null) { + List deletedLoggerList = updateLoggerGroups(loggingSystem, environment, loggerGroups); + + for (String logger : deletedLoggerList) { + if (!rootLoggersList.contains(logger)) { + rootLoggersList.add(logger); + } + } + } + + setDefaultLogLevel(rootLoggersList, loggingSystem, environment); + setLogLevels(loggingSystem, environment, loggerGroups); + } + + /** + * Merges the definition of a logger group from the configuration and returns the + * loggers that have been removed from the logger group. + * @param loggingSystem the logging system + * @param environment the environment + * @param loggerGroups the logger groups + * @return A list of logger that have been removed from the log group + */ + protected List updateLoggerGroups(LoggingSystem loggingSystem, Environment environment, + LoggerGroups loggerGroups) { + Map> loggerGroupsMap = Binder.get(environment).bind("logging.group", STRING_STRINGS_MAP) + .orElseGet(Collections::emptyMap); + + ArrayList deletedList = new ArrayList(); + + // The deleted logger group + Map> deletedGroup = new HashMap>(); + for (LoggerGroup loggerGroup : loggerGroups) { + if (!loggerGroupsMap.containsKey(loggerGroup.getName()) + && !SPRING_LOGGING_GROUP_NAMES.contains(loggerGroup.getName())) { + for (String loggerName : loggerGroup.getMembers()) { + if (!deletedList.contains(loggerName)) { + deletedList.add(loggerName); + } + } + deletedGroup.put(loggerGroup.getName(), Collections.emptyList()); + } + } + loggerGroups.putAll(deletedGroup); + + for (Entry> entry : loggerGroupsMap.entrySet()) { + LoggerGroup loggerGroup = loggerGroups.get(entry.getKey()); + if (loggerGroup != null) { + for (String loggerName : loggerGroup.getMembers()) { + if (!entry.getValue().contains(loggerName)) { + if (!deletedList.contains(loggerName)) { + deletedList.add(loggerName); + } + } + } + } + } + loggerGroups.putAll(loggerGroupsMap); + + return deletedList; } - protected void setLogLevels(LoggingSystem system, Environment environment) { + protected void setLogLevels(LoggingSystem system, Environment environment, LoggerGroups loggerGroups) { Map levels = Binder.get(environment).bind("logging.level", STRING_STRING_MAP) .orElseGet(Collections::emptyMap); for (Entry entry : levels.entrySet()) { - setLogLevel(system, environment, entry.getKey(), entry.getValue().toString()); + setLogLevel(loggingSystem, environment, loggerGroups, entry.getKey(), entry.getValue()); } } - private void setLogLevel(LoggingSystem system, Environment environment, String name, String level) { + private void setLogLevel(LoggingSystem system, Environment environment, LoggerGroups loggerGroups, String name, + String level) { try { - if (name.equalsIgnoreCase("root")) { + level = environment.resolvePlaceholders(level); + LogLevel logLevel = resolveLogLevel(level); + + if (loggerGroups != null) { + LoggerGroup loggerGroup = loggerGroups.get(name); + if (loggerGroup != null && loggerGroup.hasMembers()) { + loggerGroup.configureLogLevel(logLevel, this::setLogLevel); + } + } + + if ("root".equalsIgnoreCase(name)) { name = null; } - level = environment.resolvePlaceholders(level); - system.setLogLevel(name, resolveLogLevel(level)); + + setLogLevel(name, logLevel); } catch (RuntimeException ex) { this.logger.error("Cannot set level: " + level + " for '" + name + "'"); } } + private void setLogLevel(String name, LogLevel logLevel) { + loggingSystem.setLogLevel(name, logLevel); + } + + private void setDefaultLogLevel(List loggerNames, LoggingSystem loggingSystem, Environment environment) { + if (loggerNames.isEmpty()) { + return; + } + + LogLevel logLevel = determineLogLevel(environment); + for (String loggerName : loggerNames) { + setLogLevel(loggerName, logLevel); + } + } + + private LogLevel determineLogLevel(Environment environment) { + LogLevel logLevel = LogLevel.INFO; + String level = environment.getProperty("logging.level.root"); + if (StringUtils.hasLength(level)) { + logLevel = resolveLogLevel(level); + } + else { + Log log = LogFactory.getLog(LoggingSystem.ROOT_LOGGER_NAME); + if (log.isTraceEnabled()) { + logLevel = LogLevel.TRACE; + } + else if (log.isDebugEnabled()) { + logLevel = LogLevel.DEBUG; + } + else if (log.isInfoEnabled()) { + logLevel = LogLevel.INFO; + } + else if (log.isErrorEnabled()) { + logLevel = LogLevel.ERROR; + } + else if (log.isFatalEnabled()) { + logLevel = LogLevel.FATAL; + } + } + + return logLevel; + } + private LogLevel resolveLogLevel(String level) { String trimmedLevel = level.trim(); if ("false".equalsIgnoreCase(trimmedLevel)) { diff --git a/spring-cloud-context/src/test/java/org/springframework/cloud/logging/LoggingRebinderTests.java b/spring-cloud-context/src/test/java/org/springframework/cloud/logging/LoggingRebinderTests.java index ff1aaff78..51ebb55ad 100644 --- a/spring-cloud-context/src/test/java/org/springframework/cloud/logging/LoggingRebinderTests.java +++ b/spring-cloud-context/src/test/java/org/springframework/cloud/logging/LoggingRebinderTests.java @@ -17,14 +17,17 @@ package org.springframework.cloud.logging; import java.util.Collections; +import java.util.HashSet; import ch.qos.logback.classic.Level; import org.junit.After; +import org.junit.Before; import org.junit.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.boot.logging.LogLevel; +import org.springframework.boot.logging.LoggerGroups; import org.springframework.boot.logging.LoggingSystem; import org.springframework.boot.test.util.TestPropertyValues; import org.springframework.cloud.context.environment.EnvironmentChangeEvent; @@ -35,14 +38,23 @@ /** * @author Dave Syer * @author Olga Maciaszek-Sharma + * @author Haibo Wang * */ public class LoggingRebinderTests { - private LoggingRebinder rebinder = new LoggingRebinder(); + private LoggingRebinder rebinder = null; private Logger logger = LoggerFactory.getLogger("org.springframework.web"); + private LoggingSystem loggingSystem; + + @Before + public void init() { + loggingSystem = LoggingSystem.get(getClass().getClassLoader()); + rebinder = new LoggingRebinder(loggingSystem, null); + } + @After public void reset() { LoggingSystem.get(getClass().getClassLoader()).setLogLevel("org.springframework.web", LogLevel.INFO); @@ -82,4 +94,22 @@ public void logLevelFalseResolvedToOff() { then(Level.OFF).isEqualTo((logger.getLevel())); } + @Test + public void logLevelsChangedByLoggerGroup() { + Logger testLogger = LoggerFactory.getLogger("my-app01"); + then(testLogger.isTraceEnabled()).isFalse(); + StandardEnvironment environment = new StandardEnvironment(); + TestPropertyValues.of("logging.level.app=trace", "logging.group.app=my-app01").applyTo(environment); + + LoggingRebinder rebinder = new LoggingRebinder(loggingSystem, new LoggerGroups()); + rebinder.setEnvironment(environment); + + HashSet changeKeys = new HashSet<>(); + changeKeys.add("spring.level.app"); + changeKeys.add("spring.group.app"); + rebinder.onApplicationEvent(new EnvironmentChangeEvent(environment, changeKeys)); + + then(testLogger.isTraceEnabled()).isTrue(); + } + } From 276f296eff17c97cc2723f1c22c760efda8c43ef Mon Sep 17 00:00:00 2001 From: xfrogcn Date: Fri, 30 Apr 2021 16:47:26 +0800 Subject: [PATCH 2/3] add unit test for removed logger from groups, in this case, the removed logger will use root logger level, and then if changed the root level from configuration, the removed logger level will be change. --- .../cloud/logging/LoggingRebinderTests.java | 35 +++++++++++++++++-- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/spring-cloud-context/src/test/java/org/springframework/cloud/logging/LoggingRebinderTests.java b/spring-cloud-context/src/test/java/org/springframework/cloud/logging/LoggingRebinderTests.java index 51ebb55ad..d6099e75f 100644 --- a/spring-cloud-context/src/test/java/org/springframework/cloud/logging/LoggingRebinderTests.java +++ b/spring-cloud-context/src/test/java/org/springframework/cloud/logging/LoggingRebinderTests.java @@ -16,8 +16,9 @@ package org.springframework.cloud.logging; -import java.util.Collections; -import java.util.HashSet; +import java.util.*; +import java.util.stream.Collectors; +import java.util.stream.Stream; import ch.qos.logback.classic.Level; import org.junit.After; @@ -45,7 +46,7 @@ public class LoggingRebinderTests { private LoggingRebinder rebinder = null; - private Logger logger = LoggerFactory.getLogger("org.springframework.web"); + private final Logger logger = LoggerFactory.getLogger("org.springframework.web"); private LoggingSystem loggingSystem; @@ -112,4 +113,32 @@ public void logLevelsChangedByLoggerGroup() { then(testLogger.isTraceEnabled()).isTrue(); } + @Test + public void logLevelsChangedByGroupRemovedAndRootLevelChanged() { + LoggerGroups loggerGroups = new LoggerGroups(); + HashMap> groupsMap = new HashMap<>(); + groupsMap.put("app", Stream.of("my-app01").collect(Collectors.toList())); + + loggerGroups.putAll(groupsMap); + loggerGroups.get("app").configureLogLevel(LogLevel.TRACE, (name, logLevel) -> loggingSystem.setLogLevel(name, logLevel)); + + Logger testLogger = LoggerFactory.getLogger("my-app01"); + then(testLogger.isTraceEnabled()).isTrue(); + + // first, we removed logger from app logger group + StandardEnvironment environment = new StandardEnvironment(); + TestPropertyValues.of("logging.level.app=trace").applyTo(environment); + LoggingRebinder rebinder = new LoggingRebinder(loggingSystem, loggerGroups); + rebinder.setEnvironment(environment); + + HashSet changeKeys = new HashSet<>(); + rebinder.onApplicationEvent(new EnvironmentChangeEvent(environment, changeKeys)); + // the default root logger level is Info + then(testLogger.isInfoEnabled()).isTrue(); + + // then, we changed the root logger level to error + TestPropertyValues.of("logging.level.root=error").applyTo(environment); + rebinder.onApplicationEvent(new EnvironmentChangeEvent(environment, changeKeys)); + then(testLogger.isErrorEnabled()).isTrue(); + } } From 52a784716fbe6747fcc4a3954a3e078c9e6df7e1 Mon Sep 17 00:00:00 2001 From: xfrogcn Date: Fri, 30 Apr 2021 16:52:27 +0800 Subject: [PATCH 3/3] add unit test for removed logger from groups, in this case, the removed logger will use root logger level, and then if changed the root level from configuration, the removed logger level will be change. --- .../cloud/logging/LoggingRebinderTests.java | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/spring-cloud-context/src/test/java/org/springframework/cloud/logging/LoggingRebinderTests.java b/spring-cloud-context/src/test/java/org/springframework/cloud/logging/LoggingRebinderTests.java index d6099e75f..281021b20 100644 --- a/spring-cloud-context/src/test/java/org/springframework/cloud/logging/LoggingRebinderTests.java +++ b/spring-cloud-context/src/test/java/org/springframework/cloud/logging/LoggingRebinderTests.java @@ -16,7 +16,10 @@ package org.springframework.cloud.logging; -import java.util.*; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -120,7 +123,8 @@ public void logLevelsChangedByGroupRemovedAndRootLevelChanged() { groupsMap.put("app", Stream.of("my-app01").collect(Collectors.toList())); loggerGroups.putAll(groupsMap); - loggerGroups.get("app").configureLogLevel(LogLevel.TRACE, (name, logLevel) -> loggingSystem.setLogLevel(name, logLevel)); + loggerGroups.get("app").configureLogLevel(LogLevel.TRACE, + (name, logLevel) -> loggingSystem.setLogLevel(name, logLevel)); Logger testLogger = LoggerFactory.getLogger("my-app01"); then(testLogger.isTraceEnabled()).isTrue(); @@ -141,4 +145,5 @@ public void logLevelsChangedByGroupRemovedAndRootLevelChanged() { rebinder.onApplicationEvent(new EnvironmentChangeEvent(environment, changeKeys)); then(testLogger.isErrorEnabled()).isTrue(); } + }