From 89b40e1e0027aa2883779562899e65766d8e7899 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 18 Oct 2021 21:27:13 +0100 Subject: [PATCH] Add support for Log4j2's composite configuration Closes gh-27110 --- .../src/docs/asciidoc/howto/logging.adoc | 8 ++ .../logging/log4j2/Log4J2LoggingSystem.java | 88 +++++++++++++++++-- ...itional-spring-configuration-metadata.json | 5 ++ .../log4j2/Log4J2LoggingSystemTests.java | 69 ++++++++++----- .../src/test/resources/log4j2-override.xml | 8 ++ src/checkstyle/import-control.xml | 1 + 6 files changed, 148 insertions(+), 31 deletions(-) create mode 100644 spring-boot-project/spring-boot/src/test/resources/log4j2-override.xml diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/logging.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/logging.adoc index 24c0a254a0..280b2b7ce7 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/logging.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/logging.adoc @@ -185,3 +185,11 @@ To configure Log4j 2 to use an alternative configuration file format, add the ap | `com.fasterxml.jackson.core:jackson-databind` | `log4j2.json` + `log4j2.jsn` |=== + + + +[[howto.logging.log4j.composite-configuration]] +==== Use Composite Configuration to Configure Log4j 2 +Log4j 2 has support for combining multiple configuration files into a single composite configuration. +To use this support in Spring Boot, configure configprop:logging.log4j2.config.override[] with the locations of one or more secondary configuration files. +The secondary configuration files will be merged with the primary configuration, whether the primary's source is Spring Boot's defaults, a standard location such as `log4j.xml`, or the location configured by the configprop:logging.config[] property. diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/Log4J2LoggingSystem.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/Log4J2LoggingSystem.java index 3902825136..8dba1b2e7a 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/Log4J2LoggingSystem.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/Log4J2LoggingSystem.java @@ -25,6 +25,7 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.stream.Collectors; import org.apache.logging.log4j.Level; import org.apache.logging.log4j.LogManager; @@ -38,10 +39,14 @@ import org.apache.logging.log4j.core.config.Configuration; import org.apache.logging.log4j.core.config.ConfigurationFactory; import org.apache.logging.log4j.core.config.ConfigurationSource; import org.apache.logging.log4j.core.config.LoggerConfig; +import org.apache.logging.log4j.core.config.composite.CompositeConfiguration; import org.apache.logging.log4j.core.filter.AbstractFilter; import org.apache.logging.log4j.core.util.NameUtil; import org.apache.logging.log4j.message.Message; +import org.springframework.boot.context.properties.bind.BindResult; +import org.springframework.boot.context.properties.bind.Bindable; +import org.springframework.boot.context.properties.bind.Binder; import org.springframework.boot.logging.LogFile; import org.springframework.boot.logging.LogLevel; import org.springframework.boot.logging.LoggerConfiguration; @@ -53,6 +58,7 @@ import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; +import org.springframework.util.CollectionUtils; import org.springframework.util.ResourceUtils; import org.springframework.util.StringUtils; @@ -167,33 +173,70 @@ public class Log4J2LoggingSystem extends Slf4JLoggingSystem { @Override protected void loadDefaults(LoggingInitializationContext initializationContext, LogFile logFile) { if (logFile != null) { - loadConfiguration(getPackagedConfigFile("log4j2-file.xml"), logFile); + loadConfiguration(getPackagedConfigFile("log4j2-file.xml"), logFile, getOverrides(initializationContext)); } else { - loadConfiguration(getPackagedConfigFile("log4j2.xml"), logFile); + loadConfiguration(getPackagedConfigFile("log4j2.xml"), logFile, getOverrides(initializationContext)); } } + private List getOverrides(LoggingInitializationContext initializationContext) { + BindResult> overrides = Binder.get(initializationContext.getEnvironment()) + .bind("logging.log4j2.config.override", Bindable.listOf(String.class)); + return overrides.orElse(Collections.emptyList()); + } + @Override protected void loadConfiguration(LoggingInitializationContext initializationContext, String location, LogFile logFile) { super.loadConfiguration(initializationContext, location, logFile); - loadConfiguration(location, logFile); + loadConfiguration(location, logFile, getOverrides(initializationContext)); } + /** + * Load the configuration from the given {@code location}. + * @param location the location + * @param logFile log file configuration + * @deprecated since 2.6.0 for removal in 2.8.0 in favor of + * {@link #loadConfiguration(String, LogFile, List)} + */ + @Deprecated protected void loadConfiguration(String location, LogFile logFile) { + this.loadConfiguration(location, logFile, Collections.emptyList()); + } + + /** + * Load the configuration from the given {@code location}, creating a composite using + * the configuration from the given {@code overrides}. + * @param location the location + * @param logFile log file configuration + * @param overrides the overriding locations + * @since 2.6.0 + */ + protected void loadConfiguration(String location, LogFile logFile, List overrides) { Assert.notNull(location, "Location must not be null"); try { - LoggerContext ctx = getLoggerContext(); - URL url = ResourceUtils.getURL(location); - ConfigurationSource source = getConfigurationSource(url); - ctx.start(ConfigurationFactory.getInstance().getConfiguration(ctx, source)); + List configurations = new ArrayList<>(); + LoggerContext context = getLoggerContext(); + configurations.add(load(location, context)); + for (String override : overrides) { + configurations.add(load(override, context)); + } + Configuration configuration = (configurations.size() > 1) ? createComposite(configurations) + : configurations.iterator().next(); + context.start(configuration); } catch (Exception ex) { throw new IllegalStateException("Could not initialize Log4J2 logging from " + location, ex); } } + private Configuration load(String location, LoggerContext context) throws IOException { + URL url = ResourceUtils.getURL(location); + ConfigurationSource source = getConfigurationSource(url); + return ConfigurationFactory.getInstance().getConfiguration(context, source); + } + private ConfigurationSource getConfigurationSource(URL url) throws IOException { InputStream stream = url.openStream(); if (FILE_PROTOCOL.equals(url.getProtocol())) { @@ -202,9 +245,38 @@ public class Log4J2LoggingSystem extends Slf4JLoggingSystem { return new ConfigurationSource(stream, url); } + private CompositeConfiguration createComposite(List configurations) { + return new CompositeConfiguration( + configurations.stream().map(AbstractConfiguration.class::cast).collect(Collectors.toList())); + } + @Override protected void reinitialize(LoggingInitializationContext initializationContext) { - getLoggerContext().reconfigure(); + List overrides = getOverrides(initializationContext); + if (!CollectionUtils.isEmpty(overrides)) { + reinitializeWithOverrides(overrides); + } + else { + LoggerContext context = getLoggerContext(); + context.reconfigure(); + } + } + + private void reinitializeWithOverrides(List overrides) { + LoggerContext context = getLoggerContext(); + Configuration base = context.getConfiguration(); + List configurations = new ArrayList<>(); + configurations.add((AbstractConfiguration) base); + for (String override : overrides) { + try { + configurations.add((AbstractConfiguration) load(override, context)); + } + catch (IOException ex) { + throw new RuntimeException("Failed to load overriding configuration from '" + override + "'", ex); + } + } + CompositeConfiguration composite = new CompositeConfiguration(configurations); + context.reconfigure(composite); } @Override diff --git a/spring-boot-project/spring-boot/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/spring-boot-project/spring-boot/src/main/resources/META-INF/additional-spring-configuration-metadata.json index 52a2af1134..b25e5f0427 100644 --- a/spring-boot-project/spring-boot/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/spring-boot-project/spring-boot/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -198,6 +198,11 @@ "sourceType": "org.springframework.boot.context.logging.LoggingApplicationListener", "defaultValue": 7 }, + { + "name": "logging.log4j2.config.override", + "type": "java.util.List", + "description": "Overriding configuration files used to create a composite configuration." + }, { "name": "spring.application.index", "type": "java.lang.Integer", diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/log4j2/Log4J2LoggingSystemTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/log4j2/Log4J2LoggingSystemTests.java index 2a96b0781f..65d83cb628 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/log4j2/Log4J2LoggingSystemTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/log4j2/Log4J2LoggingSystemTests.java @@ -35,6 +35,7 @@ import org.apache.logging.log4j.core.LoggerContext; import org.apache.logging.log4j.core.config.Configuration; import org.apache.logging.log4j.core.config.LoggerConfig; import org.apache.logging.log4j.core.config.Reconfigurable; +import org.apache.logging.log4j.core.config.composite.CompositeConfiguration; import org.apache.logging.log4j.core.util.ShutdownCallbackRegistry; import org.apache.logging.log4j.util.PropertiesUtil; import org.junit.jupiter.api.AfterEach; @@ -46,10 +47,12 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.boot.logging.AbstractLoggingSystemTests; import org.springframework.boot.logging.LogLevel; import org.springframework.boot.logging.LoggerConfiguration; +import org.springframework.boot.logging.LoggingInitializationContext; import org.springframework.boot.logging.LoggingSystem; import org.springframework.boot.logging.LoggingSystemProperties; import org.springframework.boot.testsupport.system.CapturedOutput; import org.springframework.boot.testsupport.system.OutputCaptureExtension; +import org.springframework.mock.env.MockEnvironment; import org.springframework.util.ClassUtils; import org.springframework.util.StringUtils; @@ -75,6 +78,11 @@ class Log4J2LoggingSystemTests extends AbstractLoggingSystemTests { private final TestLog4J2LoggingSystem loggingSystem = new TestLog4J2LoggingSystem(); + private final MockEnvironment environment = new MockEnvironment(); + + private final LoggingInitializationContext initializationContext = new LoggingInitializationContext( + this.environment); + private Logger logger; private Configuration configuration; @@ -99,7 +107,7 @@ class Log4J2LoggingSystemTests extends AbstractLoggingSystemTests { void noFile(CapturedOutput output) { this.loggingSystem.beforeInitialize(); this.logger.info("Hidden"); - this.loggingSystem.initialize(null, null, null); + this.loggingSystem.initialize(this.initializationContext, null, null); this.logger.info("Hello world"); Configuration configuration = this.loggingSystem.getConfiguration(); assertThat(output).contains("Hello world").doesNotContain("Hidden"); @@ -111,7 +119,7 @@ class Log4J2LoggingSystemTests extends AbstractLoggingSystemTests { void withFile(CapturedOutput output) { this.loggingSystem.beforeInitialize(); this.logger.info("Hidden"); - this.loggingSystem.initialize(null, getRelativeClasspathLocation("log4j2-file.xml"), + this.loggingSystem.initialize(this.initializationContext, getRelativeClasspathLocation("log4j2-file.xml"), getLogFile(null, tmpDir())); this.logger.info("Hello world"); Configuration configuration = this.loggingSystem.getConfiguration(); @@ -123,7 +131,8 @@ class Log4J2LoggingSystemTests extends AbstractLoggingSystemTests { @Test void testNonDefaultConfigLocation(CapturedOutput output) { this.loggingSystem.beforeInitialize(); - this.loggingSystem.initialize(null, "classpath:log4j2-nondefault.xml", getLogFile(tmpDir() + "/tmp.log", null)); + this.loggingSystem.initialize(this.initializationContext, "classpath:log4j2-nondefault.xml", + getLogFile(tmpDir() + "/tmp.log", null)); this.logger.info("Hello world"); Configuration configuration = this.loggingSystem.getConfiguration(); assertThat(output).contains("Hello world").contains(tmpDir() + "/tmp.log"); @@ -136,8 +145,8 @@ class Log4J2LoggingSystemTests extends AbstractLoggingSystemTests { @Test void testNonexistentConfigLocation() { this.loggingSystem.beforeInitialize(); - assertThatIllegalStateException() - .isThrownBy(() -> this.loggingSystem.initialize(null, "classpath:log4j2-nonexistent.xml", null)); + assertThatIllegalStateException().isThrownBy(() -> this.loggingSystem.initialize(this.initializationContext, + "classpath:log4j2-nonexistent.xml", null)); } @Test @@ -148,7 +157,7 @@ class Log4J2LoggingSystemTests extends AbstractLoggingSystemTests { @Test void setLevel(CapturedOutput output) { this.loggingSystem.beforeInitialize(); - this.loggingSystem.initialize(null, null, null); + this.loggingSystem.initialize(this.initializationContext, null, null); this.logger.debug("Hello"); this.loggingSystem.setLogLevel("org.springframework.boot", LogLevel.DEBUG); this.logger.debug("Hello"); @@ -158,7 +167,7 @@ class Log4J2LoggingSystemTests extends AbstractLoggingSystemTests { @Test void setLevelToNull(CapturedOutput output) { this.loggingSystem.beforeInitialize(); - this.loggingSystem.initialize(null, null, null); + this.loggingSystem.initialize(this.initializationContext, null, null); this.logger.debug("Hello"); this.loggingSystem.setLogLevel("org.springframework.boot", LogLevel.DEBUG); this.logger.debug("Hello"); @@ -170,7 +179,7 @@ class Log4J2LoggingSystemTests extends AbstractLoggingSystemTests { @Test void getLoggingConfigurations() { this.loggingSystem.beforeInitialize(); - this.loggingSystem.initialize(null, null, null); + this.loggingSystem.initialize(this.initializationContext, null, null); this.loggingSystem.setLogLevel(getClass().getName(), LogLevel.DEBUG); List configurations = this.loggingSystem.getLoggerConfigurations(); assertThat(configurations).isNotEmpty(); @@ -181,7 +190,7 @@ class Log4J2LoggingSystemTests extends AbstractLoggingSystemTests { void getLoggingConfigurationsShouldReturnAllLoggers() { LogManager.getLogger("org.springframework.boot.logging.log4j2.Log4J2LoggingSystemTests$Nested"); this.loggingSystem.beforeInitialize(); - this.loggingSystem.initialize(null, null, null); + this.loggingSystem.initialize(this.initializationContext, null, null); this.loggingSystem.setLogLevel(getClass().getName(), LogLevel.DEBUG); List configurations = this.loggingSystem.getLoggerConfigurations(); assertThat(configurations).isNotEmpty(); @@ -202,7 +211,7 @@ class Log4J2LoggingSystemTests extends AbstractLoggingSystemTests { @Test void getLoggingConfiguration() { this.loggingSystem.beforeInitialize(); - this.loggingSystem.initialize(null, null, null); + this.loggingSystem.initialize(this.initializationContext, null, null); this.loggingSystem.setLogLevel(getClass().getName(), LogLevel.DEBUG); LoggerConfiguration configuration = this.loggingSystem.getLoggerConfiguration(getClass().getName()); assertThat(configuration) @@ -212,7 +221,7 @@ class Log4J2LoggingSystemTests extends AbstractLoggingSystemTests { @Test void getLoggingConfigurationShouldReturnLoggerWithNullConfiguredLevel() { this.loggingSystem.beforeInitialize(); - this.loggingSystem.initialize(null, null, null); + this.loggingSystem.initialize(this.initializationContext, null, null); this.loggingSystem.setLogLevel(getClass().getName(), LogLevel.DEBUG); LoggerConfiguration configuration = this.loggingSystem.getLoggerConfiguration("org"); assertThat(configuration).isEqualTo(new LoggerConfiguration("org", null, LogLevel.INFO)); @@ -221,7 +230,7 @@ class Log4J2LoggingSystemTests extends AbstractLoggingSystemTests { @Test void getLoggingConfigurationForNonExistentLoggerShouldReturnNull() { this.loggingSystem.beforeInitialize(); - this.loggingSystem.initialize(null, null, null); + this.loggingSystem.initialize(this.initializationContext, null, null); this.loggingSystem.setLogLevel(getClass().getName(), LogLevel.DEBUG); LoggerConfiguration configuration = this.loggingSystem.getLoggerConfiguration("doesnotexist"); assertThat(configuration).isEqualTo(null); @@ -230,7 +239,7 @@ class Log4J2LoggingSystemTests extends AbstractLoggingSystemTests { @Test void setLevelOfUnconfiguredLoggerDoesNotAffectRootConfiguration(CapturedOutput output) { this.loggingSystem.beforeInitialize(); - this.loggingSystem.initialize(null, null, null); + this.loggingSystem.initialize(this.initializationContext, null, null); LogManager.getRootLogger().debug("Hello"); this.loggingSystem.setLogLevel("foo.bar.baz", LogLevel.DEBUG); LogManager.getRootLogger().debug("Hello"); @@ -241,7 +250,7 @@ class Log4J2LoggingSystemTests extends AbstractLoggingSystemTests { @Disabled("Uses Logback unintentionally") void loggingThatUsesJulIsCaptured(CapturedOutput output) { this.loggingSystem.beforeInitialize(); - this.loggingSystem.initialize(null, null, null); + this.loggingSystem.initialize(this.initializationContext, null, null); java.util.logging.Logger julLogger = java.util.logging.Logger.getLogger(getClass().getName()); julLogger.setLevel(java.util.logging.Level.INFO); julLogger.severe("Hello world"); @@ -289,7 +298,7 @@ class Log4J2LoggingSystemTests extends AbstractLoggingSystemTests { @Test void exceptionsIncludeClassPackaging(CapturedOutput output) { this.loggingSystem.beforeInitialize(); - this.loggingSystem.initialize(null, getRelativeClasspathLocation("log4j2-file.xml"), + this.loggingSystem.initialize(this.initializationContext, getRelativeClasspathLocation("log4j2-file.xml"), getLogFile(null, tmpDir())); this.logger.warn("Expected exception", new RuntimeException("Expected")); String fileContents = contentOf(new File(tmpDir() + "/spring.log")); @@ -301,7 +310,7 @@ class Log4J2LoggingSystemTests extends AbstractLoggingSystemTests { void beforeInitializeFilterDisablesErrorLogging() { this.loggingSystem.beforeInitialize(); assertThat(this.logger.isErrorEnabled()).isFalse(); - this.loggingSystem.initialize(null, null, getLogFile(null, tmpDir())); + this.loggingSystem.initialize(this.initializationContext, null, getLogFile(null, tmpDir())); } @Test @@ -310,7 +319,7 @@ class Log4J2LoggingSystemTests extends AbstractLoggingSystemTests { try { this.loggingSystem.beforeInitialize(); this.logger.info("Hidden"); - this.loggingSystem.initialize(null, getRelativeClasspathLocation("log4j2-file.xml"), + this.loggingSystem.initialize(this.initializationContext, getRelativeClasspathLocation("log4j2-file.xml"), getLogFile(null, tmpDir())); this.logger.warn("Expected exception", new RuntimeException("Expected", new RuntimeException("Cause"))); String fileContents = contentOf(new File(tmpDir() + "/spring.log")); @@ -328,22 +337,22 @@ class Log4J2LoggingSystemTests extends AbstractLoggingSystemTests { PropertyChangeListener listener = mock(PropertyChangeListener.class); loggerContext.addPropertyChangeListener(listener); this.loggingSystem.beforeInitialize(); - this.loggingSystem.initialize(null, null, null); + this.loggingSystem.initialize(this.initializationContext, null, null); this.loggingSystem.beforeInitialize(); - this.loggingSystem.initialize(null, null, null); + this.loggingSystem.initialize(this.initializationContext, null, null); this.loggingSystem.beforeInitialize(); - this.loggingSystem.initialize(null, null, null); + this.loggingSystem.initialize(this.initializationContext, null, null); verify(listener, times(2)).propertyChange(any(PropertyChangeEvent.class)); this.loggingSystem.cleanUp(); this.loggingSystem.beforeInitialize(); - this.loggingSystem.initialize(null, null, null); + this.loggingSystem.initialize(this.initializationContext, null, null); verify(listener, times(4)).propertyChange(any(PropertyChangeEvent.class)); } @Test void getLoggingConfigurationWithResetLevelReturnsNull() { this.loggingSystem.beforeInitialize(); - this.loggingSystem.initialize(null, null, null); + this.loggingSystem.initialize(this.initializationContext, null, null); this.loggingSystem.setLogLevel("com.example", LogLevel.WARN); this.loggingSystem.setLogLevel("com.example.test", LogLevel.DEBUG); LoggerConfiguration configuration = this.loggingSystem.getLoggerConfiguration("com.example.test"); @@ -358,7 +367,7 @@ class Log4J2LoggingSystemTests extends AbstractLoggingSystemTests { void getLoggingConfigurationWithResetLevelWhenAlreadyConfiguredReturnsParentConfiguredLevel() { LoggerContext loggerContext = (LoggerContext) LogManager.getContext(false); this.loggingSystem.beforeInitialize(); - this.loggingSystem.initialize(null, null, null); + this.loggingSystem.initialize(this.initializationContext, null, null); loggerContext.getConfiguration().addLogger("com.example.test", new LoggerConfig("com.example.test", org.apache.logging.log4j.Level.INFO, false)); this.loggingSystem.setLogLevel("com.example", LogLevel.WARN); @@ -379,6 +388,20 @@ class Log4J2LoggingSystemTests extends AbstractLoggingSystemTests { .isFalse(); } + @Test + void compositeConfigurationWithCustomBaseConfiguration() { + this.environment.setProperty("logging.log4j2.config.override", "src/test/resources/log4j2-override.xml"); + this.loggingSystem.initialize(this.initializationContext, "src/test/resources/log4j2-nondefault.xml", null); + assertThat(this.loggingSystem.getConfiguration()).isInstanceOf(CompositeConfiguration.class); + } + + @Test + void compositeConfigurationWithStandardConfigLocationConfiguration() { + this.environment.setProperty("logging.log4j2.config.override", "src/test/resources/log4j2-override.xml"); + this.loggingSystem.initialize(this.initializationContext, null, null); + assertThat(this.loggingSystem.getConfiguration()).isInstanceOf(CompositeConfiguration.class); + } + private String getRelativeClasspathLocation(String fileName) { String defaultPath = ClassUtils.getPackageName(getClass()); defaultPath = defaultPath.replace('.', '/'); diff --git a/spring-boot-project/spring-boot/src/test/resources/log4j2-override.xml b/spring-boot-project/spring-boot/src/test/resources/log4j2-override.xml new file mode 100644 index 0000000000..f0cdfa02d2 --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/resources/log4j2-override.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/checkstyle/import-control.xml b/src/checkstyle/import-control.xml index db983dd578..af145b76d5 100644 --- a/src/checkstyle/import-control.xml +++ b/src/checkstyle/import-control.xml @@ -64,6 +64,7 @@ +