Add support for Log4j2's composite configuration

Closes gh-27110
pull/28398/head
Andy Wilkinson 3 years ago
parent d6aab2fe84
commit 89b40e1e00

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

@ -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<String> getOverrides(LoggingInitializationContext initializationContext) {
BindResult<List<String>> 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<String> 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<Configuration> 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<Configuration> configurations) {
return new CompositeConfiguration(
configurations.stream().map(AbstractConfiguration.class::cast).collect(Collectors.toList()));
}
@Override
protected void reinitialize(LoggingInitializationContext initializationContext) {
getLoggerContext().reconfigure();
List<String> overrides = getOverrides(initializationContext);
if (!CollectionUtils.isEmpty(overrides)) {
reinitializeWithOverrides(overrides);
}
else {
LoggerContext context = getLoggerContext();
context.reconfigure();
}
}
private void reinitializeWithOverrides(List<String> overrides) {
LoggerContext context = getLoggerContext();
Configuration base = context.getConfiguration();
List<AbstractConfiguration> 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

@ -198,6 +198,11 @@
"sourceType": "org.springframework.boot.context.logging.LoggingApplicationListener",
"defaultValue": 7
},
{
"name": "logging.log4j2.config.override",
"type": "java.util.List<java.lang.String>",
"description": "Overriding configuration files used to create a composite configuration."
},
{
"name": "spring.application.index",
"type": "java.lang.Integer",

@ -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<LoggerConfiguration> 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<LoggerConfiguration> 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('.', '/');

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN" monitorInterval="30">
<Appenders>
<Console name="Console" target="SYSTEM_OUT" follow="true">
<PatternLayout pattern="${LOG_PATTERN}"/>
</Console>
</Appenders>
</Configuration>

@ -64,6 +64,7 @@
<!-- Logging -->
<subpackage name="logging">
<allow pkg="org.springframework.boot.context.properties.bind" />
<disallow pkg="org.springframework.context" />
<disallow pkg="org.springframework.boot.context" />
</subpackage>

Loading…
Cancel
Save