Skip to content

Commit c299479

Browse files
vyMichaelMorrisEst
andauthored
Add MonitorResources configuration element (#3703)
Co-authored-by: MichaelMorris <[email protected]>
1 parent aa4ee5f commit c299479

File tree

22 files changed

+649
-7
lines changed

22 files changed

+649
-7
lines changed
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to you under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
package org.apache.logging.log4j.core;
18+
19+
import static org.assertj.core.api.Assertions.assertThat;
20+
import static org.awaitility.Awaitility.waitAtMost;
21+
22+
import java.io.IOException;
23+
import java.nio.file.Files;
24+
import java.nio.file.Path;
25+
import java.util.Arrays;
26+
import java.util.Collections;
27+
import java.util.LinkedHashSet;
28+
import java.util.Set;
29+
import java.util.concurrent.TimeUnit;
30+
import java.util.stream.Collectors;
31+
import org.apache.logging.log4j.core.config.Configuration;
32+
import org.apache.logging.log4j.core.config.ConfigurationSource;
33+
import org.apache.logging.log4j.core.config.Configurator;
34+
import org.apache.logging.log4j.core.config.builder.api.ComponentBuilder;
35+
import org.apache.logging.log4j.core.config.builder.api.ConfigurationBuilder;
36+
import org.apache.logging.log4j.core.config.builder.api.ConfigurationBuilderFactory;
37+
import org.apache.logging.log4j.core.config.properties.PropertiesConfiguration;
38+
import org.apache.logging.log4j.core.test.junit.LoggerContextSource;
39+
import org.apache.logging.log4j.core.util.Source;
40+
import org.junit.jupiter.api.Test;
41+
import org.junit.jupiter.api.io.CleanupMode;
42+
import org.junit.jupiter.api.io.TempDir;
43+
44+
class MonitorResourcesTest {
45+
46+
@Test
47+
void test_reconfiguration(@TempDir(cleanup = CleanupMode.ON_SUCCESS) final Path tempDir) throws IOException {
48+
final ConfigurationBuilder<?> configBuilder = ConfigurationBuilderFactory.newConfigurationBuilder(
49+
// Explicitly deviating from `BuiltConfiguration`, since it is not `Reconfigurable`, and this
50+
// results in `instanceof Reconfigurable` checks in `AbstractConfiguration` to fail while arming
51+
// the watcher. Hence, using `PropertiesConfiguration`, which is `Reconfigurable`, instead.
52+
PropertiesConfiguration.class);
53+
final Path configFile = tempDir.resolve("log4j.xml");
54+
final Path externalResourceFile1 = tempDir.resolve("external-resource-1.txt");
55+
final Path externalResourceFile2 = tempDir.resolve("external-resource-2.txt");
56+
final ConfigurationSource configSource = new ConfigurationSource(new Source(configFile), new byte[] {}, 0);
57+
final int monitorInterval = 3;
58+
59+
final ComponentBuilder<?> monitorResourcesComponent = configBuilder.newComponent("MonitorResources");
60+
monitorResourcesComponent.addComponent(configBuilder
61+
.newComponent("MonitorResource")
62+
.addAttribute("uri", externalResourceFile1.toUri().toString()));
63+
monitorResourcesComponent.addComponent(configBuilder
64+
.newComponent("MonitorResource")
65+
.addAttribute("uri", externalResourceFile2.toUri().toString()));
66+
67+
final Configuration config = configBuilder
68+
.setConfigurationSource(configSource)
69+
.setMonitorInterval(String.valueOf(monitorInterval))
70+
.addComponent(monitorResourcesComponent)
71+
.build(false);
72+
73+
try (final LoggerContext loggerContext = Configurator.initialize(config)) {
74+
assertMonitorResourceFileNames(
75+
loggerContext,
76+
configFile.getFileName().toString(),
77+
externalResourceFile1.getFileName().toString(),
78+
externalResourceFile2.getFileName().toString());
79+
Files.write(externalResourceFile2, Collections.singletonList("a change"));
80+
waitAtMost(2 * monitorInterval, TimeUnit.SECONDS).until(() -> loggerContext.getConfiguration() != config);
81+
}
82+
}
83+
84+
@Test
85+
@LoggerContextSource("config/MonitorResource/log4j.xml")
86+
void test_config_of_type_XML(final LoggerContext loggerContext) {
87+
assertMonitorResourceFileNames(loggerContext, "log4j.xml");
88+
}
89+
90+
@Test
91+
@LoggerContextSource("config/MonitorResource/log4j.json")
92+
void test_config_of_type_JSON(final LoggerContext loggerContext) {
93+
assertMonitorResourceFileNames(loggerContext, "log4j.json");
94+
}
95+
96+
@Test
97+
@LoggerContextSource("config/MonitorResource/log4j.yaml")
98+
void test_config_of_type_YAML(final LoggerContext loggerContext) {
99+
assertMonitorResourceFileNames(loggerContext, "log4j.yaml");
100+
}
101+
102+
@Test
103+
@LoggerContextSource("config/MonitorResource/log4j.properties")
104+
void test_config_of_type_properties(final LoggerContext loggerContext) {
105+
assertMonitorResourceFileNames(loggerContext, "log4j.properties");
106+
}
107+
108+
private static void assertMonitorResourceFileNames(final LoggerContext loggerContext, final String configFileName) {
109+
assertMonitorResourceFileNames(loggerContext, configFileName, "external-file-1.txt", "external-file-2.txt");
110+
}
111+
112+
private static void assertMonitorResourceFileNames(
113+
final LoggerContext loggerContext, final String configFileName, final String... externalResourceFileNames) {
114+
final Set<Source> sources = loggerContext
115+
.getConfiguration()
116+
.getWatchManager()
117+
.getConfigurationWatchers()
118+
.keySet();
119+
final Set<String> actualFileNames =
120+
sources.stream().map(source -> source.getFile().getName()).collect(Collectors.toSet());
121+
final Set<String> expectedFileNames = new LinkedHashSet<>();
122+
expectedFileNames.add(configFileName);
123+
expectedFileNames.addAll(Arrays.asList(externalResourceFileNames));
124+
assertThat(actualFileNames).as("watch manager sources: %s", sources).isEqualTo(expectedFileNames);
125+
}
126+
}

log4j-core-test/src/test/java/org/apache/logging/log4j/core/async/AsyncWaitStrategyFactoryConfigTest.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,17 @@ void testConfigWaitStrategyFactory(final LoggerContext context) {
4747
asyncWaitStrategyFactory instanceof YieldingWaitStrategyFactory);
4848
}
4949

