This is an automated email from the ASF dual-hosted git repository.
vy pushed a commit to branch 2.x
in repository https://gitbox.apache.org/repos/asf/logging-log4j2.git
The following commit(s) were added to refs/heads/2.x by this push:
new c299479b03 Add `MonitorResources` configuration element (#3703)
c299479b03 is described below
commit c299479b03aa0eeb2baca64e39b017d649b0e09b
Author: Volkan Yazıcı <[email protected]>
AuthorDate: Mon Jun 2 12:39:53 2025 +0200
Add `MonitorResources` configuration element (#3703)
Co-authored-by: MichaelMorris <[email protected]>
---
.../logging/log4j/core/MonitorResourcesTest.java | 126 +++++++++++++++++++++
.../async/AsyncWaitStrategyFactoryConfigTest.java | 11 ++
.../AsyncWaitStrategyFactoryConfigTest.properties | 19 ++++
.../resources/config/MonitorResource/log4j.json | 15 +++
.../config/MonitorResource/log4j.properties | 22 ++++
.../resources/config/MonitorResource/log4j.xml | 28 +++++
.../resources/config/MonitorResource/log4j.yaml | 22 ++++
.../log4j/core/config/AbstractConfiguration.java | 30 ++++-
.../logging/log4j/core/config/MonitorResource.java | 95 ++++++++++++++++
.../log4j/core/config/MonitorResources.java | 54 +++++++++
.../config/builder/api/ConfigurationBuilder.java | 10 ++
.../core/config/builder/api/package-info.java | 2 +-
.../config/builder/impl/BuiltConfiguration.java | 17 +++
.../builder/impl/DefaultConfigurationBuilder.java | 22 +++-
.../core/config/builder/impl/package-info.java | 2 +-
.../properties/PropertiesConfigurationBuilder.java | 26 ++++-
.../.2.x.x/3074_monitor_additional_files.xml | 9 ++
.../manual/configuration/monitor-resources.json | 15 +++
.../configuration/monitor-resources.properties | 22 ++++
.../manual/configuration/monitor-resources.xml | 28 +++++
.../manual/configuration/monitor-resources.yaml | 22 ++++
.../modules/ROOT/pages/manual/configuration.adoc | 59 ++++++++++
22 files changed, 649 insertions(+), 7 deletions(-)
diff --git
a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/MonitorResourcesTest.java
b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/MonitorResourcesTest.java
new file mode 100644
index 0000000000..be395a2d86
--- /dev/null
+++
b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/MonitorResourcesTest.java
@@ -0,0 +1,126 @@
+/*
+ * 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;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.awaitility.Awaitility.waitAtMost;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.LinkedHashSet;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+import org.apache.logging.log4j.core.config.Configuration;
+import org.apache.logging.log4j.core.config.ConfigurationSource;
+import org.apache.logging.log4j.core.config.Configurator;
+import org.apache.logging.log4j.core.config.builder.api.ComponentBuilder;
+import org.apache.logging.log4j.core.config.builder.api.ConfigurationBuilder;
+import
org.apache.logging.log4j.core.config.builder.api.ConfigurationBuilderFactory;
+import org.apache.logging.log4j.core.config.properties.PropertiesConfiguration;
+import org.apache.logging.log4j.core.test.junit.LoggerContextSource;
+import org.apache.logging.log4j.core.util.Source;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.CleanupMode;
+import org.junit.jupiter.api.io.TempDir;
+
+class MonitorResourcesTest {
+
+ @Test
+ void test_reconfiguration(@TempDir(cleanup = CleanupMode.ON_SUCCESS) final
Path tempDir) throws IOException {
+ final ConfigurationBuilder<?> configBuilder =
ConfigurationBuilderFactory.newConfigurationBuilder(
+ // Explicitly deviating from `BuiltConfiguration`, since it is
not `Reconfigurable`, and this
+ // results in `instanceof Reconfigurable` checks in
`AbstractConfiguration` to fail while arming
+ // the watcher. Hence, using `PropertiesConfiguration`, which
is `Reconfigurable`, instead.
+ PropertiesConfiguration.class);
+ final Path configFile = tempDir.resolve("log4j.xml");
+ final Path externalResourceFile1 =
tempDir.resolve("external-resource-1.txt");
+ final Path externalResourceFile2 =
tempDir.resolve("external-resource-2.txt");
+ final ConfigurationSource configSource = new ConfigurationSource(new
Source(configFile), new byte[] {}, 0);
+ final int monitorInterval = 3;
+
+ final ComponentBuilder<?> monitorResourcesComponent =
configBuilder.newComponent("MonitorResources");
+ monitorResourcesComponent.addComponent(configBuilder
+ .newComponent("MonitorResource")
+ .addAttribute("uri",
externalResourceFile1.toUri().toString()));
+ monitorResourcesComponent.addComponent(configBuilder
+ .newComponent("MonitorResource")
+ .addAttribute("uri",
externalResourceFile2.toUri().toString()));
+
+ final Configuration config = configBuilder
+ .setConfigurationSource(configSource)
+ .setMonitorInterval(String.valueOf(monitorInterval))
+ .addComponent(monitorResourcesComponent)
+ .build(false);
+
+ try (final LoggerContext loggerContext =
Configurator.initialize(config)) {
+ assertMonitorResourceFileNames(
+ loggerContext,
+ configFile.getFileName().toString(),
+ externalResourceFile1.getFileName().toString(),
+ externalResourceFile2.getFileName().toString());
+ Files.write(externalResourceFile2, Collections.singletonList("a
change"));
+ waitAtMost(2 * monitorInterval, TimeUnit.SECONDS).until(() ->
loggerContext.getConfiguration() != config);
+ }
+ }
+
+ @Test
+ @LoggerContextSource("config/MonitorResource/log4j.xml")
+ void test_config_of_type_XML(final LoggerContext loggerContext) {
+ assertMonitorResourceFileNames(loggerContext, "log4j.xml");
+ }
+
+ @Test
+ @LoggerContextSource("config/MonitorResource/log4j.json")
+ void test_config_of_type_JSON(final LoggerContext loggerContext) {
+ assertMonitorResourceFileNames(loggerContext, "log4j.json");
+ }
+
+ @Test
+ @LoggerContextSource("config/MonitorResource/log4j.yaml")
+ void test_config_of_type_YAML(final LoggerContext loggerContext) {
+ assertMonitorResourceFileNames(loggerContext, "log4j.yaml");
+ }
+
+ @Test
+ @LoggerContextSource("config/MonitorResource/log4j.properties")
+ void test_config_of_type_properties(final LoggerContext loggerContext) {
+ assertMonitorResourceFileNames(loggerContext, "log4j.properties");
+ }
+
+ private static void assertMonitorResourceFileNames(final LoggerContext
loggerContext, final String configFileName) {
+ assertMonitorResourceFileNames(loggerContext, configFileName,
"external-file-1.txt", "external-file-2.txt");
+ }
+
+ private static void assertMonitorResourceFileNames(
+ final LoggerContext loggerContext, final String configFileName,
final String... externalResourceFileNames) {
+ final Set<Source> sources = loggerContext
+ .getConfiguration()
+ .getWatchManager()
+ .getConfigurationWatchers()
+ .keySet();
+ final Set<String> actualFileNames =
+ sources.stream().map(source ->
source.getFile().getName()).collect(Collectors.toSet());
+ final Set<String> expectedFileNames = new LinkedHashSet<>();
+ expectedFileNames.add(configFileName);
+ expectedFileNames.addAll(Arrays.asList(externalResourceFileNames));
+ assertThat(actualFileNames).as("watch manager sources: %s",
sources).isEqualTo(expectedFileNames);
+ }
+}
diff --git
a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/async/AsyncWaitStrategyFactoryConfigTest.java
b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/async/AsyncWaitStrategyFactoryConfigTest.java
index 31272c2c04..7a964e3d2a 100644
---
a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/async/AsyncWaitStrategyFactoryConfigTest.java
+++
b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/async/AsyncWaitStrategyFactoryConfigTest.java
@@ -47,6 +47,17 @@ class AsyncWaitStrategyFactoryConfigTest {
asyncWaitStrategyFactory instanceof
YieldingWaitStrategyFactory);
}
+ @Test
+ @LoggerContextSource("AsyncWaitStrategyFactoryConfigTest.properties")
+ void testConfigWaitStrategyFactoryFromProperties(final LoggerContext
context) {
+ final AsyncWaitStrategyFactory asyncWaitStrategyFactory =
+ context.getConfiguration().getAsyncWaitStrategyFactory();
+ assertEquals(YieldingWaitStrategyFactory.class,
asyncWaitStrategyFactory.getClass());
+ assertThat(
+ "factory is YieldingWaitStrategyFactory",
+ asyncWaitStrategyFactory instanceof
YieldingWaitStrategyFactory);
+ }
+
@Test
@LoggerContextSource("AsyncWaitStrategyFactoryConfigTest.xml")
void testWaitStrategy(final LoggerContext context) {
diff --git
a/log4j-core-test/src/test/resources/AsyncWaitStrategyFactoryConfigTest.properties
b/log4j-core-test/src/test/resources/AsyncWaitStrategyFactoryConfigTest.properties
new file mode 100644
index 0000000000..e8a76f6fda
--- /dev/null
+++
b/log4j-core-test/src/test/resources/AsyncWaitStrategyFactoryConfigTest.properties
@@ -0,0 +1,19 @@
+#
+# 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.
+#
+
+strategy.type = AsyncWaitStrategyFactory
+strategy.class =
org.apache.logging.log4j.core.async.AsyncWaitStrategyFactoryConfigTest$YieldingWaitStrategyFactory
diff --git
a/log4j-core-test/src/test/resources/config/MonitorResource/log4j.json
b/log4j-core-test/src/test/resources/config/MonitorResource/log4j.json
new file mode 100644
index 0000000000..26c566af41
--- /dev/null
+++ b/log4j-core-test/src/test/resources/config/MonitorResource/log4j.json
@@ -0,0 +1,15 @@
+{
+ "Configuration": {
+ "monitorInterval": "30",
+ "MonitorResources": {
+ "MonitorResource": [
+ {
+ "uri": "file://path/to/external-file-1.txt"
+ },
+ {
+ "uri": "file://path/to/external-file-2.txt"
+ }
+ ]
+ }
+ }
+}
diff --git
a/log4j-core-test/src/test/resources/config/MonitorResource/log4j.properties
b/log4j-core-test/src/test/resources/config/MonitorResource/log4j.properties
new file mode 100644
index 0000000000..dd772d9ff1
--- /dev/null
+++ b/log4j-core-test/src/test/resources/config/MonitorResource/log4j.properties
@@ -0,0 +1,22 @@
+#
+# 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.
+#
+monitorInterval = 30
+monitorResources.type = MonitorResources
+monitorResources.0.type = MonitorResource
+monitorResources.0.uri = file://path/to/external-file-1.txt
+monitorResources.1.type = MonitorResource
+monitorResources.1.uri = file://path/to/external-file-2.txt
diff --git
a/log4j-core-test/src/test/resources/config/MonitorResource/log4j.xml
b/log4j-core-test/src/test/resources/config/MonitorResource/log4j.xml
new file mode 100644
index 0000000000..1529167d56
--- /dev/null
+++ b/log4j-core-test/src/test/resources/config/MonitorResource/log4j.xml
@@ -0,0 +1,28 @@
+<?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 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-config-2.xsd"
+ monitorInterval="30">
+ <MonitorResources>
+ <MonitorResource uri="file://path/to/external-file-1.txt"/>
+ <MonitorResource uri="file://path/to/external-file-2.txt"/>
+ </MonitorResources>
+</Configuration>
diff --git
a/log4j-core-test/src/test/resources/config/MonitorResource/log4j.yaml
b/log4j-core-test/src/test/resources/config/MonitorResource/log4j.yaml
new file mode 100644
index 0000000000..11f804108b
--- /dev/null
+++ b/log4j-core-test/src/test/resources/config/MonitorResource/log4j.yaml
@@ -0,0 +1,22 @@
+#
+# 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:
+ monitorInterval: '30'
+ MonitorResources:
+ MonitorResource:
+ - uri: "file://path/to/external-file-1.txt"
+ - uri: "file://path/to/external-file-2.txt"
diff --git
a/log4j-core/src/main/java/org/apache/logging/log4j/core/config/AbstractConfiguration.java
b/log4j-core/src/main/java/org/apache/logging/log4j/core/config/AbstractConfiguration.java
index 497e1e0e86..4c3dde9179 100644
---
a/log4j-core/src/main/java/org/apache/logging/log4j/core/config/AbstractConfiguration.java
+++
b/log4j-core/src/main/java/org/apache/logging/log4j/core/config/AbstractConfiguration.java
@@ -133,6 +133,7 @@ public abstract class AbstractConfiguration extends
AbstractFilterable implement
private ConcurrentMap<String, Appender> appenders = new
ConcurrentHashMap<>();
private ConcurrentMap<String, LoggerConfig> loggerConfigs = new
ConcurrentHashMap<>();
private List<CustomLevelConfig> customLevels = Collections.emptyList();
+ private Set<MonitorResource> monitorResources = Collections.emptySet();
private final ConcurrentMap<String, String> propertyMap = new
ConcurrentHashMap<>();
private final Interpolator tempLookup = new Interpolator(propertyMap);
private final StrSubstitutor runtimeStrSubstitutor = new
RuntimeStrSubstitutor(tempLookup);
@@ -268,6 +269,7 @@ public abstract class AbstractConfiguration extends
AbstractFilterable implement
setup();
setupAdvertisement();
doConfigure();
+ watchMonitorResources();
setState(State.INITIALIZED);
LOGGER.debug("Configuration {} initialized", this);
}
@@ -323,10 +325,10 @@ public abstract class AbstractConfiguration extends
AbstractFilterable implement
}
LOGGER.info("Starting configuration {}...", this);
this.setStarting();
- if (watchManager.getIntervalSeconds() >= 0) {
+ if (isConfigurationMonitoringEnabled()) {
LOGGER.info(
"Start watching for changes to {} every {} seconds",
- getConfigurationSource(),
+ watchManager.getConfigurationWatchers().keySet(),
watchManager.getIntervalSeconds());
watchManager.start();
}
@@ -348,6 +350,21 @@ public abstract class AbstractConfiguration extends
AbstractFilterable implement
LOGGER.info("Configuration {} started.", this);
}
+ private boolean isConfigurationMonitoringEnabled() {
+ return this instanceof Reconfigurable &&
watchManager.getIntervalSeconds() > 0;
+ }
+
+ private void watchMonitorResources() {
+ if (isConfigurationMonitoringEnabled()) {
+ monitorResources.forEach(monitorResource -> {
+ Source source = new Source(monitorResource.getUri());
+ final ConfigurationFileWatcher watcher = new
ConfigurationFileWatcher(
+ this, (Reconfigurable) this, listeners,
source.getFile().lastModified());
+ watchManager.watch(source, watcher);
+ });
+ }
+ }
+
private boolean hasAsyncLoggers() {
if (root instanceof AsyncLoggerConfig) {
return true;
@@ -730,9 +747,16 @@ public abstract class AbstractConfiguration extends
AbstractFilterable implement
} else if
(child.isInstanceOf(AsyncWaitStrategyFactoryConfig.class)) {
final AsyncWaitStrategyFactoryConfig awsfc =
child.getObject(AsyncWaitStrategyFactoryConfig.class);
asyncWaitStrategyFactory = awsfc.createWaitStrategyFactory();
+ } else if (child.isInstanceOf(MonitorResources.class)) {
+ monitorResources =
child.getObject(MonitorResources.class).getResources();
} else {
final List<String> expected = Arrays.asList(
- "\"Appenders\"", "\"Loggers\"", "\"Properties\"",
"\"Scripts\"", "\"CustomLevels\"");
+ "\"Appenders\"",
+ "\"Loggers\"",
+ "\"Properties\"",
+ "\"Scripts\"",
+ "\"CustomLevels\"",
+ "\"MonitorResources\"");
LOGGER.error(
"Unknown object \"{}\" of type {} is ignored: try
nesting it inside one of: {}.",
child.getName(),
diff --git
a/log4j-core/src/main/java/org/apache/logging/log4j/core/config/MonitorResource.java
b/log4j-core/src/main/java/org/apache/logging/log4j/core/config/MonitorResource.java
new file mode 100644
index 0000000000..235e8645aa
--- /dev/null
+++
b/log4j-core/src/main/java/org/apache/logging/log4j/core/config/MonitorResource.java
@@ -0,0 +1,95 @@
+/*
+ * 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 java.util.Objects.requireNonNull;
+
+import java.net.URI;
+import org.apache.logging.log4j.core.Core;
+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.validation.constraints.Required;
+
+/**
+ * Container for the {@code MonitorResource} element.
+ */
+@Plugin(name = "MonitorResource", category = Core.CATEGORY_NAME, printObject =
true)
+public final class MonitorResource {
+
+ private final URI uri;
+
+ @PluginBuilderFactory
+ public static Builder newBuilder() {
+ return new Builder();
+ }
+
+ /**
+ * Builds MonitorResource instances.
+ */
+ public static final class Builder implements
org.apache.logging.log4j.core.util.Builder<MonitorResource> {
+
+ @PluginBuilderAttribute
+ @Required(message = "No URI provided")
+ private URI uri;
+
+ public Builder setUri(final URI uri) {
+ this.uri = uri;
+ return this;
+ }
+
+ @Override
+ public MonitorResource build() {
+ return new MonitorResource(uri);
+ }
+ }
+
+ private MonitorResource(final URI uri) {
+ this.uri = requireNonNull(uri, "uri");
+ if (!"file".equals(uri.getScheme())) {
+ final String message =
+ String.format("Only `file` scheme is supported in monitor
resource URIs! Illegal URI: `%s`", uri);
+ throw new IllegalArgumentException(message);
+ }
+ }
+
+ public URI getUri() {
+ return uri;
+ }
+
+ @Override
+ public int hashCode() {
+ return uri.hashCode();
+ }
+
+ @Override
+ public boolean equals(final Object object) {
+ if (this == object) {
+ return true;
+ }
+ if (!(object instanceof MonitorResource)) {
+ return false;
+ }
+ final MonitorResource other = (MonitorResource) object;
+ return this.uri == other.uri;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("MonitorResource{%s}", uri);
+ }
+}
diff --git
a/log4j-core/src/main/java/org/apache/logging/log4j/core/config/MonitorResources.java
b/log4j-core/src/main/java/org/apache/logging/log4j/core/config/MonitorResources.java
new file mode 100644
index 0000000000..f7cc5e2026
--- /dev/null
+++
b/log4j-core/src/main/java/org/apache/logging/log4j/core/config/MonitorResources.java
@@ -0,0 +1,54 @@
+/*
+ * 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 java.util.Objects.requireNonNull;
+
+import java.util.Arrays;
+import java.util.LinkedHashSet;
+import java.util.Set;
+import java.util.stream.Collectors;
+import org.apache.logging.log4j.core.Core;
+import org.apache.logging.log4j.core.config.plugins.Plugin;
+import org.apache.logging.log4j.core.config.plugins.PluginElement;
+import org.apache.logging.log4j.core.config.plugins.PluginFactory;
+
+/**
+ * Container for the {@code MonitorResources} element.
+ */
+@Plugin(name = "MonitorResources", category = Core.CATEGORY_NAME, printObject
= true)
+public final class MonitorResources {
+
+ private final Set<MonitorResource> resources;
+
+ private MonitorResources(final Set<MonitorResource> resources) {
+ this.resources = requireNonNull(resources, "resources");
+ }
+
+ @PluginFactory
+ public static MonitorResources createMonitorResources(
+ @PluginElement("monitorResource") final MonitorResource[]
resources) {
+ requireNonNull(resources, "resources");
+ final LinkedHashSet<MonitorResource> distinctResources =
+
Arrays.stream(resources).collect(Collectors.toCollection(LinkedHashSet::new));
+ return new MonitorResources(distinctResources);
+ }
+
+ public Set<MonitorResource> getResources() {
+ return resources;
+ }
+}
diff --git
a/log4j-core/src/main/java/org/apache/logging/log4j/core/config/builder/api/ConfigurationBuilder.java
b/log4j-core/src/main/java/org/apache/logging/log4j/core/config/builder/api/ConfigurationBuilder.java
index 3b9fc48961..5858b7ac43 100644
---
a/log4j-core/src/main/java/org/apache/logging/log4j/core/config/builder/api/ConfigurationBuilder.java
+++
b/log4j-core/src/main/java/org/apache/logging/log4j/core/config/builder/api/ConfigurationBuilder.java
@@ -61,6 +61,16 @@ public interface ConfigurationBuilder<T extends
Configuration> extends Builder<T
*/
ConfigurationBuilder<T> add(CustomLevelComponentBuilder builder);
+ /**
+ * Adds a top level component.
+ * @param builder The ComponentBuilder with all of its attributes and sub
components set.
+ * @return this builder instance.
+ * @since 2.25.0
+ */
+ default ConfigurationBuilder<T> addComponent(ComponentBuilder<?> builder) {
+ throw new UnsupportedOperationException();
+ }
+
/**
* Adds a Filter component.
* @param builder the FilterComponentBuilder with all of its attributes
and sub components set.
diff --git
a/log4j-core/src/main/java/org/apache/logging/log4j/core/config/builder/api/package-info.java
b/log4j-core/src/main/java/org/apache/logging/log4j/core/config/builder/api/package-info.java
index 273906a5d4..6d91c2d89d 100644
---
a/log4j-core/src/main/java/org/apache/logging/log4j/core/config/builder/api/package-info.java
+++
b/log4j-core/src/main/java/org/apache/logging/log4j/core/config/builder/api/package-info.java
@@ -20,7 +20,7 @@
* @since 2.4
*/
@Export
-@Version("2.20.1")
+@Version("2.25.0")
package org.apache.logging.log4j.core.config.builder.api;
import org.osgi.annotation.bundle.Export;
diff --git
a/log4j-core/src/main/java/org/apache/logging/log4j/core/config/builder/impl/BuiltConfiguration.java
b/log4j-core/src/main/java/org/apache/logging/log4j/core/config/builder/impl/BuiltConfiguration.java
index f1ec35e06a..8445dc706a 100644
---
a/log4j-core/src/main/java/org/apache/logging/log4j/core/config/builder/impl/BuiltConfiguration.java
+++
b/log4j-core/src/main/java/org/apache/logging/log4j/core/config/builder/impl/BuiltConfiguration.java
@@ -46,6 +46,8 @@ public class BuiltConfiguration extends AbstractConfiguration
{
private Component propertiesComponent;
private Component customLevelsComponent;
private Component scriptsComponent;
+ private Component monitorResourcesComponent;
+ private Component asyncWaitStrategyFactoryComponent;
private String contentType = "text";
public BuiltConfiguration(
@@ -78,6 +80,14 @@ public class BuiltConfiguration extends
AbstractConfiguration {
customLevelsComponent = component;
break;
}
+ case "MonitorResources": {
+ monitorResourcesComponent = component;
+ break;
+ }
+ case "AsyncWaitStrategyFactory": {
+ asyncWaitStrategyFactoryComponent = component;
+ break;
+ }
}
}
this.rootComponent = rootComponent;
@@ -95,6 +105,13 @@ public class BuiltConfiguration extends
AbstractConfiguration {
if (customLevelsComponent.getComponents().size() > 0) {
children.add(convertToNode(rootNode, customLevelsComponent));
}
+ if (monitorResourcesComponent != null
+ && monitorResourcesComponent.getComponents().size() > 0) {
+ children.add(convertToNode(rootNode, monitorResourcesComponent));
+ }
+ if (asyncWaitStrategyFactoryComponent != null) {
+ children.add(convertToNode(rootNode,
asyncWaitStrategyFactoryComponent));
+ }
children.add(convertToNode(rootNode, loggersComponent));
children.add(convertToNode(rootNode, appendersComponent));
if (filtersComponent.getComponents().size() > 0) {
diff --git
a/log4j-core/src/main/java/org/apache/logging/log4j/core/config/builder/impl/DefaultConfigurationBuilder.java
b/log4j-core/src/main/java/org/apache/logging/log4j/core/config/builder/impl/DefaultConfigurationBuilder.java
index 633e619b28..bf039a2daf 100644
---
a/log4j-core/src/main/java/org/apache/logging/log4j/core/config/builder/impl/DefaultConfigurationBuilder.java
+++
b/log4j-core/src/main/java/org/apache/logging/log4j/core/config/builder/impl/DefaultConfigurationBuilder.java
@@ -24,6 +24,7 @@ import java.io.StringWriter;
import java.lang.reflect.Constructor;
import java.util.List;
import java.util.Map;
+import java.util.Optional;
import java.util.concurrent.TimeUnit;
import javax.xml.stream.XMLOutputFactory;
import javax.xml.stream.XMLStreamException;
@@ -136,6 +137,17 @@ public class DefaultConfigurationBuilder<T extends
BuiltConfiguration> implement
return add(appenders, builder);
}
+ @Override
+ public ConfigurationBuilder<T> addComponent(ComponentBuilder<?> builder) {
+ return add(root, builder);
+ }
+
+ private Optional<Component> getTopLevelComponent(final String pluginType) {
+ return root.getComponents().stream()
+ .filter(component ->
component.getPluginType().equals(pluginType))
+ .findAny();
+ }
+
@Override
public ConfigurationBuilder<T> add(final CustomLevelComponentBuilder
builder) {
return add(customLevels, builder);
@@ -291,6 +303,15 @@ public class DefaultConfigurationBuilder<T extends
BuiltConfiguration> implement
writeXmlSection(xmlWriter, properties);
writeXmlSection(xmlWriter, scripts);
writeXmlSection(xmlWriter, customLevels);
+ Optional<Component> monitorResourcesComponent =
getTopLevelComponent("MonitorResources");
+ if (monitorResourcesComponent.isPresent()
+ && !monitorResourcesComponent.get().getComponents().isEmpty())
{
+ writeXmlComponent(xmlWriter, monitorResourcesComponent.get());
+ }
+ Optional<Component> asyncWaitStrategyFactoryComponent =
getTopLevelComponent("AsyncWaitStrategyFactory");
+ if (asyncWaitStrategyFactoryComponent.isPresent()) {
+ writeXmlComponent(xmlWriter,
asyncWaitStrategyFactoryComponent.get());
+ }
if (filters.getComponents().size() == 1) {
writeXmlComponent(xmlWriter, filters.getComponents().get(0));
} else if (filters.getComponents().size() > 1) {
@@ -337,7 +358,6 @@ public class DefaultConfigurationBuilder<T extends
BuiltConfiguration> implement
}
}
- @Override
public ScriptComponentBuilder newScript(final String name, final String
language, final String text) {
return new DefaultScriptComponentBuilder(this, name, language, text);
}
diff --git
a/log4j-core/src/main/java/org/apache/logging/log4j/core/config/builder/impl/package-info.java
b/log4j-core/src/main/java/org/apache/logging/log4j/core/config/builder/impl/package-info.java
index c56f92230f..32a69487d7 100644
---
a/log4j-core/src/main/java/org/apache/logging/log4j/core/config/builder/impl/package-info.java
+++
b/log4j-core/src/main/java/org/apache/logging/log4j/core/config/builder/impl/package-info.java
@@ -20,7 +20,7 @@
* @since 2.4
*/
@Export
-@Version("2.20.2")
+@Version("2.25.0")
package org.apache.logging.log4j.core.config.builder.impl;
import org.osgi.annotation.bundle.Export;
diff --git
a/log4j-core/src/main/java/org/apache/logging/log4j/core/config/properties/PropertiesConfigurationBuilder.java
b/log4j-core/src/main/java/org/apache/logging/log4j/core/config/properties/PropertiesConfigurationBuilder.java
index 6dc9fd6957..e4770a80ab 100644
---
a/log4j-core/src/main/java/org/apache/logging/log4j/core/config/properties/PropertiesConfigurationBuilder.java
+++
b/log4j-core/src/main/java/org/apache/logging/log4j/core/config/properties/PropertiesConfigurationBuilder.java
@@ -187,11 +187,30 @@ public class PropertiesConfigurationBuilder extends
ConfigurationBuilderFactory
builder.add(createRootLogger(props));
}
+ processRemainingProperties(builder, rootProperties);
+
builder.setLoggerContext(loggerContext);
return builder.build(false);
}
+ private void processRemainingProperties(
+ final ConfigurationBuilder<PropertiesConfiguration> builder, final
Properties properties) {
+ while (properties.size() > 0) {
+ final String propertyName =
+ properties.stringPropertyNames().iterator().next();
+ final int index = propertyName.indexOf('.');
+ if (index > 0) {
+ final String prefix = propertyName.substring(0, index);
+ final Properties componentProperties =
PropertiesUtil.extractSubset(properties, prefix);
+ ComponentBuilder<?> componentBuilder =
createComponent(builder, prefix, componentProperties);
+ builder.addComponent(componentBuilder);
+ } else {
+ properties.remove(propertyName);
+ }
+ }
+ }
+
private ScriptComponentBuilder createScript(final Properties properties) {
final String name = (String) properties.remove("name");
final String language = (String) properties.remove("language");
@@ -332,12 +351,17 @@ public class PropertiesConfigurationBuilder extends
ConfigurationBuilderFactory
private static <B extends ComponentBuilder<B>> ComponentBuilder<B>
createComponent(
final ComponentBuilder<?> parent, final String key, final
Properties properties) {
+ return createComponent(parent.getBuilder(), key, properties);
+ }
+
+ private static <B extends ComponentBuilder<B>> ComponentBuilder<B>
createComponent(
+ final ConfigurationBuilder<?> parentBuilder, final String key,
final Properties properties) {
final String name = (String) properties.remove(CONFIG_NAME);
final String type = (String) properties.remove(CONFIG_TYPE);
if (Strings.isEmpty(type)) {
throw new ConfigurationException("No type attribute provided for
component " + key);
}
- final ComponentBuilder<B> componentBuilder =
parent.getBuilder().newComponent(name, type);
+ final ComponentBuilder<B> componentBuilder =
parentBuilder.newComponent(name, type);
return processRemainingProperties(componentBuilder, properties);
}
diff --git a/src/changelog/.2.x.x/3074_monitor_additional_files.xml
b/src/changelog/.2.x.x/3074_monitor_additional_files.xml
new file mode 100644
index 0000000000..c32545a602
--- /dev/null
+++ b/src/changelog/.2.x.x/3074_monitor_additional_files.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<entry xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xmlns="https://logging.apache.org/xml/ns"
+ xsi:schemaLocation="https://logging.apache.org/xml/ns
https://logging.apache.org/xml/ns/log4j-changelog-0.xsd"
+ type="added">
+ <issue id="3074"
link="https://github.com/apache/logging-log4j2/issues/3074"/>
+ <issue id="3501" link="https://github.com/apache/logging-log4j2/pull/3501"/>
+ <description format="asciidoc">Add `MonitorResource` configuration option to
support the monitoring of external files in addition to the configuration file
itself.</description>
+</entry>
diff --git
a/src/site/antora/modules/ROOT/examples/manual/configuration/monitor-resources.json
b/src/site/antora/modules/ROOT/examples/manual/configuration/monitor-resources.json
new file mode 100644
index 0000000000..26c566af41
--- /dev/null
+++
b/src/site/antora/modules/ROOT/examples/manual/configuration/monitor-resources.json
@@ -0,0 +1,15 @@
+{
+ "Configuration": {
+ "monitorInterval": "30",
+ "MonitorResources": {
+ "MonitorResource": [
+ {
+ "uri": "file://path/to/external-file-1.txt"
+ },
+ {
+ "uri": "file://path/to/external-file-2.txt"
+ }
+ ]
+ }
+ }
+}
diff --git
a/src/site/antora/modules/ROOT/examples/manual/configuration/monitor-resources.properties
b/src/site/antora/modules/ROOT/examples/manual/configuration/monitor-resources.properties
new file mode 100644
index 0000000000..dd772d9ff1
--- /dev/null
+++
b/src/site/antora/modules/ROOT/examples/manual/configuration/monitor-resources.properties
@@ -0,0 +1,22 @@
+#
+# 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.
+#
+monitorInterval = 30
+monitorResources.type = MonitorResources
+monitorResources.0.type = MonitorResource
+monitorResources.0.uri = file://path/to/external-file-1.txt
+monitorResources.1.type = MonitorResource
+monitorResources.1.uri = file://path/to/external-file-2.txt
diff --git
a/src/site/antora/modules/ROOT/examples/manual/configuration/monitor-resources.xml
b/src/site/antora/modules/ROOT/examples/manual/configuration/monitor-resources.xml
new file mode 100644
index 0000000000..1529167d56
--- /dev/null
+++
b/src/site/antora/modules/ROOT/examples/manual/configuration/monitor-resources.xml
@@ -0,0 +1,28 @@
+<?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 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-config-2.xsd"
+ monitorInterval="30">
+ <MonitorResources>
+ <MonitorResource uri="file://path/to/external-file-1.txt"/>
+ <MonitorResource uri="file://path/to/external-file-2.txt"/>
+ </MonitorResources>
+</Configuration>
diff --git
a/src/site/antora/modules/ROOT/examples/manual/configuration/monitor-resources.yaml
b/src/site/antora/modules/ROOT/examples/manual/configuration/monitor-resources.yaml
new file mode 100644
index 0000000000..11f804108b
--- /dev/null
+++
b/src/site/antora/modules/ROOT/examples/manual/configuration/monitor-resources.yaml
@@ -0,0 +1,22 @@
+#
+# 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:
+ monitorInterval: '30'
+ MonitorResources:
+ MonitorResource:
+ - uri: "file://path/to/external-file-1.txt"
+ - uri: "file://path/to/external-file-2.txt"
diff --git a/src/site/antora/modules/ROOT/pages/manual/configuration.adoc
b/src/site/antora/modules/ROOT/pages/manual/configuration.adoc
index 7a0e738e41..d7f90e6c20 100644
--- a/src/site/antora/modules/ROOT/pages/manual/configuration.adoc
+++ b/src/site/antora/modules/ROOT/pages/manual/configuration.adoc
@@ -988,6 +988,65 @@
include::example$manual/configuration/routing.properties[tag=appender]
Therefore, the dollar `$` sign needs to be escaped.
<2> All the attributes of children of the `Route` element have a **deferred**
evaluation. Therefore, they need only one `$` sign.
+[#monitorResources]
+== Monitor Resources
+
+Log4j can be configured to poll for changes to resources (in addition to the
configuration file) using the `MonitorResources` element of the configuration.
+If a change is detected in any of the specified resources, Log4j automatically
reconfigures the logger context.
+This feature helps with monitoring external resources (e.g., TLS certificates)
that the configuration is dependent on.
+
+The polling interval is determined by the value of the
<<configuration-attribute-monitorInterval>> attribute.
+If set to 0, polling is disabled.
+See <<configuration-attribute-monitorInterval>> for further details.
+
+A configuration can have either zero or one `MonitorResources` element at its
root.
+`MonitorResources` can have zero or more `MonitorResource` elements, which can
be configured following attributes:
+
+.`MonitorResource` configuration attributes
+[cols="1m,1,4"]
+|===
+| Attribute | Type | Description
+
+| `uri`
+| URI
+a| A
https://docs.oracle.com/javase/{java-target-version}/docs/api/java/net/URI.html[`java.net.URI`]
reference to the external resource.
+Note that only URIs of scheme `file` are accepted.
+|===
+
+See example below:
+
+.`MonitorResources` configuration example
+[tabs]
+====
+XML::
++
+[source,xml]
+----
+include::example$manual/configuration/monitor-resources.xml[lines=18..]
+----
+
+JSON::
++
+[source,json]
+----
+include::example$manual/configuration/monitor-resources.json[]
+----
+
+YAML::
++
+[source,yaml]
+----
+include::example$manual/configuration/monitor-resources.yaml[lines=17..]
+----
+
+Properties::
++
+[source,properties]
+----
+include::example$manual/configuration/monitor-resources.properties[lines=18..]
+----
+====
+
[id=arbiters]
== [[Arbiters]] Arbiters