processAttributes(final Node node, final Element ele
@Override
public String toString() {
- return getClass().getSimpleName() + "[location=" + getConfigurationSource() + "]";
+ return getClass().getSimpleName() + "[location=" + getConfigurationSource() + ", lastModified="
+ + Instant.ofEpochMilli(getConfigurationSource().getLastModified()) + "]";
}
/**
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/filter/MutableThreadContextMapFilter.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/filter/MutableThreadContextMapFilter.java
index 396033ed21c..f694a19b6fd 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/filter/MutableThreadContextMapFilter.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/filter/MutableThreadContextMapFilter.java
@@ -66,6 +66,8 @@
@PerformanceSensitive("allocation")
public class MutableThreadContextMapFilter extends AbstractFilter {
+ private static final String HTTP = "http";
+ private static final String HTTPS = "https";
private static final KeyValuePair[] EMPTY_ARRAY = {};
private volatile Filter filter;
@@ -376,29 +378,42 @@ public void run() {
final PropertyEnvironment properties = configuration.getEnvironment();
final SslConfiguration sslConfiguration = SslConfigurationFactory.getSslConfiguration(properties);
final ConfigResult result = getConfig(source, authorizationProvider, properties, sslConfiguration);
- if (result.status == Status.SUCCESS) {
- filter = ThreadContextMapFilter.newBuilder()
- .setPairs(result.pairs)
- .setOperator("or")
- .setOnMatch(getOnMatch())
- .setOnMismatch(getOnMismatch())
- .setContextDataInjector(configuration.getComponent(ContextDataInjector.KEY))
- .get();
- LOGGER.info("Filter configuration was updated: {}", filter.toString());
- for (FilterConfigUpdateListener listener : listeners) {
- listener.onEvent();
- }
- } else if (result.status == Status.NOT_FOUND) {
- if (!(filter instanceof NoOpFilter)) {
- LOGGER.info("Filter configuration was removed");
+ switch (result.status) {
+ case SUCCESS:
+ filter = ThreadContextMapFilter.newBuilder()
+ .setPairs(result.pairs)
+ .setOperator("or")
+ .setOnMatch(getOnMatch())
+ .setOnMismatch(getOnMismatch())
+ .setContextDataInjector(configuration.getComponent(ContextDataInjector.KEY))
+ .get();
+ LOGGER.info("MutableThreadContextMapFilter configuration was updated: {}", filter.toString());
+ break;
+ case NOT_FOUND:
+ if (!(filter instanceof NoOpFilter)) {
+ LOGGER.info("MutableThreadContextMapFilter configuration was removed");
+ filter = new NoOpFilter();
+ }
+ break;
+ case EMPTY:
+ LOGGER.debug("MutableThreadContextMapFilter configuration is empty");
filter = new NoOpFilter();
+ break;
+ }
+ switch (result.status) {
+ // These results cause changes in the filter
+ // We call the listeners
+ case SUCCESS:
+ case NOT_FOUND:
+ case EMPTY:
for (FilterConfigUpdateListener listener : listeners) {
listener.onEvent();
}
- }
- } else if (result.status == Status.EMPTY) {
- LOGGER.debug("Filter configuration is empty");
- filter = new NoOpFilter();
+ break;
+ // These results do no cause changes in the filter
+ case ERROR:
+ case NOT_MODIFIED:
+ break;
}
}
}
@@ -407,7 +422,7 @@ public void run() {
value = "PATH_TRAVERSAL_IN",
justification = "The location of the file comes from a configuration value.")
private static LastModifiedSource getSource(final String configLocation) {
- LastModifiedSource source = null;
+ LastModifiedSource source;
try {
final URI uri = new URI(configLocation);
if (uri.getScheme() != null) {
@@ -430,8 +445,9 @@ private static ConfigResult getConfig(
final File inputFile = source.getFile();
final ConfigResult configResult = new ConfigResult();
InputStream inputStream = null;
- HttpInputStreamUtil.Result result = null;
+ HttpInputStreamUtil.Result result;
final long lastModified = source.getLastModified();
+ URI uri = source.getURI();
try {
if (inputFile != null && inputFile.exists()) {
try {
@@ -446,7 +462,7 @@ private static ConfigResult getConfig(
} catch (Exception ex) {
result = new HttpInputStreamUtil.Result(Status.ERROR);
}
- } else if (source.getURI() != null) {
+ } else if (HTTP.equalsIgnoreCase(uri.getScheme()) || HTTPS.equalsIgnoreCase(uri.getScheme())) {
try {
result = HttpInputStreamUtil.getInputStream(source, props, authorizationProvider, sslConfiguration);
inputStream = result.getInputStream();
@@ -508,7 +524,7 @@ private static void parseJsonConfiguration(final InputStream inputStream, final
LOGGER.warn("Ignoring the value for {}, which is not an array: {}", key, jsonArray);
}
}
- if (pairs.size() > 0) {
+ if (!pairs.isEmpty()) {
configResult.pairs = pairs.toArray(EMPTY_ARRAY);
configResult.status = Status.SUCCESS;
} else {
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/util/Loader.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/Loader.java
index ed65209a13c..84667ce9483 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/util/Loader.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/Loader.java
@@ -24,6 +24,7 @@
import org.apache.logging.log4j.kit.env.PropertyEnvironment;
import org.apache.logging.log4j.status.StatusLogger;
import org.apache.logging.log4j.util.LoaderUtil;
+import org.jspecify.annotations.Nullable;
/**
* Load resources (or images) from various sources.
@@ -81,9 +82,9 @@ public static ClassLoader getThreadContextClassLoader() {
*
* @param resource The resource to load.
* @param defaultLoader The default ClassLoader.
- * @return A URL to the resource.
+ * @return A URL to the resource or {@code null}.
*/
- public static URL getResource(final String resource, final ClassLoader defaultLoader) {
+ public static @Nullable URL getResource(final String resource, final ClassLoader defaultLoader) {
try {
ClassLoader classLoader = getThreadContextClassLoader();
if (classLoader != null) {
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/util/Source.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/Source.java
index 327363d233d..b5a5cb6e2b8 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/util/Source.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/Source.java
@@ -16,6 +16,8 @@
*/
package org.apache.logging.log4j.core.util;
+import static java.util.Objects.requireNonNull;
+
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import java.io.File;
import java.io.IOException;
@@ -30,10 +32,13 @@
import org.apache.logging.log4j.core.config.ConfigurationSource;
import org.apache.logging.log4j.status.StatusLogger;
import org.apache.logging.log4j.util.Strings;
+import org.jspecify.annotations.NullMarked;
+import org.jspecify.annotations.Nullable;
/**
* Represents the source for the logging configuration as an immutable object.
*/
+@NullMarked
public class Source {
private static final Logger LOGGER = StatusLogger.getLogger();
@@ -45,9 +50,9 @@ private static String normalize(final File file) {
}
}
- private static File toFile(final Path path) {
+ private static @Nullable File toFile(Path path) {
try {
- return Objects.requireNonNull(path, "path").toFile();
+ return requireNonNull(path, "path").toFile();
} catch (final UnsupportedOperationException e) {
return null;
}
@@ -57,9 +62,9 @@ private static File toFile(final Path path) {
@SuppressFBWarnings(
value = "PATH_TRAVERSAL_IN",
justification = "The URI should be specified in a configuration file.")
- private static File toFile(final URI uri) {
+ private static @Nullable File toFile(URI uri) {
try {
- final String scheme = Objects.requireNonNull(uri, "uri").getScheme();
+ final String scheme = requireNonNull(uri, "uri").getScheme();
if (Strings.isBlank(scheme) || scheme.equals("file")) {
return new File(uri.getPath());
} else {
@@ -67,20 +72,20 @@ private static File toFile(final URI uri) {
return null;
}
} catch (final Exception e) {
- LOGGER.debug("uri is malformed: " + uri.toString());
+ LOGGER.debug("uri is malformed: " + uri);
return null;
}
}
private static URI toURI(final URL url) {
try {
- return Objects.requireNonNull(url, "url").toURI();
+ return requireNonNull(url, "url").toURI();
} catch (final URISyntaxException e) {
throw new IllegalArgumentException(e);
}
}
- private final File file;
+ private final @Nullable File file;
private final URI uri;
private final String location;
@@ -88,21 +93,23 @@ private static URI toURI(final URL url) {
* Constructs a Source from a ConfigurationSource.
*
* @param source The ConfigurationSource.
+ * @throws NullPointerException if {@code source} is {@code null}.
*/
public Source(final ConfigurationSource source) {
this.file = source.getFile();
- this.uri = source.getURI();
- this.location = source.getLocation();
+ this.uri = requireNonNull(source.getURI());
+ this.location = requireNonNull(source.getLocation());
}
/**
* Constructs a new {@code Source} with the specified file.
* file.
*
- * @param file the file where the input stream originated
+ * @param file the file where the input stream originated.
+ * @throws NullPointerException if {@code file} is {@code null}.
*/
public Source(final File file) {
- this.file = Objects.requireNonNull(file, "file");
+ this.file = requireNonNull(file, "file");
this.location = normalize(file);
this.uri = file.toURI();
}
@@ -111,9 +118,10 @@ public Source(final File file) {
* Constructs a new {@code Source} from the specified Path.
*
* @param path the Path where the input stream originated
+ * @throws NullPointerException if {@code path} is {@code null}.
*/
public Source(final Path path) {
- final Path normPath = Objects.requireNonNull(path, "path").normalize();
+ final Path normPath = requireNonNull(path, "path").normalize();
this.file = toFile(normPath);
this.uri = normPath.toUri();
this.location = normPath.toString();
@@ -123,9 +131,10 @@ public Source(final Path path) {
* Constructs a new {@code Source} from the specified URI.
*
* @param uri the URI where the input stream originated
+ * @throws NullPointerException if {@code uri} is {@code null}.
*/
public Source(final URI uri) {
- final URI normUri = Objects.requireNonNull(uri, "uri").normalize();
+ final URI normUri = requireNonNull(uri, "uri").normalize();
this.uri = normUri;
this.location = normUri.toString();
this.file = toFile(normUri);
@@ -135,6 +144,7 @@ public Source(final URI uri) {
* Constructs a new {@code Source} from the specified URL.
*
* @param url the URL where the input stream originated
+ * @throws NullPointerException if this URL is {@code null}.
* @throws IllegalArgumentException if this URL is not formatted strictly according to RFC2396 and cannot be
* converted to a URI.
*/
@@ -162,7 +172,7 @@ public boolean equals(final Object obj) {
*
* @return the configuration source file, or {@code null}
*/
- public File getFile() {
+ public @Nullable File getFile() {
return file;
}
@@ -185,7 +195,7 @@ public String getLocation() {
value = "PATH_TRAVERSAL_IN",
justification = "The `file`, `uri` and `location` fields come from Log4j properties.")
public Path getPath() {
- return file != null ? file.toPath() : uri != null ? Paths.get(uri) : Paths.get(location);
+ return file != null ? file.toPath() : Paths.get(uri);
}
/**
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/util/WatchManager.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/WatchManager.java
index 5118f1cb9c3..a7e242b6220 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/util/WatchManager.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/WatchManager.java
@@ -20,6 +20,7 @@
import aQute.bnd.annotation.Resolution;
import aQute.bnd.annotation.spi.ServiceConsumer;
import java.io.File;
+import java.time.Instant;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
@@ -53,8 +54,9 @@
public class WatchManager extends AbstractLifeCycle {
private static final class ConfigurationMonitor {
- private volatile long lastModifiedMillis;
private final Watcher watcher;
+ // Only used for logging
+ private volatile long lastModifiedMillis;
public ConfigurationMonitor(final long lastModifiedMillis, final Watcher watcher) {
this.watcher = watcher;
@@ -115,15 +117,11 @@ public void run() {
final ConfigurationMonitor monitor = entry.getValue();
if (monitor.getWatcher().isModified()) {
final long lastModified = monitor.getWatcher().getLastModified();
- if (logger.isInfoEnabled()) {
- logger.info(
- "Source '{}' was modified on {} ({}), previous modification was on {} ({})",
- source,
- millisToString(lastModified),
- lastModified,
- millisToString(monitor.lastModifiedMillis),
- monitor.lastModifiedMillis);
- }
+ logger.info(
+ "Configuration source at `{}` was modified on `{}`, previous modification was on `{}`",
+ () -> source,
+ () -> Instant.ofEpochMilli(lastModified),
+ () -> Instant.ofEpochMilli(monitor.lastModifiedMillis));
monitor.lastModifiedMillis = lastModified;
monitor.getWatcher().modified();
}
@@ -188,7 +186,7 @@ public int getIntervalSeconds() {
}
public boolean hasEventListeners() {
- return eventServiceList.size() > 0;
+ return !eventServiceList.isEmpty();
}
private String millisToString(final long millis) {
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/util/internal/HttpInputStreamUtil.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/internal/HttpInputStreamUtil.java
index b5961883287..10204e30f3a 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/util/internal/HttpInputStreamUtil.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/internal/HttpInputStreamUtil.java
@@ -16,17 +16,24 @@
*/
package org.apache.logging.log4j.core.util.internal;
+import static org.apache.logging.log4j.util.Strings.toRootUpperCase;
+
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
+import java.time.Instant;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.core.config.ConfigurationException;
import org.apache.logging.log4j.core.net.UrlConnectionFactory;
import org.apache.logging.log4j.core.net.ssl.SslConfiguration;
import org.apache.logging.log4j.core.util.AuthorizationProvider;
+import org.apache.logging.log4j.core.util.Source;
import org.apache.logging.log4j.kit.env.PropertyEnvironment;
import org.apache.logging.log4j.status.StatusLogger;
+import org.apache.logging.log4j.util.Supplier;
+import org.jspecify.annotations.NullMarked;
+import org.jspecify.annotations.Nullable;
/**
* Utility method for reading data from an HTTP InputStream.
@@ -36,9 +43,20 @@ public final class HttpInputStreamUtil {
private static final Logger LOGGER = StatusLogger.getLogger();
private static final int NOT_MODIFIED = 304;
private static final int NOT_AUTHORIZED = 401;
+ private static final int FORBIDDEN = 403;
private static final int NOT_FOUND = 404;
private static final int OK = 200;
+ /**
+ * Retrieves an HTTP resource if it has been modified.
+ *
+ * Side effects: if the request is successful, the last modified time of the {@code source}
+ * parameter is modified.
+ *
+ * @param source The location of the HTTP resource
+ * @param authorizationProvider The authentication data for the HTTP request
+ * @return A {@link Result} object containing the status code and body of the response
+ */
public static Result getInputStream(
final LastModifiedSource source,
final PropertyEnvironment props,
@@ -54,12 +72,16 @@ public static Result getInputStream(
final int code = connection.getResponseCode();
switch (code) {
case NOT_MODIFIED: {
- LOGGER.debug("Configuration not modified");
+ LOGGER.debug(
+ "{} resource {}: not modified since {}",
+ formatProtocol(source),
+ () -> source,
+ () -> Instant.ofEpochMilli(lastModified));
result.status = Status.NOT_MODIFIED;
return result;
}
case NOT_FOUND: {
- LOGGER.debug("Unable to access {}: Not Found", source.toString());
+ LOGGER.debug("{} resource {}: not found", formatProtocol(source), () -> source);
result.status = Status.NOT_FOUND;
return result;
}
@@ -67,60 +89,88 @@ public static Result getInputStream(
try (final InputStream is = connection.getInputStream()) {
source.setLastModified(connection.getLastModified());
LOGGER.debug(
- "Content was modified for {}. previous lastModified: {}, new lastModified: {}",
- source.toString(),
- lastModified,
- connection.getLastModified());
+ "{} resource {}: last modified on {}",
+ formatProtocol(source),
+ () -> source,
+ () -> Instant.ofEpochMilli(connection.getLastModified()));
result.status = Status.SUCCESS;
- result.inputStream = new ByteArrayInputStream(is.readAllBytes());
+ result.bytes = is.readAllBytes();
return result;
} catch (final IOException e) {
try (final InputStream es = connection.getErrorStream()) {
- LOGGER.info(
- "Error accessing configuration at {}: {}",
- source.toString(),
- es.readAllBytes());
+ if (LOGGER.isDebugEnabled()) {
+ LOGGER.debug(
+ "Error accessing {} resource at {}: {}",
+ formatProtocol(source).get(),
+ source,
+ es.readAllBytes(),
+ e);
+ }
} catch (final IOException ioe) {
- LOGGER.error(
- "Error accessing configuration at {}: {}", source.toString(), e.getMessage());
+ LOGGER.debug(
+ "Error accessing {} resource at {}",
+ formatProtocol(source),
+ () -> source,
+ () -> e);
}
- throw new ConfigurationException("Unable to access " + source.toString(), e);
+ throw new ConfigurationException("Unable to access " + source, e);
}
}
case NOT_AUTHORIZED: {
- throw new ConfigurationException("Authorization failed");
+ throw new ConfigurationException("Authentication required for " + source);
+ }
+ case FORBIDDEN: {
+ throw new ConfigurationException("Access denied to " + source);
}
default: {
if (code < 0) {
- LOGGER.info("Invalid response code returned");
+ LOGGER.debug("{} resource {}: invalid response code", formatProtocol(source), source);
} else {
- LOGGER.info("Unexpected response code returned {}", code);
+ LOGGER.debug(
+ "{} resource {}: unexpected response code {}",
+ formatProtocol(source),
+ source,
+ code);
}
- throw new ConfigurationException("Unable to access " + source.toString());
+ throw new ConfigurationException("Unable to access " + source);
}
}
} finally {
connection.disconnect();
}
} catch (IOException e) {
- LOGGER.warn("Error accessing {}: {}", source.toString(), e.getMessage());
- throw new ConfigurationException("Unable to access " + source.toString(), e);
+ LOGGER.debug("Error accessing {} resource at {}", formatProtocol(source), source, e);
+ throw new ConfigurationException("Unable to access " + source, e);
}
}
+ private static Supplier formatProtocol(Source source) {
+ return () -> toRootUpperCase(source.getURI().getScheme());
+ }
+
+ @NullMarked
public static class Result {
- private InputStream inputStream;
+ private byte @Nullable [] bytes = null;
private Status status;
- public Result() {}
+ public Result() {
+ this(Status.ERROR);
+ }
public Result(final Status status) {
this.status = status;
}
- public InputStream getInputStream() {
- return inputStream;
+ /**
+ * Returns the data if the status is {@link Status#SUCCESS}.
+ *
+ * In any other case the result is {@code null}.
+ *
+ * @return The contents of the HTTP response or null if empty.
+ */
+ public @Nullable InputStream getInputStream() {
+ return bytes != null ? new ByteArrayInputStream(bytes) : null;
}
public Status getStatus() {
diff --git a/log4j-parent/pom.xml b/log4j-parent/pom.xml
index 2cce92c1fbf..e189f1a2de6 100644
--- a/log4j-parent/pom.xml
+++ b/log4j-parent/pom.xml
@@ -117,7 +117,6 @@
2.0.1
3.3.4
4.0.5
- 9.4.55.v20240627
3.5.12
1.37
3.4.1
@@ -140,11 +139,8 @@
1.0.1
4.13.5
3.5.1
- 2.0.15
2.1.7
- 10.1.30
1.7
- 2.35.2
2.10.0
-
- com.github.tomakehurst
- wiremock-jre8
- ${wiremock.version}
-
-
org.xmlunit
xmlunit-core
@@ -750,7 +719,8 @@
org.slf4j:jcl-over-slf4j
org.springframework:spring-jcl
-
+
+ log4j:log4j
org.slf4j:log4j-over-slf4j
ch.qos.reload4j:reload4j
diff --git a/log4j-perf-test/pom.xml b/log4j-perf-test/pom.xml
index aadc3c8933d..d6cd2076e14 100644
--- a/log4j-perf-test/pom.xml
+++ b/log4j-perf-test/pom.xml
@@ -37,8 +37,30 @@
true
true
org.apache.logging.log4j.perf
+
+
+ 1.2.25
+ 2.0.16
+
+
+
+
+ org.slf4j
+ slf4j-api
+ ${slf4j2.version}
+
+
+
+ ch.qos.reload4j
+ reload4j
+ ${reload4j.version}
+
+
+
+
+
org.openjdk.jmh
@@ -108,10 +130,6 @@
org.openjdk.jmh
jmh-core
-
- log4j
- log4j
-
ch.qos.logback
logback-classic
@@ -122,6 +140,10 @@
logback-core
compile
+
+ ch.qos.reload4j
+ reload4j
+
org.slf4j
slf4j-api
@@ -145,7 +167,7 @@
org.apache.maven.plugins
@@ -158,6 +180,7 @@
ch.qos.logback:*
+ ch.qos.reload4j:reload4j
diff --git a/log4j-plugins/pom.xml b/log4j-plugins/pom.xml
index ac88ccb6806..379bf796562 100644
--- a/log4j-plugins/pom.xml
+++ b/log4j-plugins/pom.xml
@@ -44,14 +44,17 @@
+
org.apache.logging.log4j
log4j-api
+
org.apache.logging.log4j
log4j-kit
+
diff --git a/log4j-slf4j2-impl/pom.xml b/log4j-slf4j2-impl/pom.xml
index 7019645401f..b48e3f50feb 100644
--- a/log4j-slf4j2-impl/pom.xml
+++ b/log4j-slf4j2-impl/pom.xml
@@ -36,17 +36,19 @@
(Refer to the `log4j-to-slf4j` artifact for forwarding the Log4j API to SLF4J.)
-
-
- false
-
-
- org.slf4j;substitute="slf4j-api"
-
+ 2.0.16
+
+
+
+
+ org.slf4j
+ slf4j-api
+ ${slf4j2.version}
+
+
+
+
org.osgi
diff --git a/log4j-to-slf4j/pom.xml b/log4j-to-slf4j/pom.xml
index 413fde7a59d..2240022d4cb 100644
--- a/log4j-to-slf4j/pom.xml
+++ b/log4j-to-slf4j/pom.xml
@@ -49,7 +49,20 @@
org.jspecify;transitive=false
+
+ 2.0.16
+
+
+
+
+ org.slf4j
+ slf4j-api
+ ${slf4j2.version}
+
+
+
+
org.osgi
diff --git a/pom.xml b/pom.xml
index dc412611b06..5b4e43691f2 100644
--- a/pom.xml
+++ b/pom.xml
@@ -374,6 +374,10 @@
+
+
org.apache.logging.log4j
log4j-api
diff --git a/src/changelog/.3.x.x/2937-http-watcher.xml b/src/changelog/.3.x.x/2937-http-watcher.xml
new file mode 100644
index 00000000000..ec3e1a14263
--- /dev/null
+++ b/src/changelog/.3.x.x/2937-http-watcher.xml
@@ -0,0 +1,8 @@
+
+
+
+ Fix reloading of the configuration from an HTTP(S) source
+