From 48154a393fcb92330daf9636df2c427c9c740313 Mon Sep 17 00:00:00 2001 From: Dmytro Nosan Date: Wed, 26 Feb 2025 22:41:45 +0200 Subject: [PATCH 1/2] Add support for optional Log4J2 configuration This commit adds support for the standard 'optional:' prefix in Log4j2 override file locations, ensuring missing files are ignored without throwing exceptions. See gh-44488 Signed-off-by: Dmytro Nosan --- .../antora/modules/how-to/pages/logging.adoc | 2 + .../logging/log4j2/Log4J2LoggingSystem.java | 58 ++++++++++++++----- .../log4j2/Log4J2LoggingSystemTests.java | 43 ++++++++++++++ 3 files changed, 89 insertions(+), 14 deletions(-) diff --git a/spring-boot-project/spring-boot-docs/src/docs/antora/modules/how-to/pages/logging.adoc b/spring-boot-project/spring-boot-docs/src/docs/antora/modules/how-to/pages/logging.adoc index e053810ac61..ab3ac41942e 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/antora/modules/how-to/pages/logging.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/antora/modules/how-to/pages/logging.adoc @@ -201,3 +201,5 @@ To configure Log4j 2 to use an alternative configuration file format, add the ap 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. + +NOTE: Log4j2 override configuration file locations can be prefixed with `optional:`, for example, `optional:classpath:log4j2-override.xml`, to indicate that the location is optional and should only be loaded if the resource exists. 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 8544ebff265..fbd33e79622 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 @@ -16,6 +16,7 @@ package org.springframework.boot.logging.log4j2; +import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.net.URL; @@ -68,6 +69,7 @@ import org.springframework.core.Conventions; import org.springframework.core.annotation.Order; import org.springframework.core.env.Environment; import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.CollectionUtils; @@ -85,6 +87,8 @@ import org.springframework.util.StringUtils; */ public class Log4J2LoggingSystem extends AbstractLoggingSystem { + private static final String OPTIONAL_PREFIX = "optional:"; + private static final String LOG4J_BRIDGE_HANDLER = "org.apache.logging.log4j.jul.Log4jBridgeHandler"; private static final String LOG4J_LOG_MANAGER = "org.apache.logging.log4j.jul.LogManager"; @@ -269,21 +273,42 @@ public class Log4J2LoggingSystem extends AbstractLoggingSystem { try { List configurations = new ArrayList<>(); LoggerContext context = getLoggerContext(); - configurations.add(load(location, context)); + ResourceLoader resourceLoader = ApplicationResourceLoader.get(); + configurations.add(loadConfiguration(resourceLoader, location, context)); for (String override : overrides) { - configurations.add(load(override, context)); + Configuration overrideConfiguration = loadOptionalConfiguration(resourceLoader, override, context); + if (overrideConfiguration != null) { + configurations.add(overrideConfiguration); + } } - Configuration configuration = (configurations.size() > 1) ? createComposite(configurations) - : configurations.iterator().next(); - context.start(configuration); + context.start(createCompositeConfigurationIfNecessary(configurations)); } catch (Exception ex) { throw new IllegalStateException("Could not initialize Log4J2 logging from " + location, ex); } } - private Configuration load(String location, LoggerContext context) throws IOException { - Resource resource = ApplicationResourceLoader.get().getResource(location); + private Configuration loadOptionalConfiguration(ResourceLoader resourceLoader, String location, + LoggerContext context) throws IOException { + if (location.startsWith(OPTIONAL_PREFIX)) { + Resource resource = resourceLoader.getResource(location.substring(OPTIONAL_PREFIX.length())); + try { + return (resource.exists()) ? loadConfiguration(resource, context) : null; + } + catch (FileNotFoundException ex) { + return null; + } + } + return loadConfiguration(resourceLoader, location, context); + } + + private Configuration loadConfiguration(ResourceLoader resourceLoader, String location, LoggerContext context) + throws IOException { + Resource resource = resourceLoader.getResource(location); + return loadConfiguration(resource, context); + } + + private Configuration loadConfiguration(Resource resource, LoggerContext context) throws IOException { ConfigurationFactory factory = ConfigurationFactory.getInstance(); if (resource.isFile()) { try (InputStream inputStream = resource.getInputStream()) { @@ -303,7 +328,10 @@ public class Log4J2LoggingSystem extends AbstractLoggingSystem { } } - private CompositeConfiguration createComposite(List configurations) { + private Configuration createCompositeConfigurationIfNecessary(List configurations) { + if (configurations.size() == 1) { + return configurations.iterator().next(); + } return new CompositeConfiguration(configurations.stream().map(AbstractConfiguration.class::cast).toList()); } @@ -321,19 +349,21 @@ public class Log4J2LoggingSystem extends AbstractLoggingSystem { private void reinitializeWithOverrides(List overrides) { LoggerContext context = getLoggerContext(); - Configuration base = context.getConfiguration(); - List configurations = new ArrayList<>(); - configurations.add((AbstractConfiguration) base); + List configurations = new ArrayList<>(); + configurations.add(context.getConfiguration()); + ResourceLoader resourceLoader = ApplicationResourceLoader.get(); for (String override : overrides) { try { - configurations.add((AbstractConfiguration) load(override, context)); + Configuration overrideConfiguration = loadOptionalConfiguration(resourceLoader, override, context); + if (overrideConfiguration != null) { + configurations.add(overrideConfiguration); + } } catch (IOException ex) { throw new RuntimeException("Failed to load overriding configuration from '" + override + "'", ex); } } - CompositeConfiguration composite = new CompositeConfiguration(configurations); - context.reconfigure(composite); + context.reconfigure(createCompositeConfigurationIfNecessary(configurations)); } @Override 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 2eb022235a6..b843daf44ff 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 @@ -43,6 +43,7 @@ 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.config.plugins.util.PluginRegistry; +import org.apache.logging.log4j.core.config.xml.XmlConfiguration; import org.apache.logging.log4j.core.util.ShutdownCallbackRegistry; import org.apache.logging.log4j.jul.Log4jBridgeHandler; import org.apache.logging.log4j.status.StatusListener; @@ -453,6 +454,48 @@ class Log4J2LoggingSystemTests extends AbstractLoggingSystemTests { .isFalse(); } + @Test + @WithNonDefaultXmlResource + void loadOptionalOverrideConfigurationWhenDoesNotExist() { + this.environment.setProperty("logging.log4j2.config.override", "optional:classpath:override.xml"); + this.loggingSystem.initialize(this.initializationContext, "classpath:nondefault.xml", null); + assertThat(this.loggingSystem.getConfiguration()).isInstanceOf(XmlConfiguration.class); + } + + @Test + void loadOptionalOverrideConfigurationWhenDoesNotExistUponReinitialization() { + this.environment.setProperty("logging.log4j2.config.override", "optional:classpath:override.xml"); + this.loggingSystem.beforeInitialize(); + this.loggingSystem.initialize(this.initializationContext, null, null); + assertThat(this.loggingSystem.getConfiguration()).isInstanceOf(XmlConfiguration.class); + this.loggingSystem.cleanUp(); + this.loggingSystem.beforeInitialize(); + this.loggingSystem.initialize(this.initializationContext, null, null); + assertThat(this.loggingSystem.getConfiguration()).isInstanceOf(XmlConfiguration.class); + } + + @Test + @WithNonDefaultXmlResource + @WithOverrideXmlResource + void loadOptionalOverrideConfiguration() { + this.environment.setProperty("logging.log4j2.config.override", "optional:classpath:override.xml"); + this.loggingSystem.initialize(this.initializationContext, "classpath:nondefault.xml", null); + assertThat(this.loggingSystem.getConfiguration()).isInstanceOf(CompositeConfiguration.class); + } + + @Test + @WithOverrideXmlResource + void loadOptionalOverrideConfigurationUponReinitialization() { + this.environment.setProperty("logging.log4j2.config.override", "optional:classpath:override.xml"); + this.loggingSystem.beforeInitialize(); + this.loggingSystem.initialize(this.initializationContext, null, null); + assertThat(this.loggingSystem.getConfiguration()).isInstanceOf(CompositeConfiguration.class); + this.loggingSystem.cleanUp(); + this.loggingSystem.beforeInitialize(); + this.loggingSystem.initialize(this.initializationContext, null, null); + assertThat(this.loggingSystem.getConfiguration()).isInstanceOf(CompositeConfiguration.class); + } + @Test @WithNonDefaultXmlResource @WithOverrideXmlResource From 0e347afd31fa34be67884d3c44914c0f29e987f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Thu, 17 Apr 2025 13:59:53 +0200 Subject: [PATCH 2/2] Polish "Add support for optional Log4J2 configuration" See gh-44488 Signed-off-by: Dmytro Nosan --- .../antora/modules/how-to/pages/logging.adoc | 6 ++- .../logging/log4j2/Log4J2LoggingSystem.java | 48 ++++++++----------- ...itional-spring-configuration-metadata.json | 2 +- 3 files changed, 27 insertions(+), 29 deletions(-) diff --git a/spring-boot-project/spring-boot-docs/src/docs/antora/modules/how-to/pages/logging.adoc b/spring-boot-project/spring-boot-docs/src/docs/antora/modules/how-to/pages/logging.adoc index ab3ac41942e..980b34b913e 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/antora/modules/how-to/pages/logging.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/antora/modules/how-to/pages/logging.adoc @@ -202,4 +202,8 @@ Log4j 2 has support for combining multiple configuration files into a single com 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. -NOTE: Log4j2 override configuration file locations can be prefixed with `optional:`, for example, `optional:classpath:log4j2-override.xml`, to indicate that the location is optional and should only be loaded if the resource exists. +[NOTE] +==== +Log4j2 override configuration file locations can be prefixed with `optional:`. +For example, `optional:classpath:log4j2-override.xml` indicates that `log4j2-override.xml` should only be loaded if the resource exists. +==== 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 fbd33e79622..aad205422c9 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 @@ -274,41 +274,21 @@ public class Log4J2LoggingSystem extends AbstractLoggingSystem { List configurations = new ArrayList<>(); LoggerContext context = getLoggerContext(); ResourceLoader resourceLoader = ApplicationResourceLoader.get(); - configurations.add(loadConfiguration(resourceLoader, location, context)); + configurations.add(load(resourceLoader.getResource(location), context)); for (String override : overrides) { - Configuration overrideConfiguration = loadOptionalConfiguration(resourceLoader, override, context); + Configuration overrideConfiguration = loadOverride(resourceLoader, override, context); if (overrideConfiguration != null) { configurations.add(overrideConfiguration); } } - context.start(createCompositeConfigurationIfNecessary(configurations)); + context.start(mergeConfigurations(configurations)); } catch (Exception ex) { throw new IllegalStateException("Could not initialize Log4J2 logging from " + location, ex); } } - private Configuration loadOptionalConfiguration(ResourceLoader resourceLoader, String location, - LoggerContext context) throws IOException { - if (location.startsWith(OPTIONAL_PREFIX)) { - Resource resource = resourceLoader.getResource(location.substring(OPTIONAL_PREFIX.length())); - try { - return (resource.exists()) ? loadConfiguration(resource, context) : null; - } - catch (FileNotFoundException ex) { - return null; - } - } - return loadConfiguration(resourceLoader, location, context); - } - - private Configuration loadConfiguration(ResourceLoader resourceLoader, String location, LoggerContext context) - throws IOException { - Resource resource = resourceLoader.getResource(location); - return loadConfiguration(resource, context); - } - - private Configuration loadConfiguration(Resource resource, LoggerContext context) throws IOException { + private Configuration load(Resource resource, LoggerContext context) throws IOException { ConfigurationFactory factory = ConfigurationFactory.getInstance(); if (resource.isFile()) { try (InputStream inputStream = resource.getInputStream()) { @@ -328,7 +308,21 @@ public class Log4J2LoggingSystem extends AbstractLoggingSystem { } } - private Configuration createCompositeConfigurationIfNecessary(List configurations) { + private Configuration loadOverride(ResourceLoader resourceLoader, String location, LoggerContext context) + throws IOException { + if (location.startsWith(OPTIONAL_PREFIX)) { + Resource resource = resourceLoader.getResource(location.substring(OPTIONAL_PREFIX.length())); + try { + return (resource.exists()) ? load(resource, context) : null; + } + catch (FileNotFoundException ex) { + return null; + } + } + return load(resourceLoader.getResource(location), context); + } + + private Configuration mergeConfigurations(List configurations) { if (configurations.size() == 1) { return configurations.iterator().next(); } @@ -354,7 +348,7 @@ public class Log4J2LoggingSystem extends AbstractLoggingSystem { ResourceLoader resourceLoader = ApplicationResourceLoader.get(); for (String override : overrides) { try { - Configuration overrideConfiguration = loadOptionalConfiguration(resourceLoader, override, context); + Configuration overrideConfiguration = loadOverride(resourceLoader, override, context); if (overrideConfiguration != null) { configurations.add(overrideConfiguration); } @@ -363,7 +357,7 @@ public class Log4J2LoggingSystem extends AbstractLoggingSystem { throw new RuntimeException("Failed to load overriding configuration from '" + override + "'", ex); } } - context.reconfigure(createCompositeConfigurationIfNecessary(configurations)); + context.reconfigure(mergeConfigurations(configurations)); } @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 9066d1a1548..ca6e639151e 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 @@ -126,7 +126,7 @@ { "name": "logging.log4j2.config.override", "type": "java.util.List", - "description": "Overriding configuration files used to create a composite configuration." + "description": "Overriding configuration files used to create a composite configuration. Can be prefixed with 'optional:' to only load the override if it exists." }, { "name": "logging.logback.rollingpolicy.clean-history-on-start",