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 00000000000..4481ec62643 --- /dev/null +++ b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/MonitorResourcesTest.java @@ -0,0 +1,123 @@ +/* + * 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; + +public class MonitorResourcesTest { + + @Test + void test_reconfiguration(@TempDir(cleanup = CleanupMode.ON_SUCCESS) final Path tempDir) throws IOException { + final ConfigurationBuilder<PropertiesConfiguration> configBuilder = + ConfigurationBuilderFactory.newConfigurationBuilder(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(); + + 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 42cdcb3071c..c1ede9cb447 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 @@ -45,6 +45,17 @@ void testConfigWaitStrategyFactory(final LoggerContext context) { 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 00000000000..e8a76f6fda5 --- /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 00000000000..26c566af41b --- /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 00000000000..dd772d9ff10 --- /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 00000000000..1529167d56f --- /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 00000000000..11f804108b7 --- /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 5b60a7727b3..41fad82cd3f 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 @@ -132,6 +132,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); @@ -267,6 +268,7 @@ public void initialize() { setup(); setupAdvertisement(); doConfigure(); + watchMonitorResources(); setState(State.INITIALIZED); LOGGER.debug("Configuration {} initialized", this); } @@ -322,10 +324,10 @@ public void start() { } 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(); } @@ -347,6 +349,21 @@ public void start() { 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; @@ -729,9 +746,16 @@ protected void doConfigure() { } 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 00000000000..235e8645aa8 --- /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 00000000000..f7cc5e20260 --- /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 3b9fc489614..9fb9a3f5def 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,15 @@ 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. + */ + 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 273906a5d48..6d91c2d89d8 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 f1ec35e06a0..8445dc706a4 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 BuiltConfiguration( customLevelsComponent = component; break; } + case "MonitorResources": { + monitorResourcesComponent = component; + break; + } + case "AsyncWaitStrategyFactory": { + asyncWaitStrategyFactoryComponent = component; + break; + } } } this.rootComponent = rootComponent; @@ -95,6 +105,13 @@ public void setup() { 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 633e619b281..bf039a2dafc 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.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 ConfigurationBuilder<T> add(final AppenderComponentBuilder builder) { 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 @@ private void writeXmlConfiguration(final XMLStreamWriter xmlWriter) throws XMLSt 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 @@ private void writeXmlAttributes(final XMLStreamWriter xmlWriter, final Component } } - @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 c56f92230f3..32a69487d75 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/package-info.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/config/package-info.java index 111d1644f68..3db1c7abd6b 100644 --- a/log4j-core/src/main/java/org/apache/logging/log4j/core/config/package-info.java +++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/config/package-info.java @@ -18,7 +18,7 @@ * Configuration of Log4j 2. */ @Export -@Version("2.24.1") +@Version("2.25.0") package org.apache.logging.log4j.core.config; 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 6dc9fd6957a..e4770a80ab3 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 PropertiesConfiguration build() { 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 @@ private LayoutComponentBuilder createLayout(final String appenderName, final Pro 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 00000000000..c32545a602a --- /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 00000000000..26c566af41b --- /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 00000000000..dd772d9ff10 --- /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 00000000000..1529167d56f --- /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 00000000000..11f804108b7 --- /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 7a0e738e41c..d7f90e6c209 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