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..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 @@ -17,14 +17,21 @@ package org.springframework.cloud.logging; 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; 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,13 +42,22 @@ /** * @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 final 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() { @@ -82,4 +98,52 @@ 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(); + } + + @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(); + } + }