Merge pull request #44488 from nosan

* pr/44488:
  Polish "Add support for optional Log4J2 configuration"
  Add support for optional Log4J2 configuration

Closes gh-44488
This commit is contained in:
Stéphane Nicoll 2025-04-17 14:14:40 +02:00
commit 4d67fbb5c3
4 changed files with 88 additions and 15 deletions

View File

@ -201,3 +201,9 @@ 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` indicates that `log4j2-override.xml` should only be loaded if the resource exists.
====

View File

@ -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,22 @@ public class Log4J2LoggingSystem extends AbstractLoggingSystem {
try {
List<Configuration> configurations = new ArrayList<>();
LoggerContext context = getLoggerContext();
configurations.add(load(location, context));
ResourceLoader resourceLoader = ApplicationResourceLoader.get();
configurations.add(load(resourceLoader.getResource(location), context));
for (String override : overrides) {
configurations.add(load(override, context));
Configuration overrideConfiguration = loadOverride(resourceLoader, override, context);
if (overrideConfiguration != null) {
configurations.add(overrideConfiguration);
}
}
Configuration configuration = (configurations.size() > 1) ? createComposite(configurations)
: configurations.iterator().next();
context.start(configuration);
context.start(mergeConfigurations(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 load(Resource resource, LoggerContext context) throws IOException {
ConfigurationFactory factory = ConfigurationFactory.getInstance();
if (resource.isFile()) {
try (InputStream inputStream = resource.getInputStream()) {
@ -303,7 +308,24 @@ public class Log4J2LoggingSystem extends AbstractLoggingSystem {
}
}
private CompositeConfiguration createComposite(List<Configuration> 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<Configuration> configurations) {
if (configurations.size() == 1) {
return configurations.iterator().next();
}
return new CompositeConfiguration(configurations.stream().map(AbstractConfiguration.class::cast).toList());
}
@ -321,19 +343,21 @@ public class Log4J2LoggingSystem extends AbstractLoggingSystem {
private void reinitializeWithOverrides(List<String> overrides) {
LoggerContext context = getLoggerContext();
Configuration base = context.getConfiguration();
List<AbstractConfiguration> configurations = new ArrayList<>();
configurations.add((AbstractConfiguration) base);
List<Configuration> configurations = new ArrayList<>();
configurations.add(context.getConfiguration());
ResourceLoader resourceLoader = ApplicationResourceLoader.get();
for (String override : overrides) {
try {
configurations.add((AbstractConfiguration) load(override, context));
Configuration overrideConfiguration = loadOverride(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(mergeConfigurations(configurations));
}
@Override

View File

@ -126,7 +126,7 @@
{
"name": "logging.log4j2.config.override",
"type": "java.util.List<java.lang.String>",
"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",

View File

@ -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