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. 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..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 @@ -17,13 +17,14 @@ package org.springframework.boot.logging.log4j2; import java.io.IOException; -import java.io.InputStream; import java.net.URL; +import java.net.URLConnection; import java.util.ArrayList; 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; @@ -42,9 +43,14 @@ 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.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; @@ -56,8 +62,10 @@ 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; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.CollectionUtils; @@ -71,6 +79,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 { @@ -81,6 +90,9 @@ public class Log4J2LoggingSystem extends AbstractLoggingSystem { private static final String LOG4J_LOG_MANAGER = "org.apache.logging.log4j.jul.LogManager"; + static final String ENVIRONMENT_KEY = Conventions.getQualifiedAttributeName(Log4J2LoggingSystem.class, + "environment"); + private static final LogLevels LEVELS = new LogLevels<>(); static { @@ -123,32 +135,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"); - 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) { @@ -227,6 +236,11 @@ public class Log4J2LoggingSystem extends AbstractLoggingSystem { if (isAlreadyInitialized(loggerContext)) { return; } + Environment environment = initializationContext.getEnvironment(); + 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); @@ -290,11 +304,16 @@ public class Log4J2LoggingSystem extends AbstractLoggingSystem { } private ConfigurationSource getConfigurationSource(URL url) throws IOException { - InputStream stream = url.openStream(); if (FILE_PROTOCOL.equals(url.getProtocol())) { - return new ConfigurationSource(stream, ResourceUtils.getFile(url)); + return new ConfigurationSource(url.openStream(), ResourceUtils.getFile(url)); } - return new ConfigurationSource(stream, url); + AuthorizationProvider authorizationProvider = ConfigurationFactory + .authorizationProvider(PropertiesUtil.getProperties()); + SslConfiguration sslConfiguration = url.getProtocol().equals("https") + ? SslConfigurationFactory.getSslConfiguration() : null; + URLConnection connection = UrlConnectionFactory.createConnection(url, 0, sslConfiguration, + authorizationProvider); + return new ConfigurationSource(connection.getInputStream(), url, connection.getLastModified()); } private CompositeConfiguration createComposite(List configurations) { @@ -467,6 +486,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/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/SpringEnvironmentPropertySource.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/SpringEnvironmentPropertySource.java new file mode 100644 index 0000000000..8b8671e536 --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/SpringEnvironmentPropertySource.java @@ -0,0 +1,58 @@ +/* + * 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; +import org.springframework.util.Assert; + +/** + * Returns properties from Spring. + * + * @author Ralph Goers + */ +class SpringEnvironmentPropertySource implements PropertySource { + + /** + * System properties take precedence followed by properties in Log4j properties files. + */ + 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 PRIORITY; + } + + @Override + public String getProperty(String key) { + return this.environment.getProperty(key); + } + + @Override + public boolean containsProperty(String key) { + return this.environment.containsProperty(key); + } + +} 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..f002813dce --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/logging/log4j2/SpringProfileArbiter.java @@ -0,0 +1,108 @@ +/* + * 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 + */ +@Plugin(name = "SpringProfile", category = Node.CATEGORY, elementType = Arbiter.ELEMENT_TYPE, deferChildren = true, + printObject = true) +final class SpringProfileArbiter implements Arbiter { + + private final Environment environment; + + private final Profiles profiles; + + private SpringProfileArbiter(Environment environment, String[] profiles) { + this.environment = environment; + this.profiles = Profiles.of(profiles); + } + + @Override + public boolean isCondition() { + return (this.environment != null) ? this.environment.acceptsProfiles(this.profiles) : false; + } + + @PluginBuilderFactory + static Builder newBuilder() { + return new Builder(); + } + + /** + * Standard Builder to create the Arbiter. + */ + public static final class Builder implements org.apache.logging.log4j.core.util.Builder { + + private static final Logger statusLogger = StatusLogger.getLogger(); + + @PluginBuilderAttribute + private String name; + + @PluginConfiguration + private Configuration configuration; + + @PluginLoggerContext + private LoggerContext loggerContext; + + private Builder() { + } + + /** + * Sets the profile name or expression. + * @param name the profile name or expression + * @return this + * @see Profiles#of(String...) + */ + public Builder setName(String name) { + this.name = name; + return this; + } + + @Override + public SpringProfileArbiter build() { + Environment environment = Log4J2LoggingSystem.getEnvironment(this.loggerContext); + if (environment == null) { + statusLogger.warn("Cannot create Arbiter, no Spring Environment 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 a6ff8a1309..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,12 +19,12 @@ 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.net.ProtocolException; 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; @@ -35,12 +35,14 @@ 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; 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; @@ -56,7 +58,9 @@ 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.test.util.ReflectionTestUtils; import org.springframework.util.ClassUtils; import org.springframework.util.StringUtils; @@ -99,6 +103,7 @@ class Log4J2LoggingSystemTests extends AbstractLoggingSystemTests { this.configuration = loggerContext.getConfiguration(); this.loggingSystem.cleanUp(); this.logger = LogManager.getLogger(getClass()); + cleanUpPropertySources(); } @AfterEach @@ -107,6 +112,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 @@ -295,6 +310,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); @@ -425,6 +452,34 @@ 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); + } + + @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"); + } + + @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('.', '/'); @@ -433,29 +488,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/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; + } + +} 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(); + } + +} 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 @@ + + + + + + + +