You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@logging.apache.org by pk...@apache.org on 2026/06/26 15:39:42 UTC
(logging-log4j2) 01/01: Make XInclude processing in XML configurations opt-in
This is an automated email from the ASF dual-hosted git repository.
ppkarwasz pushed a commit to branch feat/2.x/xinclude-opt-in
in repository https://gitbox.apache.org/repos/asf/logging-log4j2.git
commit ab09d5f22c54dc0b2fd668ab6ab938dd5062caf5
Author: Piotr P. Karwasz <pk...@apache.org>
AuthorDate: Fri Jun 26 14:34:44 2026 +0200
Make XInclude processing in XML configurations opt-in
XInclude support in XML configuration files is now disabled by default and must
be enabled with the `log4j2.configurationEnableXInclude` property. The
constructor reads the property and configures the `DocumentBuilder` accordingly,
dropping the previous always-on behavior and its LOG4J2-1127 "retry without
XInclude" fallback.
The new property is documented alongside the other configuration-factory
properties, and the XInclude manual section notes that it is now opt-in.
The `ConfigurationFactoryTest.xinclude` test enables the property, and
`testGetConfigurationWithMultipleUris` now composes two plain configurations
instead of incidentally loading an XInclude file.
A new `ConfigurationFactoryUnitTest` loads the same logical configuration as
XML, JSON, YAML, Java properties, an XInclude-composed file (with the property
enabled) and a cross-format composite, asserting through the `Configuration`
interface that each yields the same components. It also covers the
XInclude-disabled fallback to the default configuration.
Assisted-By: Claude Opus 4.8 (1M context) <no...@anthropic.com>
---
.../core/config/ConfigurationFactoryTest.java | 4 +-
.../core/config/ConfigurationFactoryUnitTest.java | 171 +++++++++++++++++++++
.../src/test/resources/log4j-test1-basic.xml | 29 ++++
.../src/test/resources/log4j-test1-extended.yaml | 53 +++++++
.../log4j/core/config/xml/XmlConfiguration.java | 28 ++--
src/changelog/.2.x.x/xinclude_opt_in.xml | 12 ++
.../partials/manual/configuration-xml-format.adoc | 8 +
.../properties-configuration-factory.adoc | 14 ++
8 files changed, 299 insertions(+), 20 deletions(-)
diff --git a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/config/ConfigurationFactoryTest.java b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/config/ConfigurationFactoryTest.java
index ee19ea7ea8..a7232d1f34 100644
--- a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/config/ConfigurationFactoryTest.java
+++ b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/config/ConfigurationFactoryTest.java
@@ -42,6 +42,7 @@ import org.apache.logging.log4j.core.appender.ConsoleAppender;
import org.apache.logging.log4j.core.config.composite.CompositeConfiguration;
import org.apache.logging.log4j.core.filter.ThreadContextMapFilter;
import org.apache.logging.log4j.core.test.junit.LoggerContextSource;
+import org.apache.logging.log4j.test.junit.SetTestProperty;
import org.apache.logging.log4j.test.junit.TempLoggingDir;
import org.apache.logging.log4j.util.Strings;
import org.junit.jupiter.api.Tag;
@@ -103,6 +104,7 @@ class ConfigurationFactoryTest {
}
@Test
+ @SetTestProperty(key = "log4j2.configurationEnableXInclude", value = "true")
@LoggerContextSource("log4j-xinclude.xml")
void xinclude(final LoggerContext context) throws IOException {
checkConfiguration(context);
@@ -181,7 +183,7 @@ class ConfigurationFactoryTest {
final URI configLocation1 =
getClass().getResource("/log4j-test1.xml").toURI();
final URI configLocation2 =
- getClass().getResource("/log4j-xinclude.xml").toURI();
+ getClass().getResource("/log4j-test2.xml").toURI();
final List<URI> configLocations = Arrays.asList(configLocation1, configLocation2);
final Configuration config = factory.getConfiguration(context, "test", configLocations);
assertInstanceOf(CompositeConfiguration.class, config);
diff --git a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/config/ConfigurationFactoryUnitTest.java b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/config/ConfigurationFactoryUnitTest.java
new file mode 100644
index 0000000000..b34bf0483a
--- /dev/null
+++ b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/config/ConfigurationFactoryUnitTest.java
@@ -0,0 +1,171 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you 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
+ *
+ * http://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.apache.logging.log4j.core.config;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.net.URI;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import org.apache.logging.log4j.Level;
+import org.apache.logging.log4j.core.Appender;
+import org.apache.logging.log4j.core.LoggerContext;
+import org.apache.logging.log4j.core.appender.ConsoleAppender;
+import org.apache.logging.log4j.core.appender.FileAppender;
+import org.apache.logging.log4j.core.config.composite.CompositeConfiguration;
+import org.apache.logging.log4j.core.filter.ThreadContextMapFilter;
+import org.apache.logging.log4j.core.test.appender.ListAppender;
+import org.apache.logging.log4j.test.junit.SetTestProperty;
+import org.apache.logging.log4j.test.junit.TempLoggingDir;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+/**
+ * Verifies that the same logical configuration, expressed as XML, JSON, YAML, Java properties, an XInclude-composed
+ * XML file, or a merged configuration produces the same components when loaded through {@link ConfigurationFactory}.
+ *
+ * <p>Unlike {@code ConfigurationFactoryTest}, this exercises the {@link Configuration} directly.</p>
+ */
+class ConfigurationFactoryUnitTest {
+
+ // The File appender creates the log file at construction time, not at startup.
+ // This directory will hold the created files and delete them after the test.
+ @TempLoggingDir
+ private static Path loggingDir;
+
+ private static Configuration load(final String resource) throws Exception {
+ final URI uri =
+ ConfigurationFactoryUnitTest.class.getResource("/" + resource).toURI();
+ final Configuration configuration =
+ ConfigurationFactory.getInstance().getConfiguration(new LoggerContext("test"), resource, uri);
+ configuration.initialize();
+ return configuration;
+ }
+
+ private static Configuration loadComposite(final String... resources) throws Exception {
+ final List<URI> uris = new ArrayList<>();
+ for (final String resource : resources) {
+ uris.add(ConfigurationFactoryUnitTest.class
+ .getResource("/" + resource)
+ .toURI());
+ }
+ final Configuration configuration =
+ ConfigurationFactory.getInstance().getConfiguration(new LoggerContext("test"), "composite", uris);
+ configuration.initialize();
+ return configuration;
+ }
+
+ @ParameterizedTest
+ @SetTestProperty(key = "log4j2.configurationEnableXInclude", value = "true")
+ @ValueSource(
+ strings = {
+ "log4j-test1.xml",
+ "log4j-test1.json",
+ "log4j-test1.yaml",
+ "log4j-test1.properties",
+ "log4j-xinclude.xml"
+ })
+ void producesEquivalentComponents(final String resource) throws Exception {
+ final Configuration configuration = load(resource);
+ try {
+ assertAppenders(configuration);
+ assertLoggers(configuration);
+ // Every representation declares a `filename` substitution property resolving to a per-format log file.
+ assertThat(configuration.getStrSubstitutor().replace("${filename}")).endsWith(".log");
+ } finally {
+ configuration.stop();
+ }
+ }
+
+ /**
+ * Tests behavior with XInclude disabled.
+ *
+ * <p>With XInclude disabled (the default), the {@code xi:include} elements are not expanded,
+ * so Log4j ignores them and falls back to the default setup:
+ * an {@code ERROR}-level root logger with a default {@code Console} appender.</p>
+ *
+ * <p>The {@code Properties} declared directly in the file are still honored.</p>
+ */
+ @Test
+ void xIncludeDisabledIgnoresIncludesAndUsesDefaults() throws Exception {
+ final Configuration configuration = load("log4j-xinclude.xml");
+ try {
+ // None of the included appenders or loggers are present.
+ final Map<String, Appender> appenders = configuration.getAppenders();
+ assertThat(appenders).doesNotContainKeys("STDOUT", "File", "List");
+ assertThat(configuration.getLoggers())
+ .doesNotContainKeys("org.apache.logging.log4j.test1", "org.apache.logging.log4j.test2");
+
+ // The default setup is used instead: a single default Console appender and an ERROR-level root logger.
+ assertThat(appenders.values()).singleElement().satisfies(appender -> {
+ assertThat(appender).isInstanceOf(ConsoleAppender.class);
+ assertThat(appender.getName()).startsWith("DefaultConsole-");
+ });
+ assertThat(configuration.getRootLogger().getLevel()).isEqualTo(Level.ERROR);
+
+ // The `Properties` declared directly in the file (outside the includes) are still resolved.
+ assertThat(configuration.getStrSubstitutor().replace("${filename}")).endsWith(".log");
+ } finally {
+ configuration.stop();
+ }
+ }
+
+ /**
+ * The same component graph can be assembled from several sources, possibly in different formats, merged into a
+ * {@link CompositeConfiguration}: here an XML base (one appender and the root logger) and a YAML remainder.
+ */
+ @Test
+ void mergesMultipleSources() throws Exception {
+ final Configuration configuration = loadComposite("log4j-test1-basic.xml", "log4j-test1-extended.yaml");
+ try {
+ assertThat(configuration).isInstanceOf(CompositeConfiguration.class);
+ assertAppenders(configuration);
+ assertLoggers(configuration);
+ assertThat(configuration.getStrSubstitutor().replace("${filename}")).endsWith(".log");
+ } finally {
+ configuration.stop();
+ }
+ }
+
+ private static void assertAppenders(final Configuration configuration) {
+ final Map<String, Appender> appenders = configuration.getAppenders();
+ assertThat(appenders).containsOnlyKeys("STDOUT", "File", "List");
+ assertThat(appenders.get("STDOUT")).isInstanceOf(ConsoleAppender.class);
+ assertThat(appenders.get("File")).isInstanceOf(FileAppender.class);
+ assertThat(appenders.get("List")).isInstanceOf(ListAppender.class);
+ }
+
+ private static void assertLoggers(final Configuration configuration) {
+ final LoggerConfig test1 = configuration.getLoggerConfig("org.apache.logging.log4j.test1");
+ assertThat(test1.getLevel()).isEqualTo(Level.DEBUG);
+ assertThat(test1.isAdditive()).isFalse();
+ assertThat(test1.getFilter()).isInstanceOf(ThreadContextMapFilter.class);
+ assertThat(test1.getAppenderRefs()).extracting(AppenderRef::getRef).containsExactly("STDOUT");
+
+ final LoggerConfig test2 = configuration.getLoggerConfig("org.apache.logging.log4j.test2");
+ assertThat(test2.getLevel()).isEqualTo(Level.DEBUG);
+ assertThat(test2.isAdditive()).isFalse();
+ assertThat(test2.getAppenderRefs()).extracting(AppenderRef::getRef).containsExactly("File");
+
+ final LoggerConfig root = configuration.getRootLogger();
+ assertThat(root.getLevel()).isEqualTo(Level.ERROR);
+ assertThat(root.getAppenderRefs()).extracting(AppenderRef::getRef).containsExactly("STDOUT");
+ }
+}
diff --git a/log4j-core-test/src/test/resources/log4j-test1-basic.xml b/log4j-core-test/src/test/resources/log4j-test1-basic.xml
new file mode 100644
index 0000000000..eff1f6b3de
--- /dev/null
+++ b/log4j-core-test/src/test/resources/log4j-test1-basic.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ Licensed to the Apache Software Foundation (ASF) under one or more
+ ~ contributor license agreements. See the NOTICE file distributed with
+ ~ this work for additional information regarding copyright ownership.
+ ~ The ASF licenses this file to you 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
+ ~
+ ~ http://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.
+ -->
+<Configuration status="OFF" name="XMLConfigTestBasic">
+ <Appenders>
+ <Console name="STDOUT">
+ <PatternLayout pattern="%m%n"/>
+ </Console>
+ </Appenders>
+ <Loggers>
+ <Root level="error">
+ <AppenderRef ref="STDOUT"/>
+ </Root>
+ </Loggers>
+</Configuration>
diff --git a/log4j-core-test/src/test/resources/log4j-test1-extended.yaml b/log4j-core-test/src/test/resources/log4j-test1-extended.yaml
new file mode 100644
index 0000000000..4c23eab343
--- /dev/null
+++ b/log4j-core-test/src/test/resources/log4j-test1-extended.yaml
@@ -0,0 +1,53 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to you 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
+#
+# http://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.
+#
+##
+# Remainder of a composite configuration, merged with `log4j-test1-basic.xml` (a different format).
+Configuration:
+ status: OFF
+ name: YAMLConfigTestExtended
+ properties:
+ property:
+ name: filename
+ value: ${test:logging.path}/test-composite.log
+ appenders:
+ File:
+ name: File
+ fileName: ${filename}
+ bufferedIO: false
+ PatternLayout:
+ Pattern: "%d %p %C{1.} [%t] %m%n"
+ List:
+ name: List
+ Filters:
+ ThresholdFilter:
+ level: error
+ Loggers:
+ logger:
+ - name: org.apache.logging.log4j.test1
+ level: debug
+ additivity: false
+ ThreadContextMapFilter:
+ KeyValuePair:
+ key: test
+ value: 123
+ AppenderRef:
+ ref: STDOUT
+ - name: org.apache.logging.log4j.test2
+ level: debug
+ additivity: false
+ AppenderRef:
+ ref: File
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/config/xml/XmlConfiguration.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/config/xml/XmlConfiguration.java
index 8861171899..e36025f6d7 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/config/xml/XmlConfiguration.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/config/xml/XmlConfiguration.java
@@ -45,7 +45,7 @@ import org.apache.logging.log4j.core.util.Closer;
import org.apache.logging.log4j.core.util.Integers;
import org.apache.logging.log4j.core.util.Loader;
import org.apache.logging.log4j.core.util.Patterns;
-import org.apache.logging.log4j.core.util.Throwables;
+import org.apache.logging.log4j.util.PropertiesUtil;
import org.w3c.dom.Attr;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
@@ -60,6 +60,11 @@ import org.xml.sax.SAXException;
*/
public class XmlConfiguration extends AbstractConfiguration implements Reconfigurable {
+ /**
+ * Property that enables XInclude processing in XML configuration files. Disabled by default.
+ */
+ private static final String ENABLE_XINCLUDE_PROP = "log4j2.configurationEnableXInclude";
+
private Element rootElement;
private boolean strict;
private String schemaResource;
@@ -80,24 +85,9 @@ public class XmlConfiguration extends AbstractConfiguration implements Reconfigu
}
final InputSource source = new InputSource(new ByteArrayInputStream(buffer));
source.setSystemId(configSource.getLocation());
- final DocumentBuilder documentBuilder = newDocumentBuilder(true);
- Document document;
- try {
- document = documentBuilder.parse(source);
- } catch (final Exception e) {
- // LOG4J2-1127
- final Throwable throwable = Throwables.getRootCause(e);
- if (throwable instanceof UnsupportedOperationException) {
- LOGGER.warn(
- "The DocumentBuilder {} does not support an operation: {}."
- + "Trying again without XInclude...",
- documentBuilder,
- e);
- document = newDocumentBuilder(false).parse(source);
- } else {
- throw e;
- }
- }
+ final boolean xIncludeAware = PropertiesUtil.getProperties().getBooleanProperty(ENABLE_XINCLUDE_PROP);
+ final DocumentBuilder documentBuilder = newDocumentBuilder(xIncludeAware);
+ final Document document = documentBuilder.parse(source);
rootElement = document.getDocumentElement();
final Map<String, String> attrs = processAttributes(rootNode, rootElement);
final StatusConfiguration statusConfig = new StatusConfiguration().withStatus(getDefaultStatus());
diff --git a/src/changelog/.2.x.x/xinclude_opt_in.xml b/src/changelog/.2.x.x/xinclude_opt_in.xml
new file mode 100644
index 0000000000..b3ae06d871
--- /dev/null
+++ b/src/changelog/.2.x.x/xinclude_opt_in.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<entry xmlns="https://logging.apache.org/xml/ns"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="
+ https://logging.apache.org/xml/ns
+ https://logging.apache.org/xml/ns/log4j-changelog-0.xsd"
+ type="changed">
+ <issue id="4064" link="https://github.com/apache/logging-log4j2/issues/4064"/>
+ <description format="asciidoc">
+ Make XInclude processing in XML configurations opt-in (disabled by default): it can be enabled with the `log4j2.configurationEnableXInclude` property.
+ </description>
+</entry>
diff --git a/src/site/antora/modules/ROOT/partials/manual/configuration-xml-format.adoc b/src/site/antora/modules/ROOT/partials/manual/configuration-xml-format.adoc
index 5a19684e08..cd8d423232 100644
--- a/src/site/antora/modules/ROOT/partials/manual/configuration-xml-format.adoc
+++ b/src/site/antora/modules/ROOT/partials/manual/configuration-xml-format.adoc
@@ -54,6 +54,14 @@ This setting also enables "XML strict mode" and allows one to specify an element
XML configuration files can include other files with
https://www.w3.org/TR/xinclude/[XInclude].
+[IMPORTANT]
+====
+Since version `2.27.0`, XInclude processing is **disabled by default**.
+Set the
+xref:manual/systemproperties.adoc#log4j2.configurationEnableXInclude[`log4j2.configurationEnableXInclude`]
+configuration property to `true` to enable it.
+====
+
NOTE: The list of `XInclude` and `XPath` features supported depends upon your
https://docs.oracle.com/javase/{java-target-version}/docs/technotes/guides/xml/jaxp/index.html[JAXP implementation].
diff --git a/src/site/antora/modules/ROOT/partials/manual/systemproperties/properties-configuration-factory.adoc b/src/site/antora/modules/ROOT/partials/manual/systemproperties/properties-configuration-factory.adoc
index 1841a4ee4d..e9f0f1f7b5 100644
--- a/src/site/antora/modules/ROOT/partials/manual/systemproperties/properties-configuration-factory.adoc
+++ b/src/site/antora/modules/ROOT/partials/manual/systemproperties/properties-configuration-factory.adoc
@@ -14,6 +14,20 @@
See the License for the specific language governing permissions and
limitations under the License.
////
+[id=log4j2.configurationEnableXInclude]
+== `log4j2.configurationEnableXInclude`
+
+[cols="1h,5"]
+|===
+| Env. variable | `LOG4J_CONFIGURATION_ENABLE_XINCLUDE`
+| Type | `boolean`
+| Default value | `false`
+|===
+
+If set to `true`, enables https://www.w3.org/TR/xinclude/[XInclude] processing in XML configuration files.
+
+See xref:manual/configuration.adoc#xinclude[XInclude] for details.
+
[id=log4j2.configurationFactory]
== `log4j2.configurationFactory`