From 64eb36b7a187886fe14dce36b66edca21a414366 Mon Sep 17 00:00:00 2001 From: Ralph Goers Date: Wed, 12 Oct 2022 11:16:58 -0700 Subject: [PATCH 01/13] Support 'log4j.configurationFile' system property Update `Log4J2LoggingSystem.getStandardConfigLocations()` so that any configured 'log4j.configurationFile' system property is also included as a location. See gh-32730 --- .../boot/logging/log4j2/Log4J2LoggingSystem.java | 7 +++++++ 1 file changed, 7 insertions(+) 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 d4bec28fb6..8835ad25c5 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 @@ -24,6 +24,7 @@ import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Properties; import java.util.Set; import java.util.logging.ConsoleHandler; import java.util.logging.Handler; @@ -45,6 +46,7 @@ import org.apache.logging.log4j.core.filter.AbstractFilter; import org.apache.logging.log4j.core.util.NameUtil; import org.apache.logging.log4j.jul.Log4jBridgeHandler; import org.apache.logging.log4j.message.Message; +import org.apache.logging.log4j.util.PropertiesUtil; import org.springframework.boot.context.properties.bind.BindResult; import org.springframework.boot.context.properties.bind.Bindable; @@ -137,6 +139,11 @@ public class Log4J2LoggingSystem extends AbstractLoggingSystem { Collections.addAll(supportedConfigLocations, "log4j2.json", "log4j2.jsn"); } supportedConfigLocations.add("log4j2.xml"); + PropertiesUtil props = new PropertiesUtil(new Properties()); + String location = props.getStringProperty(ConfigurationFactory.CONFIGURATION_FILE_PROPERTY); + if (location != null) { + supportedConfigLocations.add(location); + } return StringUtils.toStringArray(supportedConfigLocations); } From a08a6378f0dd00616125183a5f4df9cba8ede2b1 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Wed, 12 Oct 2022 11:35:47 -0700 Subject: [PATCH 02/13] Polish 'Support 'log4j.configurationFile' system property' See gh-32730 --- .../logging/log4j2/Log4J2LoggingSystem.java | 39 ++++++++----------- .../log4j2/Log4J2LoggingSystemTests.java | 13 +++++++ 2 files changed, 29 insertions(+), 23 deletions(-) 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 8835ad25c5..f7aad8dee2 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 @@ -73,6 +73,7 @@ import org.springframework.util.StringUtils; * @author Andy Wilkinson * @author Alexander Heusingfeld * @author Ben Hale + * @author Ralph Goers * @since 1.2.0 */ public class Log4J2LoggingSystem extends AbstractLoggingSystem { @@ -125,37 +126,29 @@ public class Log4J2LoggingSystem extends AbstractLoggingSystem { @Override protected String[] getStandardConfigLocations() { - return getCurrentlySupportedConfigLocations(); - } - - private String[] getCurrentlySupportedConfigLocations() { - List supportedConfigLocations = new ArrayList<>(); - addTestFiles(supportedConfigLocations); - supportedConfigLocations.add("log4j2.properties"); + List locations = new ArrayList<>(); + locations.add("log4j2-test.properties"); if (isClassAvailable("com.fasterxml.jackson.dataformat.yaml.YAMLParser")) { - Collections.addAll(supportedConfigLocations, "log4j2.yaml", "log4j2.yml"); + Collections.addAll(locations, "log4j2-test.yaml", "log4j2-test.yml"); } if (isClassAvailable("com.fasterxml.jackson.databind.ObjectMapper")) { - Collections.addAll(supportedConfigLocations, "log4j2.json", "log4j2.jsn"); + Collections.addAll(locations, "log4j2-test.json", "log4j2-test.jsn"); } - supportedConfigLocations.add("log4j2.xml"); - PropertiesUtil props = new PropertiesUtil(new Properties()); - String location = props.getStringProperty(ConfigurationFactory.CONFIGURATION_FILE_PROPERTY); - if (location != null) { - supportedConfigLocations.add(location); - } - return StringUtils.toStringArray(supportedConfigLocations); - } - - private void addTestFiles(List supportedConfigLocations) { - supportedConfigLocations.add("log4j2-test.properties"); + locations.add("log4j2-test.xml"); + locations.add("log4j2.properties"); if (isClassAvailable("com.fasterxml.jackson.dataformat.yaml.YAMLParser")) { - Collections.addAll(supportedConfigLocations, "log4j2-test.yaml", "log4j2-test.yml"); + Collections.addAll(locations, "log4j2.yaml", "log4j2.yml"); } if (isClassAvailable("com.fasterxml.jackson.databind.ObjectMapper")) { - Collections.addAll(supportedConfigLocations, "log4j2-test.json", "log4j2-test.jsn"); + Collections.addAll(locations, "log4j2.json", "log4j2.jsn"); } - supportedConfigLocations.add("log4j2-test.xml"); + locations.add("log4j2.xml"); + String propertyDefinedLocation = new PropertiesUtil(new Properties()) + .getStringProperty(ConfigurationFactory.CONFIGURATION_FILE_PROPERTY); + if (propertyDefinedLocation != null) { + locations.add(propertyDefinedLocation); + } + return StringUtils.toStringArray(locations); } protected boolean isClassAvailable(String className) { 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 a6ff8a1309..6045e50a3f 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.LogManager; import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.core.LoggerContext; import org.apache.logging.log4j.core.config.Configuration; +import org.apache.logging.log4j.core.config.ConfigurationFactory; 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; @@ -295,6 +296,18 @@ class Log4J2LoggingSystemTests extends AbstractLoggingSystemTests { "log4j2.properties", "log4j2.yaml", "log4j2.yml", "log4j2.json", "log4j2.jsn", "log4j2.xml"); } + @Test + void configLocationsWithConfigurationFileSystemProperty() { + System.setProperty(ConfigurationFactory.CONFIGURATION_FILE_PROPERTY, "custom-log4j2.properties"); + try { + assertThat(this.loggingSystem.getStandardConfigLocations()).contains("log4j2-test.properties", + "log4j2-test.xml", "log4j2.properties", "log4j2.xml"); + } + finally { + System.clearProperty(ConfigurationFactory.CONFIGURATION_FILE_PROPERTY); + } + } + @Test void springConfigLocations() { String[] locations = getSpringConfigLocations(this.loggingSystem); From 05a2bd458562cff76d0cbf4c27977c5fe59f9de7 Mon Sep 17 00:00:00 2001 From: Ralph Goers Date: Wed, 12 Oct 2022 11:17:09 -0700 Subject: [PATCH 03/13] Add Spring Environment to LoggerContext Update `Log4J2LoggingSystem` to add the Spring `Environment` to Log4j2's `LoggerContext`. This allow Log4j2 plugins to access the `Environment` if they need it. See gh-32731 --- .../boot/logging/log4j2/Log4J2LoggingSystem.java | 8 ++++++++ 1 file changed, 8 insertions(+) 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 f7aad8dee2..d36a6d0f43 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 @@ -60,6 +60,7 @@ import org.springframework.boot.logging.LoggingSystem; import org.springframework.boot.logging.LoggingSystemFactory; import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; +import org.springframework.core.env.Environment; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.CollectionUtils; @@ -84,6 +85,11 @@ public class Log4J2LoggingSystem extends AbstractLoggingSystem { private static final String LOG4J_LOG_MANAGER = "org.apache.logging.log4j.jul.LogManager"; + /** + * Identifies the Spring environment. + */ + public static final String ENVIRONMENT_KEY = "SpringEnvironment"; + private static final LogLevels LEVELS = new LogLevels<>(); static { @@ -227,6 +233,8 @@ public class Log4J2LoggingSystem extends AbstractLoggingSystem { if (isAlreadyInitialized(loggerContext)) { return; } + Environment environment = initializationContext.getEnvironment(); + getLoggerContext().putObjectIfAbsent(ENVIRONMENT_KEY, environment); loggerContext.getConfiguration().removeFilter(FILTER); super.initialize(initializationContext, configLocation, logFile); markAsInitialized(loggerContext); From d665441ca955418d2456408d5483144f35ef6825 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Wed, 12 Oct 2022 11:17:09 -0700 Subject: [PATCH 04/13] Polish 'Add Spring Environment to LoggerContext' See gh-32731 --- .../logging/log4j2/Log4J2LoggingSystem.java | 18 ++++++++++++++---- .../log4j2/Log4J2LoggingSystemTests.java | 10 ++++++++++ 2 files changed, 24 insertions(+), 4 deletions(-) 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 d36a6d0f43..b1ec7c67a1 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 @@ -58,6 +58,7 @@ import org.springframework.boot.logging.LoggerConfiguration; import org.springframework.boot.logging.LoggingInitializationContext; import org.springframework.boot.logging.LoggingSystem; import org.springframework.boot.logging.LoggingSystemFactory; +import org.springframework.core.Conventions; import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; import org.springframework.core.env.Environment; @@ -85,10 +86,8 @@ public class Log4J2LoggingSystem extends AbstractLoggingSystem { private static final String LOG4J_LOG_MANAGER = "org.apache.logging.log4j.jul.LogManager"; - /** - * Identifies the Spring environment. - */ - public static final String ENVIRONMENT_KEY = "SpringEnvironment"; + static final String ENVIRONMENT_KEY = Conventions.getQualifiedAttributeName(Log4J2LoggingSystem.class, + "environment"); private static final LogLevels LEVELS = new LogLevels<>(); @@ -475,6 +474,17 @@ public class Log4J2LoggingSystem extends AbstractLoggingSystem { loggerContext.setExternalContext(null); } + /** + * Get the Spring {@link Environment} attached to the given {@link LoggerContext} or + * {@code null} if no environment is available. + * @param loggerContext the logger context + * @return the Spring {@link Environment} or {@code null} + * @since 3.0.0 + */ + public static Environment getEnvironment(LoggerContext loggerContext) { + return (Environment) ((loggerContext != null) ? loggerContext.getObject(ENVIRONMENT_KEY) : null); + } + /** * {@link LoggingSystemFactory} that returns {@link Log4J2LoggingSystem} if possible. */ 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 6045e50a3f..3155bf13f8 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 @@ -57,6 +57,7 @@ import org.springframework.boot.testsupport.classpath.ClassPathExclusions; import org.springframework.boot.testsupport.logging.ConfigureClasspathToPreferLog4j2; import org.springframework.boot.testsupport.system.CapturedOutput; import org.springframework.boot.testsupport.system.OutputCaptureExtension; +import org.springframework.core.env.Environment; import org.springframework.mock.env.MockEnvironment; import org.springframework.util.ClassUtils; import org.springframework.util.StringUtils; @@ -438,6 +439,15 @@ class Log4J2LoggingSystemTests extends AbstractLoggingSystemTests { assertThat(this.loggingSystem.getConfiguration()).isInstanceOf(CompositeConfiguration.class); } + @Test + void initializeAttachesEnvironmentToLoggerContext() { + this.loggingSystem.beforeInitialize(); + this.loggingSystem.initialize(this.initializationContext, null, null); + LoggerContext loggerContext = (LoggerContext) LogManager.getContext(false); + Environment environment = Log4J2LoggingSystem.getEnvironment(loggerContext); + assertThat(environment).isSameAs(this.environment); + } + private String getRelativeClasspathLocation(String fileName) { String defaultPath = ClassUtils.getPackageName(getClass()); defaultPath = defaultPath.replace('.', '/'); From 5228b99b22469ffc82032873331da4e647892306 Mon Sep 17 00:00:00 2001 From: Ralph Goers Date: Wed, 12 Oct 2022 11:17:18 -0700 Subject: [PATCH 05/13] Support Log4J2 string lookups from the Spring Environment Add a Log4j2 `SpringLookup` plugin which can be used to resolve strings from the Spring Environment. See gh-32732 --- .../boot/logging/log4j2/SpringLookup.java | 112 ++++++++++++++++++ 1 file changed, 112 insertions(+) create mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/SpringLookup.java diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/SpringLookup.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/SpringLookup.java new file mode 100644 index 0000000000..bcae1db7b3 --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/SpringLookup.java @@ -0,0 +1,112 @@ +/* + * Copyright 2012-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.logging.log4j2; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.core.LogEvent; +import org.apache.logging.log4j.core.LoggerContext; +import org.apache.logging.log4j.core.config.LoggerContextAware; +import org.apache.logging.log4j.core.config.plugins.Plugin; +import org.apache.logging.log4j.core.lookup.StrLookup; +import org.apache.logging.log4j.status.StatusLogger; + +import org.springframework.core.env.Environment; + +/** + * Lookup for Spring properties. + * + * @author Ralph Goers + * @since 3.0.0 + */ +@Plugin(name = "spring", category = StrLookup.CATEGORY) +public class SpringLookup implements LoggerContextAware, StrLookup { + + private static final Logger LOGGER = StatusLogger.getLogger(); + + private static final String ACTIVE = "profiles.active"; + + private static final String DEFAULT = "profiles.default"; + + private static final String PATTERN = "\\[(\\d+?)\\]"; + + private static final Pattern ACTIVE_PATTERN = Pattern.compile(ACTIVE + PATTERN); + + private static final Pattern DEFAULT_PATTERN = Pattern.compile(DEFAULT + PATTERN); + + private volatile Environment environment; + + @Override + public String lookup(String key) { + if (this.environment == null) { + return null; + } + String lowerKey = key.toLowerCase(); + if (lowerKey.startsWith(ACTIVE)) { + return doMatch(ACTIVE_PATTERN, key, this.environment.getActiveProfiles()); + } + else if (lowerKey.startsWith(DEFAULT)) { + return doMatch(DEFAULT_PATTERN, key, this.environment.getDefaultProfiles()); + } + + return this.environment.getProperty(key); + } + + private String doMatch(Pattern pattern, String key, String[] profiles) { + if (profiles.length == 0) { + return null; + } + if (profiles.length == 1) { + return profiles[0]; + } + Matcher matcher = pattern.matcher(key); + if (matcher.matches()) { + try { + int index = Integer.parseInt(matcher.group(1)); + if (index < profiles.length) { + return profiles[index]; + } + LOGGER.warn("Index out of bounds for Spring default profiles: {}", index); + return null; + } + catch (Exception ex) { + LOGGER.warn("Unable to parse {} as integer value", matcher.group(1)); + return null; + } + + } + return String.join(",", profiles); + } + + @Override + public String lookup(LogEvent event, String key) { + return lookup((key)); + } + + @Override + public void setLoggerContext(final LoggerContext loggerContext) { + if (loggerContext != null) { + this.environment = (Environment) loggerContext.getObject(Log4J2LoggingSystem.ENVIRONMENT_KEY); + } + else { + LOGGER.warn("Attempt to set LoggerContext reference to null in SpringLookup"); + } + } + +} From 71f5857363f4c4fcc6cc2daedd5b5453cdb7f9f5 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Wed, 12 Oct 2022 12:07:46 -0700 Subject: [PATCH 06/13] Polish 'Support Log4J2 string lookups from the Spring Environment' See gh-32732 --- .../log4j2/SpringEnvironmentLookup.java | 55 +++++++++ .../boot/logging/log4j2/SpringLookup.java | 112 ------------------ .../log4j2/SpringEnvironmentLookupTests.java | 82 +++++++++++++ 3 files changed, 137 insertions(+), 112 deletions(-) create mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/SpringEnvironmentLookup.java delete mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/SpringLookup.java create mode 100644 spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/log4j2/SpringEnvironmentLookupTests.java diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/SpringEnvironmentLookup.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/SpringEnvironmentLookup.java new file mode 100644 index 0000000000..71889db912 --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/SpringEnvironmentLookup.java @@ -0,0 +1,55 @@ +/* + * Copyright 2012-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.logging.log4j2; + +import org.apache.logging.log4j.core.LogEvent; +import org.apache.logging.log4j.core.LoggerContext; +import org.apache.logging.log4j.core.config.LoggerContextAware; +import org.apache.logging.log4j.core.config.plugins.Plugin; +import org.apache.logging.log4j.core.lookup.StrLookup; + +import org.springframework.core.env.Environment; +import org.springframework.util.Assert; + +/** + * Lookup for Spring properties. + * + * @author Ralph Goers + * @author Phillip Webb + */ +@Plugin(name = "spring", category = StrLookup.CATEGORY) +class SpringEnvironmentLookup implements LoggerContextAware, StrLookup { + + private volatile Environment environment; + + @Override + public String lookup(LogEvent event, String key) { + return lookup(key); + } + + @Override + public String lookup(String key) { + Assert.state(this.environment != null, "Unable to obtain Spring Environment from LoggerContext"); + return (this.environment != null) ? this.environment.getProperty(key) : null; + } + + @Override + public void setLoggerContext(LoggerContext loggerContext) { + this.environment = Log4J2LoggingSystem.getEnvironment(loggerContext); + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/SpringLookup.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/SpringLookup.java deleted file mode 100644 index bcae1db7b3..0000000000 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/SpringLookup.java +++ /dev/null @@ -1,112 +0,0 @@ -/* - * Copyright 2012-2022 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.boot.logging.log4j2; - -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import org.apache.logging.log4j.Logger; -import org.apache.logging.log4j.core.LogEvent; -import org.apache.logging.log4j.core.LoggerContext; -import org.apache.logging.log4j.core.config.LoggerContextAware; -import org.apache.logging.log4j.core.config.plugins.Plugin; -import org.apache.logging.log4j.core.lookup.StrLookup; -import org.apache.logging.log4j.status.StatusLogger; - -import org.springframework.core.env.Environment; - -/** - * Lookup for Spring properties. - * - * @author Ralph Goers - * @since 3.0.0 - */ -@Plugin(name = "spring", category = StrLookup.CATEGORY) -public class SpringLookup implements LoggerContextAware, StrLookup { - - private static final Logger LOGGER = StatusLogger.getLogger(); - - private static final String ACTIVE = "profiles.active"; - - private static final String DEFAULT = "profiles.default"; - - private static final String PATTERN = "\\[(\\d+?)\\]"; - - private static final Pattern ACTIVE_PATTERN = Pattern.compile(ACTIVE + PATTERN); - - private static final Pattern DEFAULT_PATTERN = Pattern.compile(DEFAULT + PATTERN); - - private volatile Environment environment; - - @Override - public String lookup(String key) { - if (this.environment == null) { - return null; - } - String lowerKey = key.toLowerCase(); - if (lowerKey.startsWith(ACTIVE)) { - return doMatch(ACTIVE_PATTERN, key, this.environment.getActiveProfiles()); - } - else if (lowerKey.startsWith(DEFAULT)) { - return doMatch(DEFAULT_PATTERN, key, this.environment.getDefaultProfiles()); - } - - return this.environment.getProperty(key); - } - - private String doMatch(Pattern pattern, String key, String[] profiles) { - if (profiles.length == 0) { - return null; - } - if (profiles.length == 1) { - return profiles[0]; - } - Matcher matcher = pattern.matcher(key); - if (matcher.matches()) { - try { - int index = Integer.parseInt(matcher.group(1)); - if (index < profiles.length) { - return profiles[index]; - } - LOGGER.warn("Index out of bounds for Spring default profiles: {}", index); - return null; - } - catch (Exception ex) { - LOGGER.warn("Unable to parse {} as integer value", matcher.group(1)); - return null; - } - - } - return String.join(",", profiles); - } - - @Override - public String lookup(LogEvent event, String key) { - return lookup((key)); - } - - @Override - public void setLoggerContext(final LoggerContext loggerContext) { - if (loggerContext != null) { - this.environment = (Environment) loggerContext.getObject(Log4J2LoggingSystem.ENVIRONMENT_KEY); - } - else { - LOGGER.warn("Attempt to set LoggerContext reference to null in SpringLookup"); - } - } - -} diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/log4j2/SpringEnvironmentLookupTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/log4j2/SpringEnvironmentLookupTests.java new file mode 100644 index 0000000000..e3f7a5e175 --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/log4j2/SpringEnvironmentLookupTests.java @@ -0,0 +1,82 @@ +/* + * Copyright 2012-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.logging.log4j2; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.core.LoggerContext; +import org.apache.logging.log4j.core.lookup.Interpolator; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.mock.env.MockEnvironment; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; + +/** + * Tests for {@link SpringEnvironmentLookup}. + * + * @author Phillip Webb + */ +class SpringEnvironmentLookupTests { + + private MockEnvironment environment; + + private LoggerContext loggerContext; + + @BeforeEach + void setup() { + this.environment = new MockEnvironment(); + this.loggerContext = (LoggerContext) LogManager.getContext(false); + this.loggerContext.putObject(Log4J2LoggingSystem.ENVIRONMENT_KEY, this.environment); + } + + @AfterEach + void cleanup() { + this.loggerContext.removeObject(Log4J2LoggingSystem.ENVIRONMENT_KEY); + } + + @Test + void lookupWhenFoundInEnvironmentReturnsValue() { + this.environment.setProperty("test", "test"); + Interpolator lookup = createLookup(this.loggerContext); + assertThat(lookup.lookup("spring:test")).isEqualTo("test"); + } + + @Test + void lookupWhenNotFoundInEnvironmentReturnsNull() { + Interpolator lookup = createLookup(this.loggerContext); + assertThat(lookup.lookup("spring:test")).isNull(); + } + + @Test + void lookupWhenNoSpringEnvironmentThrowsException() { + this.loggerContext.removeObject(Log4J2LoggingSystem.ENVIRONMENT_KEY); + Interpolator lookup = createLookup(this.loggerContext); + assertThatIllegalStateException().isThrownBy(() -> assertThat(lookup.lookup("spring:test")).isEqualTo("test")) + .withMessage("Unable to obtain Spring Environment from LoggerContext"); + } + + private Interpolator createLookup(LoggerContext context) { + Interpolator lookup = new Interpolator(); + lookup.setConfiguration(context.getConfiguration()); + lookup.setLoggerContext(context); + return lookup; + } + +} From 4f8a9441c2d9668b3ceb5cfcd862c0a424248542 Mon Sep 17 00:00:00 2001 From: Ralph Goers Date: Wed, 12 Oct 2022 17:24:51 -0700 Subject: [PATCH 07/13] Add Log4J2 PropertySource backed by the Spring Environment Register a new `PropertySource` when initializing Log4j2 so that properties may be resolved against Spring's Environment. See gh-32733 --- .../logging/log4j2/Log4J2LoggingSystem.java | 1 + .../logging/log4j2/SpringPropertySource.java | 64 +++++++++++++++++++ 2 files changed, 65 insertions(+) create mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/SpringPropertySource.java 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 b1ec7c67a1..00c362e1b3 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 @@ -234,6 +234,7 @@ public class Log4J2LoggingSystem extends AbstractLoggingSystem { } Environment environment = initializationContext.getEnvironment(); getLoggerContext().putObjectIfAbsent(ENVIRONMENT_KEY, environment); + PropertiesUtil.getProperties().addPropertySource(new SpringPropertySource(environment)); loggerContext.getConfiguration().removeFilter(FILTER); super.initialize(initializationContext, configLocation, logFile); markAsInitialized(loggerContext); diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/SpringPropertySource.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/SpringPropertySource.java new file mode 100644 index 0000000000..ea366d993f --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/SpringPropertySource.java @@ -0,0 +1,64 @@ +/* + * Copyright 2012-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.logging.log4j2; + +import org.apache.logging.log4j.util.PropertySource; + +import org.springframework.core.env.Environment; + +/** + * Returns properties from Spring. + * + * @author Ralph Goers + * @since 3.0.0 + */ +public class SpringPropertySource implements PropertySource { + + private static final int DEFAULT_PRIORITY = -100; + + private final Environment environment; + + public SpringPropertySource(Environment environment) { + this.environment = environment; + } + + /** + * System properties take precedence followed by properties in Log4j properties files. + * @return this PropertySource's priority. + */ + @Override + public int getPriority() { + return DEFAULT_PRIORITY; + } + + @Override + public String getProperty(String key) { + if (this.environment != null) { + return this.environment.getProperty(key); + } + return null; + } + + @Override + public boolean containsProperty(String key) { + if (this.environment != null) { + return this.environment.containsProperty(key); + } + return false; + } + +} From ed424d3adb9f074e3b5021b7d3b343b290164389 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Wed, 12 Oct 2022 17:37:08 -0700 Subject: [PATCH 08/13] Polish 'Add Log4J2 PropertySource backed by the Spring Environment' See gh-32733 --- .../logging/log4j2/Log4J2LoggingSystem.java | 6 +- ...a => SpringEnvironmentPropertySource.java} | 34 ++++---- .../log4j2/Log4J2LoggingSystemTests.java | 24 ++++++ .../SpringEnvironmentPropertySourceTests.java | 83 +++++++++++++++++++ 4 files changed, 125 insertions(+), 22 deletions(-) rename spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/{SpringPropertySource.java => SpringEnvironmentPropertySource.java} (72%) create mode 100644 spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/log4j2/SpringEnvironmentPropertySourceTests.java 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 00c362e1b3..38c19b4689 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 @@ -233,8 +233,10 @@ public class Log4J2LoggingSystem extends AbstractLoggingSystem { return; } Environment environment = initializationContext.getEnvironment(); - getLoggerContext().putObjectIfAbsent(ENVIRONMENT_KEY, environment); - PropertiesUtil.getProperties().addPropertySource(new SpringPropertySource(environment)); + if (environment != null) { + getLoggerContext().putObjectIfAbsent(ENVIRONMENT_KEY, environment); + PropertiesUtil.getProperties().addPropertySource(new SpringEnvironmentPropertySource(environment)); + } loggerContext.getConfiguration().removeFilter(FILTER); super.initialize(initializationContext, configLocation, logFile); markAsInitialized(loggerContext); diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/SpringPropertySource.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/SpringEnvironmentPropertySource.java similarity index 72% rename from spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/SpringPropertySource.java rename to spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/SpringEnvironmentPropertySource.java index ea366d993f..8b8671e536 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/SpringPropertySource.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/SpringEnvironmentPropertySource.java @@ -19,46 +19,40 @@ package org.springframework.boot.logging.log4j2; import org.apache.logging.log4j.util.PropertySource; import org.springframework.core.env.Environment; +import org.springframework.util.Assert; /** * Returns properties from Spring. * * @author Ralph Goers - * @since 3.0.0 */ -public class SpringPropertySource implements PropertySource { - - private static final int DEFAULT_PRIORITY = -100; - - private final Environment environment; - - public SpringPropertySource(Environment environment) { - this.environment = environment; - } +class SpringEnvironmentPropertySource implements PropertySource { /** * System properties take precedence followed by properties in Log4j properties files. - * @return this PropertySource's priority. */ + private static final int PRIORITY = -100; + + private final Environment environment; + + SpringEnvironmentPropertySource(Environment environment) { + Assert.notNull(environment, "Environment must not be null"); + this.environment = environment; + } + @Override public int getPriority() { - return DEFAULT_PRIORITY; + return PRIORITY; } @Override public String getProperty(String key) { - if (this.environment != null) { - return this.environment.getProperty(key); - } - return null; + return this.environment.getProperty(key); } @Override public boolean containsProperty(String key) { - if (this.environment != null) { - return this.environment.containsProperty(key); - } - return false; + return this.environment.containsProperty(key); } } 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 3155bf13f8..ec12461e83 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 @@ -25,6 +25,7 @@ import java.util.EnumSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.logging.Handler; import java.util.logging.Level; @@ -42,6 +43,7 @@ import org.apache.logging.log4j.core.config.composite.CompositeConfiguration; import org.apache.logging.log4j.core.util.ShutdownCallbackRegistry; import org.apache.logging.log4j.jul.Log4jBridgeHandler; import org.apache.logging.log4j.util.PropertiesUtil; +import org.apache.logging.log4j.util.PropertySource; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -59,6 +61,7 @@ import org.springframework.boot.testsupport.system.CapturedOutput; import org.springframework.boot.testsupport.system.OutputCaptureExtension; import org.springframework.core.env.Environment; import org.springframework.mock.env.MockEnvironment; +import org.springframework.test.util.ReflectionTestUtils; import org.springframework.util.ClassUtils; import org.springframework.util.StringUtils; @@ -101,6 +104,7 @@ class Log4J2LoggingSystemTests extends AbstractLoggingSystemTests { this.configuration = loggerContext.getConfiguration(); this.loggingSystem.cleanUp(); this.logger = LogManager.getLogger(getClass()); + cleanUpPropertySources(); } @AfterEach @@ -109,6 +113,16 @@ class Log4J2LoggingSystemTests extends AbstractLoggingSystemTests { LoggerContext loggerContext = (LoggerContext) LogManager.getContext(false); loggerContext.stop(); loggerContext.start(((Reconfigurable) this.configuration).reconfigure()); + cleanUpPropertySources(); + } + + @SuppressWarnings("unchecked") + private void cleanUpPropertySources() { // https://issues.apache.org/jira/browse/LOG4J2-3618 + PropertiesUtil properties = PropertiesUtil.getProperties(); + Object environment = ReflectionTestUtils.getField(properties, "environment"); + Set sources = (Set) ReflectionTestUtils.getField(environment, "sources"); + sources.removeIf((candidate) -> candidate instanceof SpringEnvironmentPropertySource + || candidate instanceof SpringBootPropertySource); } @Test @@ -448,6 +462,16 @@ class Log4J2LoggingSystemTests extends AbstractLoggingSystemTests { assertThat(environment).isSameAs(this.environment); } + @Test + void initializeAddsSpringEnvironmentPropertySource() { + PropertiesUtil properties = PropertiesUtil.getProperties(); + this.environment.setProperty("spring", "boot"); + this.loggingSystem.beforeInitialize(); + this.loggingSystem.initialize(this.initializationContext, null, null); + properties = PropertiesUtil.getProperties(); + assertThat(properties.getStringProperty("spring")).isEqualTo("boot"); + } + private String getRelativeClasspathLocation(String fileName) { String defaultPath = ClassUtils.getPackageName(getClass()); defaultPath = defaultPath.replace('.', '/'); diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/log4j2/SpringEnvironmentPropertySourceTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/log4j2/SpringEnvironmentPropertySourceTests.java new file mode 100644 index 0000000000..9a7162b250 --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/log4j2/SpringEnvironmentPropertySourceTests.java @@ -0,0 +1,83 @@ +/* + * Copyright 2012-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.logging.log4j2; + +import java.util.Properties; + +import org.apache.logging.log4j.util.PropertiesPropertySource; +import org.apache.logging.log4j.util.SystemPropertiesPropertySource; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.mock.env.MockEnvironment; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * Tests for {@link SpringEnvironmentPropertySource}. + * + * @author Phillip Webb + */ +class SpringEnvironmentPropertySourceTests { + + private MockEnvironment environment; + + private SpringEnvironmentPropertySource propertySource; + + @BeforeEach + void setup() { + this.environment = new MockEnvironment(); + this.environment.setProperty("spring", "boot"); + this.propertySource = new SpringEnvironmentPropertySource(this.environment); + } + + @Test + void createWhenEnvironmentIsNullThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> new SpringEnvironmentPropertySource(null)) + .withMessage("Environment must not be null"); + } + + @Test + void getPriorityIsOrderedCorrectly() { + int priority = this.propertySource.getPriority(); + assertThat(priority).isEqualTo(-100); + assertThat(priority).isLessThan(new SystemPropertiesPropertySource().getPriority()); + assertThat(priority).isLessThan(new PropertiesPropertySource(new Properties()).getPriority()); + } + + @Test + void getPropertyWhenInEnvironmentReturnsValue() { + assertThat(this.propertySource.getProperty("spring")).isEqualTo("boot"); + } + + @Test + void getPropertyWhenNotInEnvironmentReturnsNull() { + assertThat(this.propertySource.getProperty("nope")).isNull(); + } + + @Test + void containsPropertyWhenInEnvironmentReturnsTrue() { + assertThat(this.propertySource.containsProperty("spring")).isTrue(); + } + + @Test + void containsPropertyWhenNotInEnvironmentReturnsFalse() { + assertThat(this.propertySource.containsProperty("nope")).isFalse(); + } + +} From 27ed30fdbf076131ec1bd807e8744dcc65fe78e6 Mon Sep 17 00:00:00 2001 From: Ralph Goers Date: Wed, 12 Oct 2022 17:25:01 -0700 Subject: [PATCH 09/13] Support profile specific Log4j2 configuration Add a `SpringProfileArbiter` Log4j2 plugin which allows Log4j2 configuration to be included or skipped based on the active Spring `Environment` profiles. See gh-32734 --- .../logging/log4j2/SpringProfileArbiter.java | 138 ++++++++++++++++++ 1 file changed, 138 insertions(+) create mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/SpringProfileArbiter.java diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/SpringProfileArbiter.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/SpringProfileArbiter.java new file mode 100644 index 0000000000..bf5e7d1886 --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/SpringProfileArbiter.java @@ -0,0 +1,138 @@ +/* + * Copyright 2012-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.logging.log4j2; + +import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.core.LoggerContext; +import org.apache.logging.log4j.core.config.Configuration; +import org.apache.logging.log4j.core.config.Node; +import org.apache.logging.log4j.core.config.arbiters.Arbiter; +import org.apache.logging.log4j.core.config.plugins.Plugin; +import org.apache.logging.log4j.core.config.plugins.PluginBuilderAttribute; +import org.apache.logging.log4j.core.config.plugins.PluginBuilderFactory; +import org.apache.logging.log4j.core.config.plugins.PluginConfiguration; +import org.apache.logging.log4j.core.config.plugins.PluginLoggerContext; +import org.apache.logging.log4j.status.StatusLogger; + +import org.springframework.core.env.Environment; +import org.springframework.core.env.Profiles; +import org.springframework.util.StringUtils; + +/** + * An Arbiter that uses the active Spring profile to determine if configuration should be + * included. + * + * @author Ralph Goers + * @since 3.0.0 + */ +@Plugin(name = "SpringProfile", category = Node.CATEGORY, elementType = Arbiter.ELEMENT_TYPE, deferChildren = true, + printObject = true) +public final class SpringProfileArbiter implements Arbiter { + + private final String[] profileNames; + + private final Environment environment; + + private SpringProfileArbiter(final String[] profiles, Environment environment) { + this.profileNames = profiles; + this.environment = environment; + } + + @Override + public boolean isCondition() { + if (this.environment == null) { + return false; + } + + if (this.profileNames.length == 0) { + return false; + } + return this.environment.acceptsProfiles(Profiles.of(this.profileNames)); + } + + @PluginBuilderFactory + public static Builder newBuilder() { + return new Builder(); + } + + /** + * Standard Builder to create the Arbiter. + */ + public static class Builder implements org.apache.logging.log4j.core.util.Builder { + + private static final Logger LOGGER = StatusLogger.getLogger(); + + /** + * Attribute name identifier. + */ + public static final String ATTR_NAME = "name"; + + @PluginBuilderAttribute(ATTR_NAME) + private String name; + + @PluginConfiguration + private Configuration configuration; + + @PluginLoggerContext + private LoggerContext loggerContext; + + /** + * Sets the Profile Name or Names. + * @param name the profile name(s). + * @return this + */ + public Builder setName(final String name) { + this.name = name; + return asBuilder(); + } + + public Builder setConfiguration(final Configuration configuration) { + this.configuration = configuration; + return asBuilder(); + } + + public Builder setLoggerContext(final LoggerContext loggerContext) { + this.loggerContext = loggerContext; + return asBuilder(); + } + + private SpringProfileArbiter.Builder asBuilder() { + return this; + } + + public SpringProfileArbiter build() { + String[] profileNames = StringUtils.trimArrayElements(StringUtils + .commaDelimitedListToStringArray(this.configuration.getStrSubstitutor().replace(this.name))); + Environment environment = null; + if (this.loggerContext != null) { + environment = (Environment) this.loggerContext.getObject(Log4J2LoggingSystem.ENVIRONMENT_KEY); + if (environment == null) { + LOGGER.warn("Cannot create Arbiter, no Spring Environment provided"); + return null; + } + + return new SpringProfileArbiter(profileNames, environment); + } + else { + LOGGER.warn("Cannot create Arbiter, LoggerContext is not available"); + } + return null; + } + + } + +} From 5a7964af2b6d82058853cfbbd5f2798c6838de9a Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Wed, 12 Oct 2022 21:29:16 -0700 Subject: [PATCH 10/13] Polish 'Support profile specific Log4j2 configuration' See gh-32734 --- .../logging/log4j2/SpringProfileArbiter.java | 80 +++------ .../log4j2/Log4J2LoggingSystemTests.java | 25 --- .../log4j2/SpringProfileArbiterTests.java | 162 ++++++++++++++++++ .../log4j2/TestLog4J2LoggingSystem.java | 47 +++++ .../logging/log4j2/multi-profile-names.xml | 8 + .../logging/log4j2/production-profile.xml | 8 + .../logging/log4j2/profile-expression.xml | 8 + 7 files changed, 258 insertions(+), 80 deletions(-) create mode 100644 spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/log4j2/SpringProfileArbiterTests.java create mode 100644 spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/log4j2/TestLog4J2LoggingSystem.java create mode 100644 spring-boot-project/spring-boot/src/test/resources/org/springframework/boot/logging/log4j2/multi-profile-names.xml create mode 100644 spring-boot-project/spring-boot/src/test/resources/org/springframework/boot/logging/log4j2/production-profile.xml create mode 100644 spring-boot-project/spring-boot/src/test/resources/org/springframework/boot/logging/log4j2/profile-expression.xml diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/SpringProfileArbiter.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/SpringProfileArbiter.java index bf5e7d1886..f002813dce 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/SpringProfileArbiter.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/SpringProfileArbiter.java @@ -37,51 +37,38 @@ import org.springframework.util.StringUtils; * included. * * @author Ralph Goers - * @since 3.0.0 */ @Plugin(name = "SpringProfile", category = Node.CATEGORY, elementType = Arbiter.ELEMENT_TYPE, deferChildren = true, printObject = true) -public final class SpringProfileArbiter implements Arbiter { - - private final String[] profileNames; +final class SpringProfileArbiter implements Arbiter { private final Environment environment; - private SpringProfileArbiter(final String[] profiles, Environment environment) { - this.profileNames = profiles; + private final Profiles profiles; + + private SpringProfileArbiter(Environment environment, String[] profiles) { this.environment = environment; + this.profiles = Profiles.of(profiles); } @Override public boolean isCondition() { - if (this.environment == null) { - return false; - } - - if (this.profileNames.length == 0) { - return false; - } - return this.environment.acceptsProfiles(Profiles.of(this.profileNames)); + return (this.environment != null) ? this.environment.acceptsProfiles(this.profiles) : false; } @PluginBuilderFactory - public static Builder newBuilder() { + static Builder newBuilder() { return new Builder(); } /** * Standard Builder to create the Arbiter. */ - public static class Builder implements org.apache.logging.log4j.core.util.Builder { + public static final class Builder implements org.apache.logging.log4j.core.util.Builder { - private static final Logger LOGGER = StatusLogger.getLogger(); + private static final Logger statusLogger = StatusLogger.getLogger(); - /** - * Attribute name identifier. - */ - public static final String ATTR_NAME = "name"; - - @PluginBuilderAttribute(ATTR_NAME) + @PluginBuilderAttribute private String name; @PluginConfiguration @@ -90,47 +77,30 @@ public final class SpringProfileArbiter implements Arbiter { @PluginLoggerContext private LoggerContext loggerContext; + private Builder() { + } + /** - * Sets the Profile Name or Names. - * @param name the profile name(s). + * Sets the profile name or expression. + * @param name the profile name or expression * @return this + * @see Profiles#of(String...) */ - public Builder setName(final String name) { + public Builder setName(String name) { this.name = name; - return asBuilder(); - } - - public Builder setConfiguration(final Configuration configuration) { - this.configuration = configuration; - return asBuilder(); - } - - public Builder setLoggerContext(final LoggerContext loggerContext) { - this.loggerContext = loggerContext; - return asBuilder(); - } - - private SpringProfileArbiter.Builder asBuilder() { return this; } + @Override public SpringProfileArbiter build() { - String[] profileNames = StringUtils.trimArrayElements(StringUtils - .commaDelimitedListToStringArray(this.configuration.getStrSubstitutor().replace(this.name))); - Environment environment = null; - if (this.loggerContext != null) { - environment = (Environment) this.loggerContext.getObject(Log4J2LoggingSystem.ENVIRONMENT_KEY); - if (environment == null) { - LOGGER.warn("Cannot create Arbiter, no Spring Environment provided"); - return null; - } - - return new SpringProfileArbiter(profileNames, environment); + Environment environment = Log4J2LoggingSystem.getEnvironment(this.loggerContext); + if (environment == null) { + statusLogger.warn("Cannot create Arbiter, no Spring Environment available"); + return null; } - else { - LOGGER.warn("Cannot create Arbiter, LoggerContext is not available"); - } - return null; + String name = this.configuration.getStrSubstitutor().replace(this.name); + String[] profiles = StringUtils.trimArrayElements(StringUtils.commaDelimitedListToStringArray(name)); + return new SpringProfileArbiter(environment, profiles); } } 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 ec12461e83..8d0955e54f 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 @@ -19,8 +19,6 @@ package org.springframework.boot.logging.log4j2; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.io.File; -import java.util.ArrayList; -import java.util.Collections; import java.util.EnumSet; import java.util.LinkedHashMap; import java.util.List; @@ -480,29 +478,6 @@ class Log4J2LoggingSystemTests extends AbstractLoggingSystemTests { return defaultPath; } - static class TestLog4J2LoggingSystem extends Log4J2LoggingSystem { - - private List availableClasses = new ArrayList<>(); - - TestLog4J2LoggingSystem() { - super(TestLog4J2LoggingSystem.class.getClassLoader()); - } - - Configuration getConfiguration() { - return ((org.apache.logging.log4j.core.LoggerContext) LogManager.getContext(false)).getConfiguration(); - } - - @Override - protected boolean isClassAvailable(String className) { - return this.availableClasses.contains(className); - } - - private void availableClasses(String... classNames) { - Collections.addAll(this.availableClasses, classNames); - } - - } - /** * Used for testing that loggers in nested classes are returned by * {@link Log4J2LoggingSystem#getLoggerConfigurations()} . diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/log4j2/SpringProfileArbiterTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/log4j2/SpringProfileArbiterTests.java new file mode 100644 index 0000000000..ab1f6e746a --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/log4j2/SpringProfileArbiterTests.java @@ -0,0 +1,162 @@ +/* + * Copyright 2012-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.logging.log4j2; + +import java.util.Set; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.core.LoggerContext; +import org.apache.logging.log4j.core.config.Configuration; +import org.apache.logging.log4j.core.config.Reconfigurable; +import org.apache.logging.log4j.util.PropertiesUtil; +import org.apache.logging.log4j.util.PropertySource; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.boot.logging.LoggingInitializationContext; +import org.springframework.boot.testsupport.classpath.ClassPathExclusions; +import org.springframework.boot.testsupport.logging.ConfigureClasspathToPreferLog4j2; +import org.springframework.boot.testsupport.system.CapturedOutput; +import org.springframework.boot.testsupport.system.OutputCaptureExtension; +import org.springframework.mock.env.MockEnvironment; +import org.springframework.test.util.ReflectionTestUtils; +import org.springframework.util.ClassUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link SpringProfileArbiter}. + * + * @author Phillip Webb + */ +@ExtendWith(OutputCaptureExtension.class) +@ClassPathExclusions("logback-*.jar") +@ConfigureClasspathToPreferLog4j2 +class SpringProfileArbiterTests { + + private CapturedOutput output; + + 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; + + @BeforeEach + void setup(CapturedOutput output) { + this.output = output; + LoggerContext loggerContext = (LoggerContext) LogManager.getContext(false); + this.configuration = loggerContext.getConfiguration(); + this.loggingSystem.cleanUp(); + this.logger = LogManager.getLogger(getClass()); + cleanUpPropertySources(); + } + + @AfterEach + void cleanUp() { + this.loggingSystem.cleanUp(); + LoggerContext loggerContext = (LoggerContext) LogManager.getContext(false); + loggerContext.stop(); + loggerContext.start(((Reconfigurable) this.configuration).reconfigure()); + cleanUpPropertySources(); + } + + @SuppressWarnings("unchecked") + private void cleanUpPropertySources() { // https://issues.apache.org/jira/browse/LOG4J2-3618 + PropertiesUtil properties = PropertiesUtil.getProperties(); + Object environment = ReflectionTestUtils.getField(properties, "environment"); + Set sources = (Set) ReflectionTestUtils.getField(environment, "sources"); + sources.removeIf((candidate) -> candidate instanceof SpringEnvironmentPropertySource + || candidate instanceof SpringBootPropertySource); + } + + @Test + void profileActive() { + this.environment.setActiveProfiles("production"); + initialize("production-profile.xml"); + this.logger.trace("Hello"); + assertThat(this.output).contains("Hello"); + } + + @Test + void multipleNamesFirstProfileActive() { + this.environment.setActiveProfiles("production"); + initialize("multi-profile-names.xml"); + this.logger.trace("Hello"); + assertThat(this.output).contains("Hello"); + } + + @Test + void multipleNamesSecondProfileActive() { + this.environment.setActiveProfiles("test"); + initialize("multi-profile-names.xml"); + this.logger.trace("Hello"); + assertThat(this.output).contains("Hello"); + } + + @Test + void profileNotActive() { + initialize("production-profile.xml"); + this.logger.trace("Hello"); + assertThat(this.output).doesNotContain("Hello"); + } + + @Test + void profileExpressionMatchFirst() { + this.environment.setActiveProfiles("production"); + initialize("profile-expression.xml"); + this.logger.trace("Hello"); + assertThat(this.output).contains("Hello"); + } + + @Test + void profileExpressionMatchSecond() { + this.environment.setActiveProfiles("test"); + initialize("profile-expression.xml"); + this.logger.trace("Hello"); + assertThat(this.output).contains("Hello"); + } + + @Test + void profileExpressionNoMatch() { + this.environment.setActiveProfiles("development"); + initialize("profile-expression.xml"); + this.logger.trace("Hello"); + assertThat(this.output).doesNotContain("Hello"); + } + + private void initialize(String config) { + this.environment.setProperty("logging.log4j2.config.override", getPackageResource(config)); + this.loggingSystem.initialize(this.initializationContext, null, null); + } + + private String getPackageResource(String fileName) { + String path = ClassUtils.getPackageName(getClass()); + path = path.replace('.', '/'); + path = path + "/" + fileName; + return "src/test/resources/" + path; + } + +} diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/log4j2/TestLog4J2LoggingSystem.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/log4j2/TestLog4J2LoggingSystem.java new file mode 100644 index 0000000000..43cd1a6760 --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/logging/log4j2/TestLog4J2LoggingSystem.java @@ -0,0 +1,47 @@ +/* + * Copyright 2012-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.logging.log4j2; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.core.config.Configuration; + +class TestLog4J2LoggingSystem extends Log4J2LoggingSystem { + + private List availableClasses = new ArrayList<>(); + + TestLog4J2LoggingSystem() { + super(TestLog4J2LoggingSystem.class.getClassLoader()); + } + + Configuration getConfiguration() { + return ((org.apache.logging.log4j.core.LoggerContext) LogManager.getContext(false)).getConfiguration(); + } + + @Override + protected boolean isClassAvailable(String className) { + return this.availableClasses.contains(className); + } + + void availableClasses(String... classNames) { + Collections.addAll(this.availableClasses, classNames); + } + +} diff --git a/spring-boot-project/spring-boot/src/test/resources/org/springframework/boot/logging/log4j2/multi-profile-names.xml b/spring-boot-project/spring-boot/src/test/resources/org/springframework/boot/logging/log4j2/multi-profile-names.xml new file mode 100644 index 0000000000..535f4a7ae2 --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/resources/org/springframework/boot/logging/log4j2/multi-profile-names.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/spring-boot-project/spring-boot/src/test/resources/org/springframework/boot/logging/log4j2/production-profile.xml b/spring-boot-project/spring-boot/src/test/resources/org/springframework/boot/logging/log4j2/production-profile.xml new file mode 100644 index 0000000000..f0c3309f87 --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/resources/org/springframework/boot/logging/log4j2/production-profile.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/spring-boot-project/spring-boot/src/test/resources/org/springframework/boot/logging/log4j2/profile-expression.xml b/spring-boot-project/spring-boot/src/test/resources/org/springframework/boot/logging/log4j2/profile-expression.xml new file mode 100644 index 0000000000..25d0705981 --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/resources/org/springframework/boot/logging/log4j2/profile-expression.xml @@ -0,0 +1,8 @@ + + + + + + + + From 52867851271c42d0cf29b8afcacf41b72540f242 Mon Sep 17 00:00:00 2001 From: Ralph Goers Date: Wed, 12 Oct 2022 11:17:42 -0700 Subject: [PATCH 11/13] Resolve URLs using Log4j2 mechanisms Update `Log4J2LoggingSystem` to that non file URLs are resolved using Log4j2's `UrlConnectionFactory` mechanism rather than directly. See gh-32735 --- .../logging/log4j2/Log4J2LoggingSystem.java | 59 +++++++++++++++---- 1 file changed, 46 insertions(+), 13 deletions(-) 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 38c19b4689..05cbb45aee 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,9 +16,12 @@ package org.springframework.boot.logging.log4j2; +import java.io.File; +import java.io.FileNotFoundException; import java.io.IOException; -import java.io.InputStream; +import java.net.URISyntaxException; import java.net.URL; +import java.net.URLConnection; import java.util.ArrayList; import java.util.Collections; import java.util.LinkedHashMap; @@ -43,9 +46,15 @@ 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.net.UrlConnectionFactory; +import org.apache.logging.log4j.core.net.ssl.SslConfiguration; +import org.apache.logging.log4j.core.net.ssl.SslConfigurationFactory; +import org.apache.logging.log4j.core.util.AuthorizationProvider; +import org.apache.logging.log4j.core.util.FileUtils; import org.apache.logging.log4j.core.util.NameUtil; import org.apache.logging.log4j.jul.Log4jBridgeHandler; import org.apache.logging.log4j.message.Message; +import org.apache.logging.log4j.status.StatusLogger; import org.apache.logging.log4j.util.PropertiesUtil; import org.springframework.boot.context.properties.bind.BindResult; @@ -82,6 +91,8 @@ public class Log4J2LoggingSystem extends AbstractLoggingSystem { private static final String FILE_PROTOCOL = "file"; + private static final String HTTPS = "https"; + 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"; @@ -89,6 +100,8 @@ public class Log4J2LoggingSystem extends AbstractLoggingSystem { static final String ENVIRONMENT_KEY = Conventions.getQualifiedAttributeName(Log4J2LoggingSystem.class, "environment"); + private static org.apache.logging.log4j.Logger LOGGER = StatusLogger.getLogger(); + private static final LogLevels LEVELS = new LogLevels<>(); static { @@ -280,11 +293,20 @@ public class Log4J2LoggingSystem extends AbstractLoggingSystem { try { List configurations = new ArrayList<>(); LoggerContext context = getLoggerContext(); - configurations.add(load(location, context)); - for (String override : overrides) { - configurations.add(load(override, context)); + Configuration configuration = load(location, context); + if (configuration != null) { + configurations.add(load(location, context)); } - Configuration configuration = (configurations.size() > 1) ? createComposite(configurations) + else { + throw new FileNotFoundException("Cannot locate file: " + location); + } + for (String override : overrides) { + configuration = load(override, context); + if (configuration != null) { + configurations.add(configuration); + } + } + configuration = (configurations.size() > 1) ? createComposite(configurations) : configurations.iterator().next(); context.start(configuration); } @@ -293,18 +315,29 @@ public class Log4J2LoggingSystem extends AbstractLoggingSystem { } } - private Configuration load(String location, LoggerContext context) throws IOException { + private Configuration load(String location, LoggerContext context) throws IOException, URISyntaxException { URL url = ResourceUtils.getURL(location); ConfigurationSource source = getConfigurationSource(url); - return ConfigurationFactory.getInstance().getConfiguration(context, source); + return (source != null) ? ConfigurationFactory.getInstance().getConfiguration(context, source) : null; } - private ConfigurationSource getConfigurationSource(URL url) throws IOException { - InputStream stream = url.openStream(); - if (FILE_PROTOCOL.equals(url.getProtocol())) { - return new ConfigurationSource(stream, ResourceUtils.getFile(url)); + private ConfigurationSource getConfigurationSource(URL url) throws IOException, URISyntaxException { + AuthorizationProvider provider = ConfigurationFactory.authorizationProvider(PropertiesUtil.getProperties()); + SslConfiguration sslConfiguration = url.getProtocol().equals(HTTPS) + ? SslConfigurationFactory.getSslConfiguration() : null; + URLConnection urlConnection = UrlConnectionFactory.createConnection(url, 0, sslConfiguration, provider); + + File file = FileUtils.fileFromUri(url.toURI()); + try { + if (file != null) { + return new ConfigurationSource(urlConnection.getInputStream(), FileUtils.fileFromUri(url.toURI())); + } + return new ConfigurationSource(urlConnection.getInputStream(), url, urlConnection.getLastModified()); + } + catch (FileNotFoundException ex) { + LOGGER.info("Unable to locate file {}, ignoring.", url.toString()); + return null; } - return new ConfigurationSource(stream, url); } private CompositeConfiguration createComposite(List configurations) { @@ -332,7 +365,7 @@ public class Log4J2LoggingSystem extends AbstractLoggingSystem { try { configurations.add((AbstractConfiguration) load(override, context)); } - catch (IOException ex) { + catch (Exception ex) { throw new RuntimeException("Failed to load overriding configuration from '" + override + "'", ex); } } From cec090c32e2c69e68160d2c2a6da093f33d06134 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Wed, 12 Oct 2022 22:04:11 -0700 Subject: [PATCH 12/13] Polish 'Resolve URLs using Log4J2 mechanisms' See gh-32735 --- .../logging/log4j2/Log4J2LoggingSystem.java | 56 ++++++------------- .../log4j2/Log4J2LoggingSystemTests.java | 10 ++++ 2 files changed, 26 insertions(+), 40 deletions(-) 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 05cbb45aee..2dc0600417 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,10 +16,7 @@ package org.springframework.boot.logging.log4j2; -import java.io.File; -import java.io.FileNotFoundException; import java.io.IOException; -import java.net.URISyntaxException; import java.net.URL; import java.net.URLConnection; import java.util.ArrayList; @@ -50,11 +47,9 @@ import org.apache.logging.log4j.core.net.UrlConnectionFactory; import org.apache.logging.log4j.core.net.ssl.SslConfiguration; import org.apache.logging.log4j.core.net.ssl.SslConfigurationFactory; import org.apache.logging.log4j.core.util.AuthorizationProvider; -import org.apache.logging.log4j.core.util.FileUtils; import org.apache.logging.log4j.core.util.NameUtil; import org.apache.logging.log4j.jul.Log4jBridgeHandler; import org.apache.logging.log4j.message.Message; -import org.apache.logging.log4j.status.StatusLogger; import org.apache.logging.log4j.util.PropertiesUtil; import org.springframework.boot.context.properties.bind.BindResult; @@ -91,8 +86,6 @@ public class Log4J2LoggingSystem extends AbstractLoggingSystem { private static final String FILE_PROTOCOL = "file"; - private static final String HTTPS = "https"; - 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"; @@ -100,8 +93,6 @@ public class Log4J2LoggingSystem extends AbstractLoggingSystem { static final String ENVIRONMENT_KEY = Conventions.getQualifiedAttributeName(Log4J2LoggingSystem.class, "environment"); - private static org.apache.logging.log4j.Logger LOGGER = StatusLogger.getLogger(); - private static final LogLevels LEVELS = new LogLevels<>(); static { @@ -293,20 +284,11 @@ public class Log4J2LoggingSystem extends AbstractLoggingSystem { try { List configurations = new ArrayList<>(); LoggerContext context = getLoggerContext(); - Configuration configuration = load(location, context); - if (configuration != null) { - configurations.add(load(location, context)); - } - else { - throw new FileNotFoundException("Cannot locate file: " + location); - } + configurations.add(load(location, context)); for (String override : overrides) { - configuration = load(override, context); - if (configuration != null) { - configurations.add(configuration); - } + configurations.add(load(override, context)); } - configuration = (configurations.size() > 1) ? createComposite(configurations) + Configuration configuration = (configurations.size() > 1) ? createComposite(configurations) : configurations.iterator().next(); context.start(configuration); } @@ -315,29 +297,23 @@ public class Log4J2LoggingSystem extends AbstractLoggingSystem { } } - private Configuration load(String location, LoggerContext context) throws IOException, URISyntaxException { + private Configuration load(String location, LoggerContext context) throws IOException { URL url = ResourceUtils.getURL(location); ConfigurationSource source = getConfigurationSource(url); - return (source != null) ? ConfigurationFactory.getInstance().getConfiguration(context, source) : null; + return ConfigurationFactory.getInstance().getConfiguration(context, source); } - private ConfigurationSource getConfigurationSource(URL url) throws IOException, URISyntaxException { - AuthorizationProvider provider = ConfigurationFactory.authorizationProvider(PropertiesUtil.getProperties()); - SslConfiguration sslConfiguration = url.getProtocol().equals(HTTPS) + private ConfigurationSource getConfigurationSource(URL url) throws IOException { + if (FILE_PROTOCOL.equals(url.getProtocol())) { + return new ConfigurationSource(url.openStream(), ResourceUtils.getFile(url)); + } + AuthorizationProvider authorizationProvider = ConfigurationFactory + .authorizationProvider(PropertiesUtil.getProperties()); + SslConfiguration sslConfiguration = url.getProtocol().equals("https") ? SslConfigurationFactory.getSslConfiguration() : null; - URLConnection urlConnection = UrlConnectionFactory.createConnection(url, 0, sslConfiguration, provider); - - File file = FileUtils.fileFromUri(url.toURI()); - try { - if (file != null) { - return new ConfigurationSource(urlConnection.getInputStream(), FileUtils.fileFromUri(url.toURI())); - } - return new ConfigurationSource(urlConnection.getInputStream(), url, urlConnection.getLastModified()); - } - catch (FileNotFoundException ex) { - LOGGER.info("Unable to locate file {}, ignoring.", url.toString()); - return null; - } + URLConnection connection = UrlConnectionFactory.createConnection(url, 0, sslConfiguration, + authorizationProvider); + return new ConfigurationSource(connection.getInputStream(), url, connection.getLastModified()); } private CompositeConfiguration createComposite(List configurations) { @@ -365,7 +341,7 @@ public class Log4J2LoggingSystem extends AbstractLoggingSystem { try { configurations.add((AbstractConfiguration) load(override, context)); } - catch (Exception ex) { + catch (IOException ex) { throw new RuntimeException("Failed to load overriding configuration from '" + override + "'", ex); } } 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 8d0955e54f..e8f5fbe5ea 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 @@ -19,6 +19,7 @@ package org.springframework.boot.logging.log4j2; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.io.File; +import java.net.ProtocolException; import java.util.EnumSet; import java.util.LinkedHashMap; import java.util.List; @@ -470,6 +471,15 @@ class Log4J2LoggingSystemTests extends AbstractLoggingSystemTests { assertThat(properties.getStringProperty("spring")).isEqualTo("boot"); } + @Test + void nonFileUrlsAreResolvedUsingLog4J2UrlConnectionFactory() { + this.loggingSystem.beforeInitialize(); + assertThatIllegalStateException() + .isThrownBy(() -> this.loggingSystem.initialize(this.initializationContext, + "http://localhost:8080/shouldnotwork", null)) + .havingCause().isInstanceOf(ProtocolException.class).withMessageContaining("http has not been enabled"); + } + private String getRelativeClasspathLocation(String fileName) { String defaultPath = ClassUtils.getPackageName(getClass()); defaultPath = defaultPath.replace('.', '/'); From 029aab6b58cc5469835dc56ca776eeef519f5384 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Fri, 14 Oct 2022 09:36:41 -0700 Subject: [PATCH 13/13] Document Log4j2 extensions See gh-32578 --- .../src/docs/asciidoc/features/logging.adoc | 79 ++++++++++++++++++- 1 file changed, 76 insertions(+), 3 deletions(-) diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/logging.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/logging.adoc index 6876bc3216..0d48f2e534 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/logging.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/logging.adoc @@ -1,7 +1,7 @@ [[features.logging]] == Logging Spring Boot uses https://commons.apache.org/logging[Commons Logging] for all internal logging but leaves the underlying log implementation open. -Default configurations are provided for {java-api}/java/util/logging/package-summary.html[Java Util Logging], https://logging.apache.org/log4j/2.x/[Log4J2], and https://logback.qos.ch/[Logback]. +Default configurations are provided for {java-api}/java/util/logging/package-summary.html[Java Util Logging], https://logging.apache.org/log4j/2.x/[Log4j2], and https://logback.qos.ch/[Logback]. In each case, loggers are pre-configured to use console output with optional file output also available. By default, if you use the "`Starters`", Logback is used for logging. @@ -158,7 +158,7 @@ As a result, specific configuration keys (such as `logback.configurationFile` fo [[features.logging.file-rotation]] === File Rotation If you are using the Logback, it is possible to fine-tune log rotation settings using your `application.properties` or `application.yaml` file. -For all other logging system, you will need to configure rotation settings directly yourself (for example, if you use Log4J2 then you could add a `log4j2.xml` or `log4j2-spring.xml` file). +For all other logging system, you will need to configure rotation settings directly yourself (for example, if you use Log4j2 then you could add a `log4j2.xml` or `log4j2-spring.xml` file). The following rotation policy properties are supported: @@ -433,7 +433,7 @@ Profile sections are supported anywhere within the `` element. Use the `name` attribute to specify which profile accepts the configuration. The `` tag can contain a profile name (for example `staging`) or a profile expression. A profile expression allows for more complicated profile logic to be expressed, for example `production & (eu-central | eu-west)`. -Check the {spring-framework-docs}/core.html#beans-definition-profiles-java[reference guide] for more details. +Check the {spring-framework-docs}/core.html#beans-definition-profiles-java[Spring Framework reference guide] for more details. The following listing shows three sample profiles: [source,xml,subs="verbatim",indent=0] @@ -475,3 +475,76 @@ The following example shows how to expose properties for use within Logback: NOTE: The `source` must be specified in kebab case (such as `my.property-name`). However, properties can be added to the `Environment` by using the relaxed rules. + + + +[[features.logging.log4j2-extensions]] +=== Log4j2 Extensions +Spring Boot includes a number of extensions to Log4j2 that can help with advanced configuration. +You can use these extensions in any `log4j2-spring.xml` configuration file. + +NOTE: Because the standard `log4j2.xml` configuration file is loaded too early, you cannot use extensions in it. +You need to either use `log4j2-spring.xml` or define a configprop:logging.config[] property. + +NOTE: The extensions supersede the https://logging.apache.org/log4j/2.x/log4j-spring-boot/index.html[Spring Boot support] provided by Log4J. +You should make sure not include the `org.apache.logging.log4j:log4j-spring-boot` module in your build. + + + +[[features.logging.log4j2-extensions.profile-specific]] +==== Profile-specific Configuration +The `` tag lets you optionally include or exclude sections of configuration based on the active Spring profiles. +Profile sections are supported anywhere within the `` element. +Use the `name` attribute to specify which profile accepts the configuration. +The `` tag can contain a profile name (for example `staging`) or a profile expression. +A profile expression allows for more complicated profile logic to be expressed, for example `production & (eu-central | eu-west)`. +Check the {spring-framework-docs}/core.html#beans-definition-profiles-java[Spring Framework reference guide] for more details. +The following listing shows three sample profiles: + +[source,xml,subs="verbatim",indent=0] +---- + + + + + + + + + + + +---- + + + +[[features.logging.log4j2-extensions.environment-properties-lookup]] +==== Environment Properties Lookup +If you want to refer to properties from your Spring `Environment` within your Log4j2 configuration you can use `spring:` prefixed https://logging.apache.org/log4j/2.x/manual/lookups.html[lookups]. +Doing so can be useful if you want to access values from your `application.properties` file in your Log4j2 configuration. + +The following example shows how to set a Log4j2 property named `applicationName` that reads `spring.application.name` from the Spring `Environment`: + +[source,xml,subs="verbatim",indent=0] +---- + + ${spring:spring.application.name} + +---- + +NOTE: The lookup key should be specified in kebab case (such as `my.property-name`). + + + +[[features.logging.log4j2-extensions.environment-peroperty-source]] +==== Log4j2 System Properties +Log4j2 supports a number of https://logging.apache.org/log4j/2.x/manual/configuration.html#SystemProperties[System Properties] that can be used configure various items. +For example, the `log4j2.skipJansi` system property can be used to configure if the `ConsoleAppender` will try to use a https://github.com/fusesource/jansi[Jansi] output stream on Windows. + +All system properties that are loaded after the Log4J initialization can be obtained from the Spring `Environment`. +For example, you could add `log4j2.skipJansi=false` to your `application.properties` file to have the `ConsoleAppender` use a Jansi on Windows. + +NOTE: The Spring `Environment` is only considered when system properties and OS environment variables do not contain the value being loaded. + +WARNING: System properties that are loaded during early Log4j2 initialization cannot reference the Spring `Environment`. +For example, the property Log4j2 uses to allow the default Log4j2 implementation to be chosen is used before the Spring Environment is available.