diff --git a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/MonitorUriTest.java b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/MonitorUriTest.java new file mode 100644 index 00000000000..20c519afa55 --- /dev/null +++ b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/MonitorUriTest.java @@ -0,0 +1,61 @@ +/* + * 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.awaitility.Awaitility.waitAtMost; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Collections; +import java.util.concurrent.TimeUnit; +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.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.util.Source; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.CleanupMode; +import org.junit.jupiter.api.io.TempDir; + +public class MonitorUriTest { + + private static final int MONITOR_INTERVAL = 3; + + @Test + void testReconfigureOnChangeInMonitorUri(@TempDir(cleanup = CleanupMode.ON_SUCCESS) final Path tempDir) + throws IOException { + ConfigurationBuilder configBuilder = + ConfigurationBuilderFactory.newConfigurationBuilder(PropertiesConfiguration.class); + Path config = tempDir.resolve("config.xml"); + Path monitorUri = tempDir.resolve("monitorUri.xml"); + ConfigurationSource configSource = new ConfigurationSource(new Source(config), new byte[] {}, 0); + Configuration configuration = configBuilder + .setConfigurationSource(configSource) + .setMonitorInterval(String.valueOf(MONITOR_INTERVAL)) + .add(configBuilder.newMonitorUri(monitorUri.toUri().toString())) + .build(); + + try (LoggerContext loggerContext = Configurator.initialize(configuration)) { + Files.write(monitorUri, Collections.singletonList("a change")); + waitAtMost(MONITOR_INTERVAL + 2, TimeUnit.SECONDS) + .until(() -> loggerContext.getConfiguration() != configuration); + } + } +} 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..523a70da8ca 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 @@ -22,6 +22,7 @@ import java.io.InputStream; import java.lang.ref.WeakReference; import java.net.URI; +import java.net.URISyntaxException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -132,6 +133,7 @@ public abstract class AbstractConfiguration extends AbstractFilterable implement private ConcurrentMap appenders = new ConcurrentHashMap<>(); private ConcurrentMap loggerConfigs = new ConcurrentHashMap<>(); private List customLevels = Collections.emptyList(); + private List uris = Collections.emptyList(); private final ConcurrentMap propertyMap = new ConcurrentHashMap<>(); private final Interpolator tempLookup = new Interpolator(propertyMap); private final StrSubstitutor runtimeStrSubstitutor = new RuntimeStrSubstitutor(tempLookup); @@ -323,10 +325,19 @@ public void start() { LOGGER.info("Starting configuration {}...", this); this.setStarting(); if (watchManager.getIntervalSeconds() >= 0) { - LOGGER.info( - "Start watching for changes to {} every {} seconds", - getConfigurationSource(), - watchManager.getIntervalSeconds()); + if (uris != null && uris.size() > 0) { + LOGGER.info( + "Start watching for changes to {} and {} every {} seconds", + getConfigurationSource(), + uris, + watchManager.getIntervalSeconds()); + watchMonitorUris(); + } else { + LOGGER.info( + "Start watching for changes to {} every {} seconds", + getConfigurationSource(), + watchManager.getIntervalSeconds()); + } watchManager.start(); } if (hasAsyncLoggers()) { @@ -347,6 +358,17 @@ public void start() { LOGGER.info("Configuration {} started.", this); } + private void watchMonitorUris() { + if (this instanceof Reconfigurable) { + uris.stream().forEach(uri -> { + Source source = new Source(uri); + 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 +751,16 @@ protected void doConfigure() { } else if (child.isInstanceOf(AsyncWaitStrategyFactoryConfig.class)) { final AsyncWaitStrategyFactoryConfig awsfc = child.getObject(AsyncWaitStrategyFactoryConfig.class); asyncWaitStrategyFactory = awsfc.createWaitStrategyFactory(); + } else if (child.isInstanceOf(MonitorUris.class)) { + uris = convertToJavaNetUris(child.getObject(MonitorUris.class).getUris()); } else { final List expected = Arrays.asList( - "\"Appenders\"", "\"Loggers\"", "\"Properties\"", "\"Scripts\"", "\"CustomLevels\""); + "\"Appenders\"", + "\"Loggers\"", + "\"Properties\"", + "\"Scripts\"", + "\"CustomLevels\"", + "\"MonitorUris\""); LOGGER.error( "Unknown object \"{}\" of type {} is ignored: try nesting it inside one of: {}.", child.getName(), @@ -767,6 +796,18 @@ protected void doConfigure() { setParents(); } + private List convertToJavaNetUris(final List uris) { + final List javaNetUris = new ArrayList<>(); + for (Uri uri : uris) { + try { + javaNetUris.add(new URI(uri.getUri())); + } catch (URISyntaxException e) { + LOGGER.error("Error parsing monitor URI: " + uri, e); + } + } + return javaNetUris; + } + public static Level getDefaultLevel() { final String levelName = PropertiesUtil.getProperties() .getStringProperty(DefaultConfiguration.DEFAULT_LEVEL, Level.ERROR.name()); diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/config/MonitorUris.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/config/MonitorUris.java new file mode 100644 index 00000000000..d294e8fa083 --- /dev/null +++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/config/MonitorUris.java @@ -0,0 +1,58 @@ +/* + * 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 java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +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 MonitorUri objects. + */ +@Plugin(name = "MonitorUris", category = Core.CATEGORY_NAME, printObject = true) +public final class MonitorUris { + + private final List uris; + + private MonitorUris(final Uri[] uris) { + this.uris = new ArrayList<>(Arrays.asList(uris)); + } + + /** + * Create a MonitorUris object to contain all the URIs to be monitored. + * + * @param uris An array of URIs. + * @return A MonitorUris object. + */ + @PluginFactory + public static MonitorUris createMonitorUris( // + @PluginElement("Uris") final Uri[] uris) { + return new MonitorUris(uris == null ? Uri.EMPTY_ARRAY : uris); + } + + /** + * Returns a list of the {@code Uri} objects created during configuration. + * @return the URIs to be monitored + */ + public List getUris() { + return uris; + } +} diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/config/Uri.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/config/Uri.java new file mode 100644 index 00000000000..a07dc3e2a03 --- /dev/null +++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/config/Uri.java @@ -0,0 +1,88 @@ +/* + * 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 java.util.Objects; +import org.apache.logging.log4j.core.Core; +import org.apache.logging.log4j.core.config.plugins.Plugin; +import org.apache.logging.log4j.core.config.plugins.PluginFactory; +import org.apache.logging.log4j.core.config.plugins.PluginValue; +import org.apache.logging.log4j.status.StatusLogger; + +/** + * Descriptor of a URI object that is created via configuration. + */ +@Plugin(name = "Uri", category = Core.CATEGORY_NAME, printObject = true) +public final class Uri { + + /** + * The empty array. + */ + static final Uri[] EMPTY_ARRAY = {}; + + private final String uri; + + private Uri(final String uri) { + this.uri = Objects.requireNonNull(uri, "uri is null"); + } + + /** + * Creates a Uri object. + * + * @param uri the URI. + * @return A Uri object. + */ + @PluginFactory + public static Uri createUri( // @formatter:off + @PluginValue("uri") final String uri) { + // @formatter:on + + StatusLogger.getLogger().debug("Creating Uri('{}')", uri); + return new Uri(uri); + } + + /** + * Returns the URI. + * + * @return the URI + */ + public String 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 Uri)) { + return false; + } + final Uri other = (Uri) object; + return this.uri == other.uri; + } + + @Override + public String toString() { + return "Uri[" + uri + "]"; + } +} 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..badbd346cda 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 extends Builder add(CustomLevelComponentBuilder builder); + /** + * Adds a MonitorUri component. + * @param builder The MonitorUriComponentBuilder with all of its attributes set. + * @return this builder instance. + */ + default ConfigurationBuilder add(MonitorUriComponentBuilder builder) { + return this; + } + /** * Adds a Filter component. * @param builder the FilterComponentBuilder with all of its attributes and sub components set. @@ -272,6 +281,15 @@ public interface ConfigurationBuilder extends Builder {} 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..8cf02dde440 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,7 @@ public class BuiltConfiguration extends AbstractConfiguration { private Component propertiesComponent; private Component customLevelsComponent; private Component scriptsComponent; + private Component monitorUriComponent; private String contentType = "text"; public BuiltConfiguration( @@ -78,6 +79,10 @@ public BuiltConfiguration( customLevelsComponent = component; break; } + case "MonitorUris": { + monitorUriComponent = component; + break; + } } } this.rootComponent = rootComponent; @@ -95,6 +100,9 @@ public void setup() { if (customLevelsComponent.getComponents().size() > 0) { children.add(convertToNode(rootNode, customLevelsComponent)); } + if (monitorUriComponent.getComponents().size() > 0) { + children.add(convertToNode(rootNode, monitorUriComponent)); + } 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..1dc4a66ac1e 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 @@ -55,6 +55,7 @@ import org.apache.logging.log4j.core.config.builder.api.KeyValuePairComponentBuilder; import org.apache.logging.log4j.core.config.builder.api.LayoutComponentBuilder; import org.apache.logging.log4j.core.config.builder.api.LoggerComponentBuilder; +import org.apache.logging.log4j.core.config.builder.api.MonitorUriComponentBuilder; import org.apache.logging.log4j.core.config.builder.api.PropertyComponentBuilder; import org.apache.logging.log4j.core.config.builder.api.RootLoggerComponentBuilder; import org.apache.logging.log4j.core.config.builder.api.ScriptComponentBuilder; @@ -77,6 +78,7 @@ public class DefaultConfigurationBuilder implement private Component properties; private Component customLevels; private Component scripts; + private Component monitorUris; private final Class clazz; private ConfigurationSource source; private int monitorInterval; @@ -124,6 +126,8 @@ public DefaultConfigurationBuilder(final Class clazz) { components.add(appenders); loggers = new Component("Loggers"); components.add(loggers); + monitorUris = new Component("MonitorUris"); + components.add(monitorUris); } protected ConfigurationBuilder add(final Component parent, final ComponentBuilder builder) { @@ -136,6 +140,11 @@ public ConfigurationBuilder add(final AppenderComponentBuilder builder) { return add(appenders, builder); } + @Override + public ConfigurationBuilder add(final MonitorUriComponentBuilder builder) { + return add(monitorUris, builder); + } + @Override public ConfigurationBuilder add(final CustomLevelComponentBuilder builder) { return add(customLevels, builder); @@ -291,6 +300,7 @@ private void writeXmlConfiguration(final XMLStreamWriter xmlWriter) throws XMLSt writeXmlSection(xmlWriter, properties); writeXmlSection(xmlWriter, scripts); writeXmlSection(xmlWriter, customLevels); + writeXmlSection(xmlWriter, monitorUris); if (filters.getComponents().size() == 1) { writeXmlComponent(xmlWriter, filters.getComponents().get(0)); } else if (filters.getComponents().size() > 1) { @@ -337,7 +347,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); } @@ -453,6 +462,11 @@ public CustomLevelComponentBuilder newCustomLevel(final String name, final int l return new DefaultCustomLevelComponentBuilder(this, name, level); } + @Override + public MonitorUriComponentBuilder newMonitorUri(final String uri) { + return new DefaultMonitorUriComponentBuilder(this, uri); + } + @Override public FilterComponentBuilder newFilter( final String type, final Filter.Result onMatch, final Filter.Result onMismatch) { diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/config/builder/impl/DefaultMonitorUriComponentBuilder.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/config/builder/impl/DefaultMonitorUriComponentBuilder.java new file mode 100644 index 00000000000..21299003cf8 --- /dev/null +++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/config/builder/impl/DefaultMonitorUriComponentBuilder.java @@ -0,0 +1,33 @@ +/* + * 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.builder.impl; + +import org.apache.logging.log4j.core.config.Configuration; +import org.apache.logging.log4j.core.config.builder.api.MonitorUriComponentBuilder; + +/** + * + */ +class DefaultMonitorUriComponentBuilder extends DefaultComponentAndConfigurationBuilder + implements MonitorUriComponentBuilder { + + public DefaultMonitorUriComponentBuilder( + final DefaultConfigurationBuilder builder, final String uri) { + super(builder, "Uri"); + addAttribute("uri", uri); + } +} 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..8f7cb3e5b1b 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 @@ -35,6 +35,7 @@ import org.apache.logging.log4j.core.config.builder.api.LayoutComponentBuilder; import org.apache.logging.log4j.core.config.builder.api.LoggableComponentBuilder; import org.apache.logging.log4j.core.config.builder.api.LoggerComponentBuilder; +import org.apache.logging.log4j.core.config.builder.api.MonitorUriComponentBuilder; import org.apache.logging.log4j.core.config.builder.api.RootLoggerComponentBuilder; import org.apache.logging.log4j.core.config.builder.api.ScriptComponentBuilder; import org.apache.logging.log4j.core.config.builder.api.ScriptFileComponentBuilder; @@ -124,6 +125,12 @@ public PropertiesConfiguration build() { } } + final Map monitorUris = + PropertiesUtil.partitionOnCommonPrefixes(PropertiesUtil.extractSubset(rootProperties, "monitorUri")); + for (final Map.Entry entry : monitorUris.entrySet()) { + builder.add(createMonitorUri(entry.getKey().trim(), entry.getValue())); + } + final String filterProp = rootProperties.getProperty("filters"); if (filterProp != null) { final String[] filterNames = filterProp.split(","); @@ -250,6 +257,14 @@ private AppenderRefComponentBuilder createAppenderRef(final String key, final Pr return addFiltersToComponent(appenderRefBuilder, properties); } + private MonitorUriComponentBuilder createMonitorUri(final String key, final Properties properties) { + final String uri = (String) properties.remove("uri"); + if (Strings.isEmpty(uri)) { + throw new ConfigurationException("No uri attribute provided for MonitorUri " + key); + } + return builder.newMonitorUri(uri); + } + private LoggerComponentBuilder createLogger(final String key, final Properties properties) { final String levelAndRefs = properties.getProperty(""); final String name = (String) properties.remove(CONFIG_NAME); 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..dcc2d028def --- /dev/null +++ b/src/changelog/.2.x.x/3074_monitor_additional_files.xml @@ -0,0 +1,8 @@ + + + + Support configuration option to provide URIs to monitor in addition to the config file. +