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 c365ffd20e53..f7fd09dfa206 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,14 +16,18 @@ 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; 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; @@ -43,9 +47,16 @@ 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; import org.springframework.boot.context.properties.bind.Bindable; @@ -59,6 +70,7 @@ 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; @@ -78,10 +90,19 @@ 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"; + /** + * Identifies the Spring environment. + */ + public static final String ENVIRONMENT_KEY = "SpringEnvironment"; + + private static org.apache.logging.log4j.Logger LOGGER = StatusLogger.getLogger(); + private static final LogLevels LEVELS = new LogLevels<>(); static { @@ -138,6 +159,11 @@ private String[] getCurrentlySupportedConfigLocations() { 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); } @@ -228,6 +254,9 @@ public void initialize(LoggingInitializationContext initializationContext, Strin if (isAlreadyInitialized(loggerContext)) { return; } + Environment environment = initializationContext.getEnvironment(); + PropertiesUtil.getProperties().addPropertySource(new SpringPropertySource(environment)); + getLoggerContext().putObjectIfAbsent(ENVIRONMENT_KEY, environment); loggerContext.getConfiguration().removeFilter(FILTER); super.initialize(initializationContext, configLocation, logFile); markAsInitialized(loggerContext); @@ -271,11 +300,20 @@ protected void loadConfiguration(String location, LogFile logFile, List try { List configurations = new ArrayList<>(); LoggerContext context = getLoggerContext(); - configurations.add(load(location, context)); + Configuration configuration = load(location, context); + if (configuration != null) { + configurations.add(load(location, context)); + } + else { + throw new FileNotFoundException("Cannot locate file: " + location); + } for (String override : overrides) { - configurations.add(load(override, context)); + configuration = load(override, context); + if (configuration != null) { + configurations.add(configuration); + } } - Configuration configuration = (configurations.size() > 1) ? createComposite(configurations) + configuration = (configurations.size() > 1) ? createComposite(configurations) : configurations.iterator().next(); context.start(configuration); } @@ -284,18 +322,29 @@ protected void loadConfiguration(String location, LogFile logFile, List } } - 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) { @@ -324,7 +373,7 @@ private void reinitializeWithOverrides(List overrides) { try { configurations.add((AbstractConfiguration) load(override, context)); } - catch (IOException ex) { + catch (Exception ex) { throw new RuntimeException("Failed to load overriding configuration from '" + override + "'", ex); } } 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 000000000000..bcae1db7b366 --- /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"); + } + } + +} 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 000000000000..bf5e7d188643 --- /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; + } + + } + +} 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 000000000000..ea366d993fbf --- /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; + } + +}