50+
@Test
51+
@LoggerContextSource("AsyncWaitStrategyFactoryConfigTest.properties")
52+
void testConfigWaitStrategyFactoryFromProperties(final LoggerContext context) {
53+
final AsyncWaitStrategyFactory asyncWaitStrategyFactory =
54+
context.getConfiguration().getAsyncWaitStrategyFactory();
55+
assertEquals(YieldingWaitStrategyFactory.class, asyncWaitStrategyFactory.getClass());
56+
assertThat(
57+
"factory is YieldingWaitStrategyFactory",
58+
asyncWaitStrategyFactory instanceof YieldingWaitStrategyFactory);
59+
}
60+
5061
@Test
5162
@LoggerContextSource("AsyncWaitStrategyFactoryConfigTest.xml")
5263
void testWaitStrategy(final LoggerContext context) {
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
#
2+
# Licensed to the Apache Software Foundation (ASF) under one or more
3+
# contributor license agreements. See the NOTICE file distributed with
4+
# this work for additional information regarding copyright ownership.
5+
# The ASF licenses this file to you under the Apache License, Version 2.0
6+
# (the "License"); you may not use this file except in compliance with
7+
# the License. You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing, software
12+
# distributed under the License is distributed on an "AS IS" BASIS,
13+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
# See the License for the specific language governing permissions and
15+
# limitations under the License.
16+
#
17+
18+
strategy.type = AsyncWaitStrategyFactory
19+
strategy.class = org.apache.logging.log4j.core.async.AsyncWaitStrategyFactoryConfigTest$YieldingWaitStrategyFactory
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"Configuration": {
3+
"monitorInterval": "30",
4+
"MonitorResources": {
5+
"MonitorResource": [
6+
{
7+
"uri": "file://path/to/external-file-1.txt"
8+
},
9+
{
10+
"uri": "file://path/to/external-file-2.txt"
11+
}
12+
]
13+
}
14+
}
15+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
#
2+
# Licensed to the Apache Software Foundation (ASF) under one or more
3+
# contributor license agreements. See the NOTICE file distributed with
4+
# this work for additional information regarding copyright ownership.
5+
# The ASF licenses this file to you under the Apache License, Version 2.0
6+
# (the "License"); you may not use this file except in compliance with
7+
# the License. You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing, software
12+
# distributed under the License is distributed on an "AS IS" BASIS,
13+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
# See the License for the specific language governing permissions and
15+
# limitations under the License.
16+
#
17+
monitorInterval = 30
18+
monitorResources.type = MonitorResources
19+
monitorResources.0.type = MonitorResource
20+
monitorResources.0.uri = file://path/to/external-file-1.txt
21+
monitorResources.1.type = MonitorResource
22+
monitorResources.1.uri = file://path/to/external-file-2.txt
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!--
3+
~ Licensed to the Apache Software Foundation (ASF) under one or more
4+
~ contributor license agreements. See the NOTICE file distributed with
5+
~ this work for additional information regarding copyright ownership.
6+
~ The ASF licenses this file to you under the Apache License, Version 2.0
7+
~ (the "License"); you may not use this file except in compliance with
8+
~ the License. You may obtain a copy of the License at
9+
~
10+
~ http://www.apache.org/licenses/LICENSE-2.0
11+
~
12+
~ Unless required by applicable law or agreed to in writing, software
13+
~ distributed under the License is distributed on an "AS IS" BASIS,
14+
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
~ See the License for the specific language governing permissions and
16+
~ limitations under the License.
17+
-->
18+
<Configuration xmlns="https://logging.apache.org/xml/ns"
19+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
20+
xsi:schemaLocation="
21+
https://logging.apache.org/xml/ns
22+
https://logging.apache.org/xml/ns/log4j-config-2.xsd"
23+
monitorInterval="30">
24+
<MonitorResources>
25+
<MonitorResource uri="file://path/to/external-file-1.txt"/>
26+
<MonitorResource uri="file://path/to/external-file-2.txt"/>
27+
</MonitorResources>
28+
</Configuration>
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
#
2+
# Licensed to the Apache Software Foundation (ASF) under one or more
3+
# contributor license agreements. See the NOTICE file distributed with
4+
# this work for additional information regarding copyright ownership.
5+
# The ASF licenses this file to you under the Apache License, Version 2.0
6+
# (the "License"); you may not use this file except in compliance with
7+
# the License. You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing, software
12+
# distributed under the License is distributed on an "AS IS" BASIS,
13+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
# See the License for the specific language governing permissions and
15+
# limitations under the License.
16+
#
17+
Configuration:
18+
monitorInterval: '30'
19+
MonitorResources:
20+
MonitorResource:
21+
- uri: "file://path/to/external-file-1.txt"
22+
- uri: "file://path/to/external-file-2.txt"

log4j-core/src/main/java/org/apache/logging/log4j/core/config/AbstractConfiguration.java

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@ public abstract class AbstractConfiguration extends AbstractFilterable implement
133133
private ConcurrentMap<String, Appender> appenders = new ConcurrentHashMap<>();
134134
private ConcurrentMap<String, LoggerConfig> loggerConfigs = new ConcurrentHashMap<>();
135135
private List<CustomLevelConfig> customLevels = Collections.emptyList();
136+
private Set<MonitorResource> monitorResources = Collections.emptySet();
136137
private final ConcurrentMap<String, String> propertyMap = new ConcurrentHashMap<>();
137138
private final Interpolator tempLookup = new Interpolator(propertyMap);
138139
private final StrSubstitutor runtimeStrSubstitutor = new RuntimeStrSubstitutor(tempLookup);
@@ -268,6 +269,7 @@ public void initialize() {
268269
setup();
269270
setupAdvertisement();
270271
doConfigure();
272+
watchMonitorResources();
271273
setState(State.INITIALIZED);
272274
LOGGER.debug("Configuration {} initialized", this);
273275
}
@@ -323,10 +325,10 @@ public void start() {
323325
}
324326
LOGGER.info("Starting configuration {}...", this);
325327
this.setStarting();
326-
if (watchManager.getIntervalSeconds() >= 0) {
328+
if (isConfigurationMonitoringEnabled()) {
327329
LOGGER.info(
328330
"Start watching for changes to {} every {} seconds",
329-
getConfigurationSource(),
331+
watchManager.getConfigurationWatchers().keySet(),
330332
watchManager.getIntervalSeconds());
331333
watchManager.start();
332334
}
@@ -348,6 +350,21 @@ public void start() {
348350
LOGGER.info("Configuration {} started.", this);
349351
}
350352

353+
private boolean isConfigurationMonitoringEnabled() {
354+
return this instanceof Reconfigurable && watchManager.getIntervalSeconds() > 0;
355+
}
356+
357+
private void watchMonitorResources() {
358+
if (isConfigurationMonitoringEnabled()) {
359+
monitorResources.forEach(monitorResource -> {
360+
Source source = new Source(monitorResource.getUri());
361+
final ConfigurationFileWatcher watcher = new ConfigurationFileWatcher(
362+
this, (Reconfigurable) this, listeners, source.getFile().lastModified());
363+
watchManager.watch(source, watcher);
364+
});
365+
}
366+
}
367+
351368
private boolean hasAsyncLoggers() {
352369
if (root instanceof AsyncLoggerConfig) {
353370
return true;
@@ -730,9 +747,16 @@ protected void doConfigure() {
730747
} else if (child.isInstanceOf(AsyncWaitStrategyFactoryConfig.class)) {
731748
final AsyncWaitStrategyFactoryConfig awsfc = child.getObject(AsyncWaitStrategyFactoryConfig.class);
732749
asyncWaitStrategyFactory = awsfc.createWaitStrategyFactory();
750+
} else if (child.isInstanceOf(MonitorResources.class)) {
751+
monitorResources = child.getObject(MonitorResources.class).getResources();
733752
} else {
734753
final List<String> expected = Arrays.asList(
735-
"\"Appenders\"", "\"Loggers\"", "\"Properties\"", "\"Scripts\"", "\"CustomLevels\"");
754+
"\"Appenders\"",
755+
"\"Loggers\"",
756+
"\"Properties\"",
757+
"\"Scripts\"",
758+
"\"CustomLevels\"",
759+
"\"MonitorResources\"");
736760
LOGGER.error(
737761
"Unknown object \"{}\" of type {} is ignored: try nesting it inside one of: {}.",
738762
child.getName(),

0 commit comments

Comments
 (0)