Skip to content

Improve Log4j Core Configuration File Detection for Version 3 #46409

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@
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;
Expand Down Expand Up @@ -83,6 +82,7 @@
* @author Alexander Heusingfeld
* @author Ben Hale
* @author Ralph Goers
* @author Piotr P. Karwasz
* @since 1.2.0
*/
public class Log4J2LoggingSystem extends AbstractLoggingSystem {
Expand All @@ -93,6 +93,41 @@ public class Log4J2LoggingSystem extends AbstractLoggingSystem {

private static final String LOG4J_LOG_MANAGER = "org.apache.logging.log4j.jul.LogManager";

/**
* JSON tree parser used by Log4j 2 (optional dependency).
*/
private static final String JSON_TREE_PARSER_V2 = "com.fasterxml.jackson.databind.ObjectMapper";

/**
* JSON tree parser embedded in Log4j 3.
*/
private static final String JSON_TREE_PARSER_V3 = "org.apache.logging.log4j.kit.json.JsonReader";

/**
* Configuration factory for properties files (Log4j 2).
*/
private static final String PROPS_CONFIGURATION_FACTORY_V2 = "org.apache.logging.log4j.core.config.properties.PropertiesConfigurationFactory";

/**
* Configuration factory for properties files (Log4j 3, optional dependency).
*/
private static final String PROPS_CONFIGURATION_FACTORY_V3 = "org.apache.logging.log4j.config.properties.JavaPropsConfigurationFactory";

/**
* YAML tree parser used by Log4j 2 (optional dependency).
*/
private static final String YAML_TREE_PARSER_V2 = "com.fasterxml.jackson.dataformat.yaml.YAMLMapper";

/**
* Configuration factory for YAML files (Log4j 2, embedded).
*/
private static final String YAML_CONFIGURATION_FACTORY_V2 = "org.apache.logging.log4j.core.config.yaml.YamlConfigurationFactory";

/**
* Configuration factory for YAML files (Log4j 3, optional dependency).
*/
private static final String YAML_CONFIGURATION_FACTORY_V3 = "org.apache.logging.log4j.config.yaml.YamlConfigurationFactory";

private static final SpringEnvironmentPropertySource propertySource = new SpringEnvironmentPropertySource();

static final String ENVIRONMENT_KEY = Conventions.getQualifiedAttributeName(Log4J2LoggingSystem.class,
Expand Down Expand Up @@ -122,32 +157,61 @@ public Log4J2LoggingSystem(ClassLoader classLoader) {
@Override
protected String[] getStandardConfigLocations() {
List<String> locations = new ArrayList<>();
locations.add("log4j2-test.properties");
if (isClassAvailable("com.fasterxml.jackson.dataformat.yaml.YAMLParser")) {
Collections.addAll(locations, "log4j2-test.yaml", "log4j2-test.yml");
// The `log4j2.configurationFile` and `log4j.configuration.location` properties
// should be checked first, as they can be set to a custom location.
for (String property : new String[] { "log4j2.configurationFile", "log4j.configuration.location" }) {
String propertyDefinedLocation = PropertiesUtil.getProperties().getStringProperty(property);
if (propertyDefinedLocation != null) {
locations.add(propertyDefinedLocation);
}
}
if (isClassAvailable("com.fasterxml.jackson.databind.ObjectMapper")) {
Collections.addAll(locations, "log4j2-test.json", "log4j2-test.jsn");

// If no custom location is defined, we use the standard locations.
LoggerContext loggerContext = getLoggerContext();
String contextName = loggerContext.getName();
List<String> extensions = getStandardConfigExtensions();
extensions.forEach((e) -> locations.add("log4j2-test" + contextName + e));
extensions.forEach((e) -> locations.add("log4j2-test" + e));
extensions.forEach((e) -> locations.add("log4j2" + contextName + e));
extensions.forEach((e) -> locations.add("log4j2" + e));

return StringUtils.toStringArray(locations);
}

private List<String> getStandardConfigExtensions() {
List<String> extensions = new ArrayList<>();
// These classes need to be visible by the classloader that loads Log4j Core.
ClassLoader classLoader = LoggerContext.class.getClassLoader();
// The order of the extensions corresponds to the order
// in which Log4j Core 2 and 3 will try to load them,
// in decreasing value of `@Order`.
if (isClassAvailable(classLoader, PROPS_CONFIGURATION_FACTORY_V2)
|| isClassAvailable(classLoader, PROPS_CONFIGURATION_FACTORY_V3)) {
extensions.add(".properties");
}
locations.add("log4j2-test.xml");
locations.add("log4j2.properties");
if (isClassAvailable("com.fasterxml.jackson.dataformat.yaml.YAMLParser")) {
Collections.addAll(locations, "log4j2.yaml", "log4j2.yml");
if (areClassesAvailable(classLoader, YAML_CONFIGURATION_FACTORY_V2, YAML_TREE_PARSER_V2)
|| isClassAvailable(classLoader, YAML_CONFIGURATION_FACTORY_V3)) {
Collections.addAll(extensions, ".yaml", ".yml");
}
if (isClassAvailable("com.fasterxml.jackson.databind.ObjectMapper")) {
Collections.addAll(locations, "log4j2.json", "log4j2.jsn");
if (isClassAvailable(classLoader, JSON_TREE_PARSER_V2) || isClassAvailable(classLoader, JSON_TREE_PARSER_V3)) {
Collections.addAll(extensions, ".json", ".jsn");
}
locations.add("log4j2.xml");
String propertyDefinedLocation = new PropertiesUtil(new Properties())
.getStringProperty(ConfigurationFactory.CONFIGURATION_FILE_PROPERTY);
if (propertyDefinedLocation != null) {
locations.add(propertyDefinedLocation);
// We assume the `java.xml` module is always available.
extensions.add(".xml");
return extensions;
}

private boolean areClassesAvailable(ClassLoader classLoader, String... classNames) {
for (String className : classNames) {
if (!isClassAvailable(classLoader, className)) {
return false;
}
}
return StringUtils.toStringArray(locations);
return true;
}

protected boolean isClassAvailable(String className) {
return ClassUtils.isPresent(className, getClassLoader());
protected boolean isClassAvailable(ClassLoader classLoader, String className) {
return ClassUtils.isPresent(className, classLoader);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,15 @@
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.net.ProtocolException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.EnumSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Handler;
import java.util.logging.Level;
import java.util.stream.Stream;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.commons.logging.Log;
Expand All @@ -38,12 +41,15 @@
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.config.json.JsonConfigurationFactory;
import org.apache.logging.log4j.core.config.plugins.util.PluginRegistry;
import org.apache.logging.log4j.core.config.properties.PropertiesConfigurationBuilder;
import org.apache.logging.log4j.core.config.properties.PropertiesConfigurationFactory;
import org.apache.logging.log4j.core.config.xml.XmlConfiguration;
import org.apache.logging.log4j.core.config.yaml.YamlConfigurationFactory;
import org.apache.logging.log4j.core.util.ShutdownCallbackRegistry;
import org.apache.logging.log4j.jul.Log4jBridgeHandler;
import org.apache.logging.log4j.status.StatusListener;
Expand All @@ -53,6 +59,9 @@
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.slf4j.MDC;

import org.springframework.boot.logging.AbstractLoggingSystemTests;
Expand Down Expand Up @@ -89,6 +98,7 @@
* @author Andy Wilkinson
* @author Ben Hale
* @author Madhura Bhave
* @author Piotr P. Karwasz
*/
@ExtendWith(OutputCaptureExtension.class)
@ClassPathExclusions("logback-*.jar")
Expand All @@ -105,6 +115,8 @@ class Log4J2LoggingSystemTests extends AbstractLoggingSystemTests {

private Configuration configuration;

private String contextName;

@BeforeEach
void setup() {
PluginRegistry.getInstance().clear();
Expand All @@ -115,6 +127,7 @@ void setup() {
this.configuration = loggerContext.getConfiguration();
this.loggingSystem.cleanUp();
this.logger = LogManager.getLogger(getClass());
this.contextName = loggerContext.getName();
}

@AfterEach
Expand Down Expand Up @@ -293,54 +306,79 @@ void loggingThatUsesJulIsCaptured(CapturedOutput output) {
assertThat(output).contains("Hello world");
}

@Test
void configLocationsWithNoExtraDependencies() {
assertThat(this.loggingSystem.getStandardConfigLocations()).contains("log4j2-test.properties",
"log4j2-test.xml", "log4j2.properties", "log4j2.xml");
}

@Test
void configLocationsWithJacksonDatabind() {
this.loggingSystem.availableClasses(ObjectMapper.class.getName());
assertThat(this.loggingSystem.getStandardConfigLocations()).containsExactly("log4j2-test.properties",
"log4j2-test.json", "log4j2-test.jsn", "log4j2-test.xml", "log4j2.properties", "log4j2.json",
"log4j2.jsn", "log4j2.xml");
}

@Test
void configLocationsWithJacksonDataformatYaml() {
this.loggingSystem.availableClasses("com.fasterxml.jackson.dataformat.yaml.YAMLParser");
assertThat(this.loggingSystem.getStandardConfigLocations()).containsExactly("log4j2-test.properties",
"log4j2-test.yaml", "log4j2-test.yml", "log4j2-test.xml", "log4j2.properties", "log4j2.yaml",
"log4j2.yml", "log4j2.xml");
}

@Test
void configLocationsWithJacksonDatabindAndDataformatYaml() {
this.loggingSystem.availableClasses("com.fasterxml.jackson.dataformat.yaml.YAMLParser",
ObjectMapper.class.getName());
assertThat(this.loggingSystem.getStandardConfigLocations()).containsExactly("log4j2-test.properties",
"log4j2-test.yaml", "log4j2-test.yml", "log4j2-test.json", "log4j2-test.jsn", "log4j2-test.xml",
"log4j2.properties", "log4j2.yaml", "log4j2.yml", "log4j2.json", "log4j2.jsn", "log4j2.xml");
static Stream<String> configLocationsWithConfigurationFileSystemProperty() {
return Stream.of("log4j2.configurationFile", "log4j.configuration.location");
}

@Test
void configLocationsWithConfigurationFileSystemProperty() {
System.setProperty(ConfigurationFactory.CONFIGURATION_FILE_PROPERTY, "custom-log4j2.properties");
@ParameterizedTest
@MethodSource
void configLocationsWithConfigurationFileSystemProperty(String propertyName) {
System.setProperty(propertyName, "custom-log4j2.properties");
try {
assertThat(this.loggingSystem.getStandardConfigLocations()).containsExactly("log4j2-test.properties",
"log4j2-test.xml", "log4j2.properties", "log4j2.xml", "custom-log4j2.properties");
assertThat(this.loggingSystem.getStandardConfigLocations()).containsExactly("custom-log4j2.properties",
"log4j2-test" + this.contextName + ".xml", "log4j2-test.xml", "log4j2" + this.contextName + ".xml",
"log4j2.xml");
}
finally {
System.clearProperty(ConfigurationFactory.CONFIGURATION_FILE_PROPERTY);
System.clearProperty(propertyName);
}
}

static Stream<Arguments> standardConfigLocations() {
// For each configuration file format we make "available" to the
// Log4j2LoggingSystem:
// - The Log4j Core `ConfigurationFactory` class
// - The tree parser used internally by that configuration factory
return Stream.of(
// No classes, only XML
Arguments.of(Collections.emptyList(), List.of(".xml")),
// Log4j Core 2
Arguments.of(List.of(JsonConfigurationFactory.class.getName(), ObjectMapper.class.getName()),
List.of(".json", ".jsn", ".xml")),
Arguments.of(List.of(PropertiesConfigurationFactory.class.getName(),
PropertiesConfigurationBuilder.class.getName()), List.of(".properties", ".xml")),
Arguments.of(List.of(YamlConfigurationFactory.class.getName(),
"com.fasterxml.jackson.dataformat.yaml.YAMLMapper"), List.of(".yaml", ".yml", ".xml")),
Arguments.of(List.of(JsonConfigurationFactory.class.getName(), ObjectMapper.class.getName(),
PropertiesConfigurationFactory.class.getName(), PropertiesConfigurationBuilder.class.getName(),
YamlConfigurationFactory.class.getName(), "com.fasterxml.jackson.dataformat.yaml.YAMLMapper"),
List.of(".properties", ".yaml", ".yml", ".json", ".jsn", ".xml")),
// Log4j Core 3
Arguments.of(List.of(JsonConfigurationFactory.class.getName(),
"org.apache.logging.log4j.kit.json.JsonReader"), List.of(".json", ".jsn", ".xml")),
Arguments.of(List.of("org.apache.logging.log4j.config.properties.JavaPropsConfigurationFactory",
"tools.jackson.dataformat.javaprop.JavaPropsMapper"), List.of(".properties", ".xml")),
Arguments.of(List.of("org.apache.logging.log4j.config.yaml.YamlConfigurationFactory",
"tools.jackson.dataformat.yaml.YAMLMapper"), List.of(".yaml", ".yml", ".xml")),
Arguments.of(
List.of(JsonConfigurationFactory.class.getName(),
"org.apache.logging.log4j.kit.json.JsonReader",
"org.apache.logging.log4j.config.properties.JavaPropsConfigurationFactory",
"tools.jackson.dataformat.javaprop.JavaPropsMapper",
"org.apache.logging.log4j.config.yaml.YamlConfigurationFactory",
"tools.jackson.dataformat.yaml.YAMLMapper"),
List.of(".properties", ".yaml", ".yml", ".json", ".jsn", ".xml")));
}

@ParameterizedTest
@MethodSource
void standardConfigLocations(List<String> availableClasses, List<String> expectedSuffixes) {
this.loggingSystem.availableClasses(availableClasses.toArray(new String[0]));
String[] locations = this.loggingSystem.getStandardConfigLocations();
assertThat(locations).hasSize(4 * expectedSuffixes.size());
List<String> expected = new ArrayList<>();
expectedSuffixes.forEach((s) -> expected.add("log4j2-test" + this.contextName + s));
expectedSuffixes.forEach((s) -> expected.add("log4j2-test" + s));
expectedSuffixes.forEach((s) -> expected.add("log4j2" + this.contextName + s));
expectedSuffixes.forEach((s) -> expected.add("log4j2" + s));
assertThat(locations).containsExactlyElementsOf(expected);
}

@Test
void springConfigLocations() {
String[] locations = getSpringConfigLocations(this.loggingSystem);
assertThat(locations).containsExactly("log4j2-test-spring.properties", "log4j2-test-spring.xml",
"log4j2-spring.properties", "log4j2-spring.xml");
assertThat(locations).containsExactly("log4j2-test" + this.contextName + "-spring.xml",
"log4j2-test-spring.xml", "log4j2" + this.contextName + "-spring.xml", "log4j2-spring.xml");
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ private LoggerContext getLoggerContext() {
}

@Override
protected boolean isClassAvailable(String className) {
protected boolean isClassAvailable(ClassLoader classLoader, String className) {
return this.availableClasses.contains(className);
}

Expand Down
Loading