This is an automated email from the ASF dual-hosted git repository.
rgoers pushed a commit to branch release-2.x
in repository https://gitbox.apache.org/repos/asf/logging-log4j2.git
The following commit(s) were added to refs/heads/release-2.x by this push:
new ad414c1ac0 LOG4J2-3495, LOG4J2-3481, LOG4J2-3482 Add
MutableThreadContextMapFilter. Pass Auth provider when creating a connection.
ad414c1ac0 is described below
commit ad414c1ac0ae6e4d488eefb1a56c2842f48f248a
Author: Ralph Goers <[email protected]>
AuthorDate: Mon May 2 14:51:56 2022 +0200
LOG4J2-3495, LOG4J2-3481, LOG4J2-3482 Add MutableThreadContextMapFilter.
Pass Auth provider when creating a connection.
---
.../logging/log4j/core/config/HttpWatcher.java | 104 +++---
.../core/filter/MutableThreadContextMapFilter.java | 399 +++++++++++++++++++++
.../core/filter/mutable/KeyValuePairConfig.java | 46 +++
.../log4j/core/net/UrlConnectionFactory.java | 80 +++--
.../core/util/internal/HttpInputStreamUtil.java | 135 +++++++
.../core/util/internal/LastModifiedSource.java | 51 +++
.../logging/log4j/core/util/internal/Status.java | 25 ++
.../filter/HttpThreadContextMapFilterTest.java | 198 ++++++++++
.../filter/MutableThreadContextMapFilterTest.java | 98 +++++
.../log4j/core/net/UrlConnectionFactoryTest.java | 25 +-
log4j-core/src/test/resources/emptyConfig.json | 4 +
log4j-core/src/test/resources/filterConfig.json | 6 +
.../src/test/resources/log4j2-mutableFilter.xml | 37 ++
.../boot/Log4j2CloudConfigLoggingSystem.java | 20 +-
src/changes/changes.xml | 11 +-
src/site/xdoc/manual/configuration.xml.vm | 10 +-
src/site/xdoc/manual/filters.xml | 82 +++++
17 files changed, 1204 insertions(+), 127 deletions(-)
diff --git
a/log4j-core/src/main/java/org/apache/logging/log4j/core/config/HttpWatcher.java
b/log4j-core/src/main/java/org/apache/logging/log4j/core/config/HttpWatcher.java
index 72f3128f4d..3429b0949a 100644
---
a/log4j-core/src/main/java/org/apache/logging/log4j/core/config/HttpWatcher.java
+++
b/log4j-core/src/main/java/org/apache/logging/log4j/core/config/HttpWatcher.java
@@ -21,6 +21,7 @@ import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
+import java.net.URISyntaxException;
import java.net.URL;
import java.util.List;
@@ -31,9 +32,13 @@ import
org.apache.logging.log4j.core.net.UrlConnectionFactory;
import org.apache.logging.log4j.core.net.ssl.SslConfiguration;
import org.apache.logging.log4j.core.net.ssl.SslConfigurationFactory;
import org.apache.logging.log4j.core.util.AbstractWatcher;
+import org.apache.logging.log4j.core.util.AuthorizationProvider;
import org.apache.logging.log4j.core.util.Source;
import org.apache.logging.log4j.core.util.Watcher;
+import org.apache.logging.log4j.core.util.internal.HttpInputStreamUtil;
+import org.apache.logging.log4j.core.util.internal.LastModifiedSource;
import org.apache.logging.log4j.status.StatusLogger;
+import org.apache.logging.log4j.util.PropertiesUtil;
/**
*
@@ -42,19 +47,17 @@ import org.apache.logging.log4j.status.StatusLogger;
@PluginAliases("https")
public class HttpWatcher extends AbstractWatcher {
- private Logger LOGGER = StatusLogger.getLogger();
+ private final Logger LOGGER = StatusLogger.getLogger();
- private SslConfiguration sslConfiguration;
+ private final SslConfiguration sslConfiguration;
+ private AuthorizationProvider authorizationProvider;
private URL url;
private volatile long lastModifiedMillis;
- private static final int NOT_MODIFIED = 304;
- private static final int OK = 200;
- private static final int BUF_SIZE = 1024;
private static final String HTTP = "http";
private static final String HTTPS = "https";
public HttpWatcher(final Configuration configuration, final Reconfigurable
reconfigurable,
- final List<ConfigurationListener> configurationListeners, long
lastModifiedMillis) {
+ final List<ConfigurationListener> configurationListeners, final long
lastModifiedMillis) {
super(configuration, reconfigurable, configurationListeners);
sslConfiguration = SslConfigurationFactory.getSslConfiguration();
this.lastModifiedMillis = lastModifiedMillis;
@@ -71,23 +74,24 @@ public class HttpWatcher extends AbstractWatcher {
}
@Override
- public void watching(Source source) {
+ public void watching(final Source source) {
if (!source.getURI().getScheme().equals(HTTP) &&
!source.getURI().getScheme().equals(HTTPS)) {
throw new IllegalArgumentException(
"HttpWatcher requires a url using the HTTP or HTTPS protocol,
not " + source.getURI().getScheme());
}
try {
url = source.getURI().toURL();
- } catch (MalformedURLException ex) {
+ authorizationProvider =
ConfigurationFactory.authorizationProvider(PropertiesUtil.getProperties());
+ } catch (final MalformedURLException ex) {
throw new IllegalArgumentException("Invalid URL for HttpWatcher "
+ source.getURI(), ex);
}
super.watching(source);
}
@Override
- public Watcher newWatcher(Reconfigurable reconfigurable,
List<ConfigurationListener> listeners,
- long lastModifiedMillis) {
- HttpWatcher watcher = new HttpWatcher(getConfiguration(),
reconfigurable, listeners, lastModifiedMillis);
+ public Watcher newWatcher(final Reconfigurable reconfigurable, final
List<ConfigurationListener> listeners,
+ final long lastModifiedMillis) {
+ final HttpWatcher watcher = new HttpWatcher(getConfiguration(),
reconfigurable, listeners, lastModifiedMillis);
if (getSource() != null) {
watcher.watching(getSource());
}
@@ -96,61 +100,37 @@ public class HttpWatcher extends AbstractWatcher {
private boolean refreshConfiguration() {
try {
- final HttpURLConnection urlConnection =
UrlConnectionFactory.createConnection(url, lastModifiedMillis,
- sslConfiguration);
- urlConnection.connect();
-
- try {
- int code = urlConnection.getResponseCode();
- switch (code) {
- case NOT_MODIFIED: {
- LOGGER.debug("Configuration Not Modified");
- return false;
- }
- case OK: {
- try (InputStream is = urlConnection.getInputStream()) {
- ConfigurationSource configSource =
getConfiguration().getConfigurationSource();
- configSource.setData(readStream(is));
- lastModifiedMillis =
urlConnection.getLastModified();
- configSource.setModifiedMillis(lastModifiedMillis);
- LOGGER.debug("Content was modified for {}",
url.toString());
- return true;
- } catch (final IOException e) {
- try (InputStream es =
urlConnection.getErrorStream()) {
- LOGGER.info("Error accessing configuration at
{}: {}", url, readStream(es));
- } catch (final IOException ioe) {
- LOGGER.error("Error accessing configuration at
{}: {}", url, e.getMessage());
- }
- return false;
- }
- }
- default: {
- if (code < 0) {
- LOGGER.info("Invalid response code returned");
- } else {
- LOGGER.info("Unexpected response code returned
{}", code);
- }
+ final LastModifiedSource source = new
LastModifiedSource(url.toURI(), lastModifiedMillis);
+ final HttpInputStreamUtil.Result result =
HttpInputStreamUtil.getInputStream(source, authorizationProvider);
+ switch (result.getStatus()) {
+ case NOT_MODIFIED: {
+ LOGGER.debug("Configuration Not Modified");
+ return false;
+ }
+ case SUCCESS: {
+ final ConfigurationSource configSource =
getConfiguration().getConfigurationSource();
+ try {
+
configSource.setData(HttpInputStreamUtil.readStream(result.getInputStream()));
+
configSource.setModifiedMillis(source.getLastModified());
+ LOGGER.debug("Content was modified for {}",
url.toString());
+ return true;
+ } catch (final IOException e) {
+ LOGGER.error("Error accessing configuration at {}:
{}", url, e.getMessage());
return false;
}
}
- } catch (final IOException ioe) {
- LOGGER.error("Error accessing configuration at {}: {}", url,
ioe.getMessage());
- } finally {
- urlConnection.disconnect();
+ case NOT_FOUND: {
+ LOGGER.info("Unable to locate configuration at {}",
url.toString());
+ return false;
+ }
+ default: {
+ LOGGER.warn("Unexpected error accessing configuration at
{}", url.toString());
+ return false;
+ }
}
- } catch (final IOException ioe) {
- LOGGER.error("Error connecting to configuration at {}: {}", url,
ioe.getMessage());
- }
- return false;
- }
-
- private byte[] readStream(InputStream is) throws IOException {
- ByteArrayOutputStream result = new ByteArrayOutputStream();
- byte[] buffer = new byte[BUF_SIZE];
- int length;
- while ((length = is.read(buffer)) != -1) {
- result.write(buffer, 0, length);
+ } catch(final URISyntaxException ex) {
+ LOGGER.error("Bad configuration URL: {}, {}", url.toString(),
ex.getMessage());
+ return false;
}
- return result.toByteArray();
}
}
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
new file mode 100644
index 0000000000..ff0b5c2d34
--- /dev/null
+++
b/log4j-core/src/main/java/org/apache/logging/log4j/core/filter/MutableThreadContextMapFilter.java
@@ -0,0 +1,399 @@
+/*
+ * 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.filter;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.InputStream;
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.logging.log4j.Level;
+import org.apache.logging.log4j.Marker;
+import org.apache.logging.log4j.core.Filter;
+import org.apache.logging.log4j.core.LogEvent;
+import org.apache.logging.log4j.core.Logger;
+import org.apache.logging.log4j.core.config.Configuration;
+import org.apache.logging.log4j.core.config.ConfigurationException;
+import org.apache.logging.log4j.core.config.ConfigurationFactory;
+import org.apache.logging.log4j.core.config.ConfigurationScheduler;
+import org.apache.logging.log4j.core.config.Node;
+import org.apache.logging.log4j.core.config.plugins.Plugin;
+import org.apache.logging.log4j.core.config.plugins.PluginAliases;
+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.PluginConfiguration;
+import org.apache.logging.log4j.core.filter.mutable.KeyValuePairConfig;
+import org.apache.logging.log4j.core.util.AuthorizationProvider;
+import org.apache.logging.log4j.core.util.KeyValuePair;
+import org.apache.logging.log4j.core.util.internal.HttpInputStreamUtil;
+import org.apache.logging.log4j.core.util.internal.LastModifiedSource;
+import org.apache.logging.log4j.core.util.internal.Status;
+import org.apache.logging.log4j.message.Message;
+import org.apache.logging.log4j.util.PerformanceSensitive;
+import org.apache.logging.log4j.util.PropertiesUtil;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+/**
+ * Filter based on a value in the Thread Context Map (MDC).
+ */
+@Plugin(name = "MutableThreadContextMapFilter", category = Node.CATEGORY,
elementType = Filter.ELEMENT_TYPE, printObject = true)
+@PluginAliases("MutableContextMapFilter")
+@PerformanceSensitive("allocation")
+public class MutableThreadContextMapFilter extends AbstractFilter {
+
+ private static final ObjectMapper MAPPER = new ObjectMapper();
+
+ private volatile Filter filter;
+ private final long pollInterval;
+ private final ConfigurationScheduler scheduler;
+ private final LastModifiedSource source;
+ private final AuthorizationProvider authorizationProvider;
+ private final List<FilterConfigUpdateListener> listeners = new
ArrayList<>();
+ private ScheduledFuture<?> future = null;
+
+ private MutableThreadContextMapFilter(final Filter filter, final
LastModifiedSource source,
+ final long pollInterval, final AuthorizationProvider
authorizationProvider,
+ final Result onMatch, final Result onMismatch, final Configuration
configuration) {
+ super(onMatch, onMismatch);
+ this.filter = filter;
+ this.pollInterval = pollInterval;
+ this.source = source;
+ this.scheduler = configuration.getScheduler();
+ this.authorizationProvider = authorizationProvider;
+ }
+
+ @Override
+ public void start() {
+
+ if (pollInterval > 0) {
+ future = scheduler.scheduleWithFixedDelay(new FileMonitor(), 0,
pollInterval, TimeUnit.SECONDS);
+ LOGGER.debug("Watching {} with poll interval {}",
source.toString(), pollInterval);
+ }
+ super.start();
+ }
+
+ @Override
+ public boolean stop(long timeout, TimeUnit timeUnit) {
+ future.cancel(true);
+ return super.stop(timeout, timeUnit);
+ }
+
+ public void registerListener(FilterConfigUpdateListener listener) {
+ listeners.add(listener);
+ }
+
+ @PluginBuilderFactory
+ public static Builder newBuilder() {
+ return new Builder();
+ }
+
+ @Override
+ public Result filter(LogEvent event) {
+ return filter.filter(event);
+ }
+
+ @Override
+ public Result filter(Logger logger, Level level, Marker marker, Message
msg, Throwable t) {
+ return filter.filter(logger, level, marker, msg, t);
+ }
+
+ @Override
+ public Result filter(Logger logger, Level level, Marker marker, Object
msg, Throwable t) {
+ return filter.filter(logger, level, marker, msg, t);
+ }
+
+ @Override
+ public Result filter(Logger logger, Level level, Marker marker, String
msg, Object... params) {
+ return filter.filter(logger, level, marker, msg, params);
+ }
+
+ @Override
+ public Result filter(Logger logger, Level level, Marker marker, String
msg, Object p0) {
+ return filter.filter(logger, level, marker, msg, p0);
+ }
+
+ @Override
+ public Result filter(Logger logger, Level level, Marker marker, String
msg, Object p0,
+ Object p1) {
+ return filter.filter(logger, level, marker, msg, p0, p1);
+ }
+
+ @Override
+ public Result filter(Logger logger, Level level, Marker marker, String
msg, Object p0,
+ Object p1,
+ Object p2) {
+ return filter.filter(logger, level, marker, msg, p0, p1, p2);
+ }
+
+ @Override
+ public Result filter(Logger logger, Level level, Marker marker, String
msg, Object p0,
+ Object p1,
+ Object p2, Object p3) {
+ return filter.filter(logger, level, marker, msg, p0, p1, p2, p3);
+ }
+
+ @Override
+ public Result filter(Logger logger, Level level, Marker marker, String
msg, Object p0,
+ Object p1,
+ Object p2, Object p3, Object p4) {
+ return filter.filter(logger, level, marker, msg, p0, p1, p2, p3, p4);
+ }
+
+ @Override
+ public Result filter(Logger logger, Level level, Marker marker, String
msg, Object p0,
+ Object p1,
+ Object p2, Object p3, Object p4, Object p5) {
+ return filter.filter(logger, level, marker, msg, p0, p1, p2, p3, p4,
p5);
+ }
+
+ @Override
+ public Result filter(Logger logger, Level level, Marker marker, String
msg, Object p0,
+ Object p1,
+ Object p2, Object p3, Object p4, Object p5, Object p6) {
+ return filter.filter(logger, level, marker, msg, p0, p1, p2, p3, p4,
p5, p6);
+ }
+
+ @Override
+ public Result filter(Logger logger, Level level, Marker marker, String
msg, Object p0,
+ Object p1,
+ Object p2, Object p3, Object p4, Object p5, Object p6, Object p7) {
+ return filter.filter(logger, level, marker, msg, p0, p1, p2, p3, p4,
p5, p6, p7);
+ }
+
+ @Override
+ public Result filter(Logger logger, Level level, Marker marker, String
msg, Object p0,
+ Object p1,
+ Object p2, Object p3, Object p4, Object p5, Object p6, Object p7,
Object p8) {
+ return filter.filter(logger, level, marker, msg, p0, p1, p2, p3, p4,
p5, p6, p7, p8);
+ }
+
+ @Override
+ public Result filter(Logger logger, Level level, Marker marker, String
msg, Object p0,
+ Object p1,
+ Object p2, Object p3, Object p4, Object p5, Object p6, Object p7,
Object p8, Object p9) {
+ return filter.filter(logger, level, marker, msg, p0, p1, p2, p3, p4,
p5, p6, p7, p8, p9);
+ }
+
+ public static class Builder extends AbstractFilterBuilder<Builder>
+ implements
org.apache.logging.log4j.core.util.Builder<MutableThreadContextMapFilter> {
+ @PluginBuilderAttribute
+ private String configLocation;
+
+ @PluginBuilderAttribute
+ private long pollInterval;
+
+ @PluginConfiguration
+ private Configuration configuration;
+
+ /**
+ * Sets the Configuration.
+ * @param configuration The Configuration.
+ * @return this.
+ */
+ public Builder setConfiguration(final Configuration configuration) {
+ this.configuration = configuration;
+ return this;
+ }
+
+ /**
+ * Set the frequency in seconds that changes to the list a
ThreadContext valudes should be
+ * checked.
+ * @param pollInterval interval in seconds to check the file for
changes.
+ * @return this.
+ */
+ public Builder setPollInterval(final long pollInterval) {
+ this.pollInterval = pollInterval;
+ return this;
+ }
+
+ /**
+ * Sets the configuration to use.
+ * @param configLocation the location of the configuration.
+ * @return this
+ */
+ public Builder setConfigLocation(final String configLocation) {
+ this.configLocation = configLocation;
+ return this;
+ }
+
+ @Override
+ public MutableThreadContextMapFilter build() {
+ final LastModifiedSource source = getSource(configLocation);
+ if (source == null) {
+ return new MutableThreadContextMapFilter(new NoOpFilter(),
null, 0,
+ null, getOnMatch(), getOnMismatch(), configuration);
+ }
+ final AuthorizationProvider authorizationProvider =
+
ConfigurationFactory.authorizationProvider(PropertiesUtil.getProperties());
+ final ConfigResult result = getConfig(source,
authorizationProvider);
+ Filter filter;
+ if (result.status == Status.SUCCESS) {
+ if (result.pairs.length > 0) {
+ filter = ThreadContextMapFilter.createFilter(result.pairs,
"or",
+ getOnMatch(), getOnMismatch());
+ } else {
+ filter = new NoOpFilter();
+ }
+ } else if (result.status == Status.NOT_FOUND || result.status ==
Status.EMPTY) {
+ filter = new NoOpFilter();
+ } else {
+ LOGGER.warn("Unexpected response returned on initial call:
{}", result.status);
+ filter = new NoOpFilter();
+ }
+
+ if (pollInterval > 0) {
+ configuration.getScheduler().incrementScheduledItems();
+ }
+ return new MutableThreadContextMapFilter(filter, source,
pollInterval, authorizationProvider,
+ getOnMatch(), getOnMismatch(), configuration);
+ }
+ }
+
+ private class FileMonitor implements Runnable {
+
+ @Override
+ public void run() {
+ final ConfigResult result = getConfig(source,
authorizationProvider);
+ if (result.status == Status.SUCCESS) {
+ filter = ThreadContextMapFilter.createFilter(result.pairs,
"or", getOnMatch(), getOnMismatch());
+ 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");
+ filter = new NoOpFilter();
+ for (FilterConfigUpdateListener listener : listeners) {
+ listener.onEvent();
+ }
+ }
+ } else if (result.status == Status.EMPTY) {
+ LOGGER.debug("Filter configuration is empty");
+ filter = new NoOpFilter();
+ }
+ }
+ }
+
+ private static LastModifiedSource getSource(final String configLocation) {
+ LastModifiedSource source = null;
+ try {
+ final URI uri = new URI(configLocation);
+ if (uri.getScheme() != null) {
+ source = new LastModifiedSource(new URI(configLocation));
+ } else {
+ source = new LastModifiedSource(new File(configLocation));
+ }
+
+ } catch (Exception ex) {
+ source = new LastModifiedSource(new File(configLocation));
+ }
+ return source;
+ }
+
+ private static ConfigResult getConfig(final LastModifiedSource source,
+ final AuthorizationProvider authorizationProvider) {
+ final File inputFile = source.getFile();
+ InputStream inputStream = null;
+ HttpInputStreamUtil.Result result = null;
+ final long lastModified = source.getLastModified();
+ if (inputFile != null && inputFile.exists()) {
+ try {
+ final long modified = inputFile.lastModified();
+ if (modified > lastModified) {
+ source.setLastModified(modified);
+ inputStream = new FileInputStream(inputFile);
+ result = new HttpInputStreamUtil.Result(Status.SUCCESS);
+ } else {
+ result = new
HttpInputStreamUtil.Result(Status.NOT_MODIFIED);
+ }
+ } catch (Exception ex) {
+ result = new HttpInputStreamUtil.Result(Status.ERROR);
+ }
+ } else if (source.getURI() != null) {
+ try {
+ result = HttpInputStreamUtil.getInputStream(source,
authorizationProvider);
+ inputStream = result.getInputStream();
+ } catch (ConfigurationException ex) {
+ result = new HttpInputStreamUtil.Result(Status.ERROR);
+ }
+ } else {
+ result = new HttpInputStreamUtil.Result(Status.NOT_FOUND);
+ }
+ final ConfigResult configResult = new ConfigResult();
+ if (result.getStatus() == Status.SUCCESS) {
+ LOGGER.debug("Processing Debug key/value pairs from: {}",
source.toString());
+ try {
+ final KeyValuePairConfig config =
MAPPER.readValue(inputStream, KeyValuePairConfig.class);
+ if (config != null && config.getdebugIds() != null) {
+ if (config.getdebugIds().size() > 0) {
+ final List<KeyValuePair> pairs = new ArrayList<>();
+ for (Map.Entry<String, String[]> entry :
config.getdebugIds().entrySet()) {
+ final String key = entry.getKey();
+ for (final String value : entry.getValue()) {
+ if (value != null) {
+ pairs.add(new KeyValuePair(key, value));
+ } else {
+ LOGGER.warn("Ignoring null value for {}",
key);
+ }
+ }
+ }
+ if (pairs.size() > 0) {
+ configResult.pairs =
pairs.toArray(KeyValuePair.EMPTY_ARRAY);
+ configResult.status = Status.SUCCESS;
+ } else {
+ configResult.status = Status.EMPTY;
+ }
+ } else {
+ configResult.status = Status.EMPTY;
+ }
+ } else {
+ LOGGER.warn("No debugIds element in ThreadContextMapFilter
configuration");
+ configResult.status = Status.ERROR;
+ }
+ } catch (Exception ex) {
+ LOGGER.warn("Invalid key/value pair configuration, input
ignored: {}", ex.getMessage());
+ configResult.status = Status.ERROR;
+ }
+ } else {
+ configResult.status = result.getStatus();
+ }
+ return configResult;
+ }
+
+ private static class NoOpFilter extends AbstractFilter {
+
+ public NoOpFilter() {
+ super(Result.NEUTRAL, Result.NEUTRAL);
+ }
+ }
+
+ public interface FilterConfigUpdateListener {
+ void onEvent();
+ }
+
+ private static class ConfigResult extends HttpInputStreamUtil.Result {
+ public KeyValuePair[] pairs;
+ public Status status;
+ }
+}
diff --git
a/log4j-core/src/main/java/org/apache/logging/log4j/core/filter/mutable/KeyValuePairConfig.java
b/log4j-core/src/main/java/org/apache/logging/log4j/core/filter/mutable/KeyValuePairConfig.java
new file mode 100644
index 0000000000..e0372e6146
--- /dev/null
+++
b/log4j-core/src/main/java/org/apache/logging/log4j/core/filter/mutable/KeyValuePairConfig.java
@@ -0,0 +1,46 @@
+/*
+ * 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.filter.mutable;
+
+import java.util.Map;
+
+/**
+ * Class representing the configuration of KeyValue pairs.
+ */
+public class KeyValuePairConfig {
+
+ /**
+ * Map of keys and values for the MutableThreadContextMapFilter. Example
file:
+ * <pre>
+ * {
+ * "debugIds": {
+ * "loginId": ["rgoers", "adam"],
+ * "accountNumber": ["30510263"]
+ * }
+ * }
+ * </pre>
+ */
+ private Map<String, String[]> debugIds;
+
+ public Map<String, String[]> getdebugIds() {
+ return debugIds;
+ }
+
+ public void setKeyValuePairs(Map<String, String[]> debugIds) {
+ this.debugIds = debugIds;
+ }
+}
diff --git
a/log4j-core/src/main/java/org/apache/logging/log4j/core/net/UrlConnectionFactory.java
b/log4j-core/src/main/java/org/apache/logging/log4j/core/net/UrlConnectionFactory.java
index 93841022d8..3d327dabe7 100644
---
a/log4j-core/src/main/java/org/apache/logging/log4j/core/net/UrlConnectionFactory.java
+++
b/log4j-core/src/main/java/org/apache/logging/log4j/core/net/UrlConnectionFactory.java
@@ -41,23 +41,27 @@ import org.apache.logging.log4j.util.Strings;
*/
public class UrlConnectionFactory {
- private static int DEFAULT_TIMEOUT = 60000;
- private static int connectTimeoutMillis = DEFAULT_TIMEOUT;
- private static int readTimeoutMillis = DEFAULT_TIMEOUT;
+ private static final int DEFAULT_TIMEOUT = 60000;
+ private static final int connectTimeoutMillis = DEFAULT_TIMEOUT;
+ private static final int readTimeoutMillis = DEFAULT_TIMEOUT;
private static final String JSON = "application/json";
private static final String XML = "application/xml";
private static final String PROPERTIES = "text/x-java-properties";
private static final String TEXT = "text/plain";
private static final String HTTP = "http";
private static final String HTTPS = "https";
+ private static final String JAR = "jar";
+ private static final String DEFAULT_ALLOWED_PROTOCOLS = "https, file, jar";
private static final String NO_PROTOCOLS = "_none";
public static final String ALLOWED_PROTOCOLS =
"log4j2.Configuration.allowedProtocols";
- public static HttpURLConnection createConnection(final URL url, final long
lastModifiedMillis, final SslConfiguration sslConfiguration)
+ @SuppressWarnings("unchecked")
+ public static <T extends URLConnection> T createConnection(final URL url,
final long lastModifiedMillis,
+ final SslConfiguration sslConfiguration, final
AuthorizationProvider authorizationProvider)
throws IOException {
final PropertiesUtil props = PropertiesUtil.getProperties();
final List<String> allowed = Arrays.asList(Strings.splitList(props
- .getStringProperty(ALLOWED_PROTOCOLS,
HTTPS).toLowerCase(Locale.ROOT)));
+ .getStringProperty(ALLOWED_PROTOCOLS,
DEFAULT_ALLOWED_PROTOCOLS).toLowerCase(Locale.ROOT)));
if (allowed.size() == 1 && NO_PROTOCOLS.equals(allowed.get(0))) {
throw new ProtocolException("No external protocols have been
enabled");
}
@@ -68,42 +72,50 @@ public class UrlConnectionFactory {
if (!allowed.contains(protocol)) {
throw new ProtocolException("Protocol " + protocol + " has not
been enabled as an allowed protocol");
}
- final HttpURLConnection urlConnection = (HttpURLConnection)
url.openConnection();
-
- final AuthorizationProvider provider =
ConfigurationFactory.authorizationProvider(props);
- if (provider != null) {
- provider.addAuthorization(urlConnection);
- }
- urlConnection.setAllowUserInteraction(false);
- urlConnection.setDoOutput(true);
- urlConnection.setDoInput(true);
- urlConnection.setRequestMethod("GET");
- if (connectTimeoutMillis > 0) {
- urlConnection.setConnectTimeout(connectTimeoutMillis);
- }
- if (readTimeoutMillis > 0) {
- urlConnection.setReadTimeout(readTimeoutMillis);
- }
- final String[] fileParts = url.getFile().split("\\.");
- final String type = fileParts[fileParts.length - 1].trim();
- final String contentType = isXml(type) ? XML : isJson(type) ? JSON :
isProperties(type) ? PROPERTIES : TEXT;
- urlConnection.setRequestProperty("Content-Type", contentType);
- if (lastModifiedMillis > 0) {
- urlConnection.setIfModifiedSince(lastModifiedMillis);
- }
- if (url.getProtocol().equals(HTTPS) && sslConfiguration != null) {
- ((HttpsURLConnection)
urlConnection).setSSLSocketFactory(sslConfiguration.getSslSocketFactory());
- if (!sslConfiguration.isVerifyHostName()) {
- ((HttpsURLConnection)
urlConnection).setHostnameVerifier(LaxHostnameVerifier.INSTANCE);
+ URLConnection urlConnection;
+ if (url.getProtocol().equals(HTTP) || url.getProtocol().equals(HTTPS))
{
+ final HttpURLConnection httpURLConnection = (HttpURLConnection)
url.openConnection();
+ if (authorizationProvider != null) {
+ authorizationProvider.addAuthorization(httpURLConnection);
+ }
+ httpURLConnection.setAllowUserInteraction(false);
+ httpURLConnection.setDoOutput(true);
+ httpURLConnection.setDoInput(true);
+ httpURLConnection.setRequestMethod("GET");
+ if (connectTimeoutMillis > 0) {
+ httpURLConnection.setConnectTimeout(connectTimeoutMillis);
+ }
+ if (readTimeoutMillis > 0) {
+ httpURLConnection.setReadTimeout(readTimeoutMillis);
}
+ final String[] fileParts = url.getFile().split("\\.");
+ final String type = fileParts[fileParts.length - 1].trim();
+ final String contentType = isXml(type) ? XML : isJson(type) ? JSON
: isProperties(type) ? PROPERTIES : TEXT;
+ httpURLConnection.setRequestProperty("Content-Type", contentType);
+ if (lastModifiedMillis > 0) {
+ httpURLConnection.setIfModifiedSince(lastModifiedMillis);
+ }
+ if (url.getProtocol().equals(HTTPS) && sslConfiguration != null) {
+ ((HttpsURLConnection)
httpURLConnection).setSSLSocketFactory(sslConfiguration.getSslSocketFactory());
+ if (!sslConfiguration.isVerifyHostName()) {
+ ((HttpsURLConnection)
httpURLConnection).setHostnameVerifier(LaxHostnameVerifier.INSTANCE);
+ }
+ }
+ urlConnection = httpURLConnection;
+ } else if (url.getProtocol().equals(JAR)) {
+ urlConnection = url.openConnection();
+ urlConnection.setUseCaches(false);
+ } else {
+ urlConnection = url.openConnection();
}
- return urlConnection;
+ return (T) urlConnection;
}
public static URLConnection createConnection(final URL url) throws
IOException {
URLConnection urlConnection = null;
if (url.getProtocol().equals(HTTPS) || url.getProtocol().equals(HTTP))
{
- urlConnection = createConnection(url, 0,
SslConfigurationFactory.getSslConfiguration());
+ final AuthorizationProvider provider =
ConfigurationFactory.authorizationProvider(PropertiesUtil.getProperties());
+ urlConnection = createConnection(url, 0,
SslConfigurationFactory.getSslConfiguration(), provider);
} else {
urlConnection = url.openConnection();
if (urlConnection instanceof JarURLConnection) {
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
new file mode 100644
index 0000000000..a5458ea5ea
--- /dev/null
+++
b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/internal/HttpInputStreamUtil.java
@@ -0,0 +1,135 @@
+/*
+ * 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.util.internal;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.HttpURLConnection;
+
+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.SslConfigurationFactory;
+import org.apache.logging.log4j.core.util.AuthorizationProvider;
+import org.apache.logging.log4j.status.StatusLogger;
+
+/**
+ * Utility method for reading data from an HTTP InputStream.
+ */
+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 NOT_FOUND = 404;
+ private static final int OK = 200;
+ private static final int BUF_SIZE = 1024;
+
+ public static Result getInputStream(final LastModifiedSource source,
+ final AuthorizationProvider authorizationProvider) {
+ final Result result = new Result();
+ try {
+ long lastModified = source.getLastModified();
+ HttpURLConnection connection =
UrlConnectionFactory.createConnection(source.getURI().toURL(),
+ lastModified,
SslConfigurationFactory.getSslConfiguration(), authorizationProvider);
+ connection.connect();
+ try {
+ int code = connection.getResponseCode();
+ switch (code) {
+ case NOT_MODIFIED: {
+ LOGGER.debug("Configuration not modified");
+ result.status = Status.NOT_MODIFIED;
+ return result;
+ }
+ case NOT_FOUND: {
+ LOGGER.debug("Unable to access {}: Not Found",
source.toString());
+ result.status = Status.NOT_FOUND;
+ return result;
+ }
+ case OK: {
+ try (InputStream is = connection.getInputStream()) {
+
source.setLastModified(connection.getLastModified());
+ LOGGER.debug("Content was modified for {}.
previous lastModified: {}, new lastModified: {}",
+ source.toString(), lastModified,
connection.getLastModified());
+ result.status = Status.SUCCESS;
+ result.inputStream = new
ByteArrayInputStream(readStream(is));
+ return result;
+ } catch (final IOException e) {
+ try (InputStream es = connection.getErrorStream())
{
+ LOGGER.info("Error accessing configuration at
{}: {}", source.toString(),
+ readStream(es));
+ } catch (final IOException ioe) {
+ LOGGER.error("Error accessing configuration at
{}: {}", source.toString(),
+ e.getMessage());
+ }
+ throw new ConfigurationException("Unable to access
" + source.toString(), e);
+ }
+ }
+ case NOT_AUTHORIZED: {
+ throw new ConfigurationException("Authorization
failed");
+ }
+ default: {
+ if (code < 0) {
+ LOGGER.info("Invalid response code returned");
+ } else {
+ LOGGER.info("Unexpected response code returned
{}", code);
+ }
+ throw new ConfigurationException("Unable to access " +
source.toString());
+ }
+ }
+ } finally {
+ connection.disconnect();
+ }
+ } catch (IOException e) {
+ LOGGER.warn("Error accessing {}: {}", source.toString(),
e.getMessage());
+ throw new ConfigurationException("Unable to access " +
source.toString(), e);
+ }
+ }
+
+ public static byte[] readStream(final InputStream is) throws IOException {
+ final ByteArrayOutputStream result = new ByteArrayOutputStream();
+ final byte[] buffer = new byte[BUF_SIZE];
+ int length;
+ while ((length = is.read(buffer)) != -1) {
+ result.write(buffer, 0, length);
+ }
+ return result.toByteArray();
+ }
+
+ public static class Result {
+
+ private InputStream inputStream;
+ private Status status;
+
+ public Result() {
+ }
+
+ public Result(Status status) {
+ this.status = status;
+ }
+
+ public InputStream getInputStream() {
+ return inputStream;
+ }
+
+ public Status getStatus() {
+ return status;
+ }
+ }
+}
diff --git
a/log4j-core/src/main/java/org/apache/logging/log4j/core/util/internal/LastModifiedSource.java
b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/internal/LastModifiedSource.java
new file mode 100644
index 0000000000..9b2817bc11
--- /dev/null
+++
b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/internal/LastModifiedSource.java
@@ -0,0 +1,51 @@
+/*
+ * 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.util.internal;
+
+import java.io.File;
+import java.net.URI;
+
+import org.apache.logging.log4j.core.util.Source;
+
+/**
+ * A Source that includes the last modified time.
+ */
+public class LastModifiedSource extends Source {
+ private volatile long lastModified;
+
+ public LastModifiedSource(final File file) {
+ super(file);
+ lastModified = 0;
+ }
+
+ public LastModifiedSource(final URI uri) {
+ this(uri, 0);
+ }
+
+ public LastModifiedSource(final URI uri, long lastModifiedMillis) {
+ super(uri);
+ lastModified = lastModifiedMillis;
+ }
+
+ public long getLastModified() {
+ return lastModified;
+ }
+
+ public void setLastModified(long lastModified) {
+ this.lastModified = lastModified;
+ }
+}
diff --git
a/log4j-core/src/main/java/org/apache/logging/log4j/core/util/internal/Status.java
b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/internal/Status.java
new file mode 100644
index 0000000000..baf12c2c92
--- /dev/null
+++
b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/internal/Status.java
@@ -0,0 +1,25 @@
+/*
+ * 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.util.internal;
+
+/**
+ * Status from reading the InputStream.
+ * This class should be considered internal to the Log4j implementation.
+ */
+public enum Status {
+ SUCCESS, NOT_MODIFIED, NOT_FOUND, ERROR, EMPTY;
+}
diff --git
a/log4j-core/src/test/java/org/apache/logging/log4j/core/filter/HttpThreadContextMapFilterTest.java
b/log4j-core/src/test/java/org/apache/logging/log4j/core/filter/HttpThreadContextMapFilterTest.java
new file mode 100644
index 0000000000..df0e4ed370
--- /dev/null
+++
b/log4j-core/src/test/java/org/apache/logging/log4j/core/filter/HttpThreadContextMapFilterTest.java
@@ -0,0 +1,198 @@
+/*
+ * 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.filter;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardCopyOption;
+import java.util.Base64;
+import java.util.Enumeration;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.logging.log4j.Logger;
+import org.apache.logging.log4j.ThreadContext;
+import org.apache.logging.log4j.core.Appender;
+import org.apache.logging.log4j.core.LoggerContext;
+import org.apache.logging.log4j.core.config.Configurator;
+import org.apache.logging.log4j.test.appender.ListAppender;
+import org.eclipse.jetty.http.HttpHeader;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.server.ServerConnector;
+import org.eclipse.jetty.servlet.DefaultServlet;
+import org.eclipse.jetty.servlet.ServletContextHandler;
+import org.eclipse.jetty.servlet.ServletHolder;
+import org.junit.Assert;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+/**
+ * Unit test for simple App.
+ */
+public class HttpThreadContextMapFilterTest implements
MutableThreadContextMapFilter.FilterConfigUpdateListener {
+
+ private static final String BASIC = "Basic ";
+ private static final String expectedCreds = "log4j:log4j";
+ private static Server server;
+ private static final Base64.Decoder decoder = Base64.getDecoder();
+ private static int port;
+ static final String CONFIG = "log4j2-mutableFilter.xml";
+ static LoggerContext loggerContext = null;
+ static final File targetFile = new
File("target/test-classes/testConfig.json");
+ static final Path target = targetFile.toPath();
+ CountDownLatch updated = new CountDownLatch(1);
+
+
+ @BeforeAll
+ public static void startServer() throws Exception {
+ try {
+ server = new Server(0);
+ ServletContextHandler context = new ServletContextHandler();
+ ServletHolder defaultServ = new ServletHolder("default",
TestServlet.class);
+ defaultServ.setInitParameter("resourceBase",
System.getProperty("user.dir"));
+ defaultServ.setInitParameter("dirAllowed", "true");
+ context.addServlet(defaultServ, "/");
+ server.setHandler(context);
+
+ // Start Server
+ server.start();
+ port = ((ServerConnector)
server.getConnectors()[0]).getLocalPort();
+ try {
+ Files.deleteIfExists(target);
+ } catch (IOException ioe) {
+ // Ignore this.
+ }
+ } catch (Throwable ex) {
+ ex.printStackTrace();
+ throw ex;
+ }
+ }
+
+ @AfterAll
+ public static void stopServer() throws Exception {
+ server.stop();
+ }
+ @AfterEach
+ public void after() {
+ try {
+ Files.deleteIfExists(target);
+ } catch (IOException ioe) {
+ // Ignore this.
+ }
+ loggerContext.stop();
+ loggerContext = null;
+ }
+
+ @Test
+ public void filterTest() throws Exception {
+ System.setProperty("log4j2.Configuration.allowedProtocols", "http");
+ System.setProperty("logging.auth.username", "log4j");
+ System.setProperty("logging.auth.password", "log4j");
+ System.setProperty("configLocation", "http://localhost:" + port +
"/testConfig.json");
+ ThreadContext.put("loginId", "rgoers");
+ Path source = new
File("target/test-classes/emptyConfig.json").toPath();
+ Files.copy(source, target, StandardCopyOption.REPLACE_EXISTING);
+ long fileTime = targetFile.lastModified() - 2000;
+ assertTrue(targetFile.setLastModified(fileTime));
+ loggerContext = Configurator.initialize(null, CONFIG);
+ assertNotNull(loggerContext);
+ Appender app = loggerContext.getConfiguration().getAppender("List");
+ assertNotNull(app);
+ assertTrue(app instanceof ListAppender);
+ MutableThreadContextMapFilter filter = (MutableThreadContextMapFilter)
loggerContext.getConfiguration().getFilter();
+ assertNotNull(filter);
+ filter.registerListener(this);
+ Logger logger = loggerContext.getLogger("Test");
+ logger.debug("This is a test");
+ Assertions.assertEquals(0, ((ListAppender) app).getEvents().size());
+ source = new File("target/test-classes/filterConfig.json").toPath();
+ Files.copy(source, target, StandardCopyOption.REPLACE_EXISTING);
+ assertNotEquals(fileTime, targetFile.lastModified());
+ if (!updated.await(5, TimeUnit.SECONDS)) {
+ fail("File update was not detected");
+ }
+ updated = new CountDownLatch(1);
+ logger.debug("This is a test");
+ Assertions.assertEquals(1, ((ListAppender) app).getEvents().size());
+ Assertions.assertTrue(Files.deleteIfExists(target));
+ if (!updated.await(5, TimeUnit.SECONDS)) {
+ fail("File update for delete was not detected");
+ }
+ }
+
+ public static class TestServlet extends DefaultServlet {
+
+ private static final long serialVersionUID = -2885158530511450659L;
+
+ @Override
+ protected void doGet(HttpServletRequest request,
+ HttpServletResponse response) throws ServletException,
IOException {
+ Enumeration<String> headers =
request.getHeaders(HttpHeader.AUTHORIZATION.toString());
+ if (headers == null) {
+ response.sendError(401, "No Auth header");
+ return;
+ }
+ while (headers.hasMoreElements()) {
+ String authData = headers.nextElement();
+ Assert.assertTrue("Not a Basic auth header",
authData.startsWith(BASIC));
+ String credentials = new
String(decoder.decode(authData.substring(BASIC.length())));
+ if (!expectedCreds.equals(credentials)) {
+ response.sendError(401, "Invalid credentials");
+ return;
+ }
+ }
+ if (request.getServletPath().equals("/testConfig.json")) {
+ File file = new File("target/test-classes/testConfig.json");
+ if (!file.exists()) {
+ response.sendError(404, "File not found");
+ return;
+ }
+ long modifiedSince =
request.getDateHeader(HttpHeader.IF_MODIFIED_SINCE.toString());
+ long lastModified = (file.lastModified() / 1000) * 1000;
+ if (modifiedSince > 0 && lastModified <= modifiedSince) {
+ response.setStatus(304);
+ return;
+ }
+ response.setDateHeader(HttpHeader.LAST_MODIFIED.toString(),
lastModified);
+ response.setContentLengthLong(file.length());
+ Files.copy(file.toPath(), response.getOutputStream());
+ response.getOutputStream().flush();
+ response.setStatus(200);
+ } else {
+ response.sendError(400, "Unsupported request");
+ }
+ }
+ }
+
+ @Override
+ public void onEvent() {
+ updated.countDown();
+ }
+}
diff --git
a/log4j-core/src/test/java/org/apache/logging/log4j/core/filter/MutableThreadContextMapFilterTest.java
b/log4j-core/src/test/java/org/apache/logging/log4j/core/filter/MutableThreadContextMapFilterTest.java
new file mode 100644
index 0000000000..ad3f0fdcd9
--- /dev/null
+++
b/log4j-core/src/test/java/org/apache/logging/log4j/core/filter/MutableThreadContextMapFilterTest.java
@@ -0,0 +1,98 @@
+/*
+ * 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.filter;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardCopyOption;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.logging.log4j.Logger;
+import org.apache.logging.log4j.ThreadContext;
+import org.apache.logging.log4j.core.Appender;
+import org.apache.logging.log4j.core.LoggerContext;
+import org.apache.logging.log4j.core.config.Configurator;
+import org.apache.logging.log4j.test.appender.ListAppender;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+/**
+ * Unit test for simple App.
+ */
+public class MutableThreadContextMapFilterTest implements
MutableThreadContextMapFilter.FilterConfigUpdateListener {
+
+ static final String CONFIG = "log4j2-mutableFilter.xml";
+ static LoggerContext loggerContext = null;
+ static File targetFile = new File("target/test-classes/testConfig.json");
+ static Path target = targetFile.toPath();
+ CountDownLatch updated = new CountDownLatch(1);
+
+ @AfterEach
+ public void after() {
+ try {
+ Files.deleteIfExists(target);
+ } catch (IOException ioe) {
+ // Ignore this.
+ }
+ ThreadContext.clearMap();
+ loggerContext.stop();
+ loggerContext = null;
+ }
+
+ @Test
+ public void filterTest() throws Exception {
+ System.setProperty("configLocation",
"target/test-classes/testConfig.json");
+ ThreadContext.put("loginId", "rgoers");
+ Path source = new
File("target/test-classes/emptyConfig.json").toPath();
+ Files.copy(source, target, StandardCopyOption.REPLACE_EXISTING);
+ long fileTime = targetFile.lastModified() - 1000;
+ assertTrue(targetFile.setLastModified(fileTime));
+ loggerContext = Configurator.initialize(null, CONFIG);
+ assertNotNull(loggerContext);
+ Appender app = loggerContext.getConfiguration().getAppender("List");
+ assertNotNull(app);
+ assertTrue(app instanceof ListAppender);
+ MutableThreadContextMapFilter filter = (MutableThreadContextMapFilter)
loggerContext.getConfiguration().getFilter();
+ assertNotNull(filter);
+ filter.registerListener(this);
+ Logger logger = loggerContext.getLogger("Test");
+ logger.debug("This is a test");
+ Assertions.assertEquals(0, ((ListAppender) app).getEvents().size());
+ source = new File("target/test-classes/filterConfig.json").toPath();
+ Files.copy(source, target, StandardCopyOption.REPLACE_EXISTING);
+ assertNotEquals(fileTime, targetFile.lastModified());
+ if (!updated.await(5, TimeUnit.SECONDS)) {
+ fail("File update was not detected");
+ }
+ logger.debug("This is a test");
+ Assertions.assertEquals(1, ((ListAppender) app).getEvents().size());
+ }
+
+ @Override
+ public void onEvent() {
+ updated.countDown();
+ }
+}
diff --git
a/log4j-core/src/test/java/org/apache/logging/log4j/core/net/UrlConnectionFactoryTest.java
b/log4j-core/src/test/java/org/apache/logging/log4j/core/net/UrlConnectionFactoryTest.java
index 202a221c3f..1c40dbcef7 100644
---
a/log4j-core/src/test/java/org/apache/logging/log4j/core/net/UrlConnectionFactoryTest.java
+++
b/log4j-core/src/test/java/org/apache/logging/log4j/core/net/UrlConnectionFactoryTest.java
@@ -46,8 +46,11 @@ import javax.servlet.http.HttpServletResponse;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
+import org.apache.logging.log4j.core.config.ConfigurationFactory;
import org.apache.logging.log4j.core.config.ConfigurationSource;
+import org.apache.logging.log4j.core.util.AuthorizationProvider;
import org.apache.logging.log4j.core.util.FileUtils;
+import org.apache.logging.log4j.util.PropertiesUtil;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
@@ -71,7 +74,7 @@ public class UrlConnectionFactoryTest {
private static final String BASIC = "Basic ";
private static final String expectedCreds = "testuser:password";
private static Server server;
- private static Base64.Decoder decoder = Base64.getDecoder();
+ private static final Base64.Decoder decoder = Base64.getDecoder();
private static int port;
private static final int BUF_SIZE = 1024;
@@ -79,8 +82,8 @@ public class UrlConnectionFactoryTest {
public static void startServer() throws Exception {
try {
server = new Server(0);
- ServletContextHandler context = new ServletContextHandler();
- ServletHolder defaultServ = new ServletHolder("default",
TestServlet.class);
+ final ServletContextHandler context = new ServletContextHandler();
+ final ServletHolder defaultServ = new ServletHolder("default",
TestServlet.class);
defaultServ.setInitParameter("resourceBase",
System.getProperty("user.dir"));
defaultServ.setInitParameter("dirAllowed", "true");
context.addServlet(defaultServ, "/");
@@ -105,8 +108,8 @@ public class UrlConnectionFactoryTest {
System.setProperty("log4j2.Configuration.username", "foo");
System.setProperty("log4j2.Configuration.password", "bar");
System.setProperty("log4j2.Configuration.allowedProtocols", "http");
- URI uri = new URI("http://localhost:" + port + "/log4j2-config.xml");
- ConfigurationSource source = ConfigurationSource.fromUri(uri);
+ final URI uri = new URI("http://localhost:" + port +
"/log4j2-config.xml");
+ final ConfigurationSource source = ConfigurationSource.fromUri(uri);
assertNull(source, "A ConfigurationSource should not have been
returned");
}
@@ -115,13 +118,13 @@ public class UrlConnectionFactoryTest {
System.setProperty("log4j2.Configuration.username", "testuser");
System.setProperty("log4j2.Configuration.password", "password");
System.setProperty("log4j2.Configuration.allowedProtocols", "http");
- URI uri = new URI("http://localhost:" + port + "/log4j2-config.xml");
- ConfigurationSource source = ConfigurationSource.fromUri(uri);
+ final URI uri = new URI("http://localhost:" + port +
"/log4j2-config.xml");
+ final ConfigurationSource source = ConfigurationSource.fromUri(uri);
assertNotNull(source, "No ConfigurationSource returned");
- InputStream is = source.getInputStream();
+ final InputStream is = source.getInputStream();
assertNotNull(is, "No data returned");
is.close();
- long lastModified = source.getLastModified();
+ final long lastModified = source.getLastModified();
int result = verifyNotModified(uri, lastModified);
assertEquals(SC_NOT_MODIFIED, result,"File was modified");
File file = new File("target/test-classes/log4j2-config.xml");
@@ -132,9 +135,9 @@ public class UrlConnectionFactoryTest {
assertEquals(SC_OK, result,"File was not modified");
}
- private int verifyNotModified(URI uri, long lastModifiedMillis) throws
Exception {
+ private int verifyNotModified(final URI uri, final long
lastModifiedMillis) throws Exception {
final HttpURLConnection urlConnection =
UrlConnectionFactory.createConnection(uri.toURL(),
- lastModifiedMillis, null);
+ lastModifiedMillis, null, null);
urlConnection.connect();
try {
diff --git a/log4j-core/src/test/resources/emptyConfig.json
b/log4j-core/src/test/resources/emptyConfig.json
new file mode 100644
index 0000000000..c3a465a6ab
--- /dev/null
+++ b/log4j-core/src/test/resources/emptyConfig.json
@@ -0,0 +1,4 @@
+{
+ "keyValuePairs": {
+ }
+}
\ No newline at end of file
diff --git a/log4j-core/src/test/resources/filterConfig.json
b/log4j-core/src/test/resources/filterConfig.json
new file mode 100644
index 0000000000..8857185068
--- /dev/null
+++ b/log4j-core/src/test/resources/filterConfig.json
@@ -0,0 +1,6 @@
+{
+ "debugIds": {
+ "loginId": ["rgoers", "adam"],
+ "corpAcctNumber": ["30510263"]
+ }
+}
\ No newline at end of file
diff --git a/log4j-core/src/test/resources/log4j2-mutableFilter.xml
b/log4j-core/src/test/resources/log4j2-mutableFilter.xml
new file mode 100644
index 0000000000..aeb6585d9c
--- /dev/null
+++ b/log4j-core/src/test/resources/log4j2-mutableFilter.xml
@@ -0,0 +1,37 @@
+<?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 name="ConfigTest" status="ERROR">
+ <MutableThreadContextMapFilter configLocation="${sys:configLocation}"
pollInterval="1" onMatch="ACCEPT" onMismatch="NEUTRAL">
+ </MutableThreadContextMapFilter>
+ <Appenders>
+ <Console name="STDOUT">
+ <PatternLayout pattern="%m%n"/>
+ </Console>
+ <List name="List">
+ </List>
+ </Appenders>
+ <Loggers>
+ <Logger name="Test" level="error">
+ <AppenderRef ref="List"/>
+ </Logger>
+ <Root level="error">
+ <AppenderRef ref="STDOUT"/>
+ </Root>
+ </Loggers>
+</Configuration>
\ No newline at end of file
diff --git
a/log4j-spring-boot/src/main/java/org/apache/logging/log4j/spring/boot/Log4j2CloudConfigLoggingSystem.java
b/log4j-spring-boot/src/main/java/org/apache/logging/log4j/spring/boot/Log4j2CloudConfigLoggingSystem.java
index aaff3206e2..badd9a4c24 100644
---
a/log4j-spring-boot/src/main/java/org/apache/logging/log4j/spring/boot/Log4j2CloudConfigLoggingSystem.java
+++
b/log4j-spring-boot/src/main/java/org/apache/logging/log4j/spring/boot/Log4j2CloudConfigLoggingSystem.java
@@ -39,7 +39,7 @@ import org.apache.logging.log4j.core.config.Configuration;
import org.apache.logging.log4j.core.config.ConfigurationFactory;
import org.apache.logging.log4j.core.config.ConfigurationSource;
import org.apache.logging.log4j.core.config.composite.CompositeConfiguration;
-import org.apache.logging.log4j.core.net.ssl.LaxHostnameVerifier;
+import org.apache.logging.log4j.core.net.UrlConnectionFactory;
import org.apache.logging.log4j.core.net.ssl.SslConfiguration;
import org.apache.logging.log4j.core.net.ssl.SslConfigurationFactory;
import org.apache.logging.log4j.core.util.AuthorizationProvider;
@@ -194,20 +194,12 @@ public class Log4j2CloudConfigLoggingSystem extends
Log4J2LoggingSystem {
}
private ConfigurationSource getConfigurationSource(URL url) throws
IOException, URISyntaxException {
- URLConnection urlConnection = url.openConnection();
- // A "jar:" URL file remains open after the stream is closed, so do
not cache it.
- urlConnection.setUseCaches(false);
AuthorizationProvider provider =
ConfigurationFactory.authorizationProvider(PropertiesUtil.getProperties());
- provider.addAuthorization(urlConnection);
- if (url.getProtocol().equals(HTTPS)) {
- SslConfiguration sslConfiguration =
SslConfigurationFactory.getSslConfiguration();
- if (sslConfiguration != null) {
- ((HttpsURLConnection)
urlConnection).setSSLSocketFactory(sslConfiguration.getSslSocketFactory());
- if (!sslConfiguration.isVerifyHostName()) {
- ((HttpsURLConnection)
urlConnection).setHostnameVerifier(LaxHostnameVerifier.INSTANCE);
- }
- }
- }
+ SslConfiguration sslConfiguration = url.getProtocol().equals(HTTPS)
+ ? SslConfigurationFactory.getSslConfiguration() : null;
+ URLConnection urlConnection =
UrlConnectionFactory.createConnection(url, 0, sslConfiguration,
+ provider);
+
File file = FileUtils.fileFromUri(url.toURI());
try {
if (file != null) {
diff --git a/src/changes/changes.xml b/src/changes/changes.xml
index 3677dd536f..04e30996fe 100644
--- a/src/changes/changes.xml
+++ b/src/changes/changes.xml
@@ -29,8 +29,17 @@
- "update" - Change
- "remove" - Removed
-->
- <release version="2.17.3" date="2022-TBD" description="GA Release 2.18.0">
+ <release version="2.18.0" date="2022-TBD" description="GA Release 2.18.0">
<!-- FIXES -->
+ <action issue="LOG4J2-3481" dev="rgoers" type="fix">
+ HttpWatcher did not pass credentials when polling.
+ </action>
+ <action issue="LOG4J2-3482" dev="rgoers" type="fix">
+ UrlConnectionFactory.createConnection now accepts an
AuthorizationProvider as a parameter.
+ </action>
+ <action issue="LOG4J2-3495" dev="rgoers" type="fix">
+ Add MutableThreadContextMapFilter.
+ </action>
<action issue="LOG4J2-3473" dev="rpopma" type="update">
Make the default disruptor WaitStrategy used by Async Loggers
garbage-free.
</action>
diff --git a/src/site/xdoc/manual/configuration.xml.vm
b/src/site/xdoc/manual/configuration.xml.vm
index e543e1adf1..51a3b58938 100644
--- a/src/site/xdoc/manual/configuration.xml.vm
+++ b/src/site/xdoc/manual/configuration.xml.vm
@@ -376,9 +376,9 @@ public class Bar {
the value of the
<code>log4j2.Configuration.allowedProtocols</code> system property. If the
provided list
contains the protocol specified then Log4j will use the URI to
locate the specified configuration file. If
not an exception will be thrown and an error message will be
logged. If no value is provided for the
- system property it will default to "https". Use of any protocol
other than "file" can be prevented by
- setting the system property value to "_none". This value would be
an invalid protocol so cannot conflict
- with any custom protocols that may be present.
+ system property it will default to "https, file, jar". Use of any
protocol other than "file" can be
+ prevented by setting the system property value to "_none". This
value would be an invalid protocol so cannot
+ conflict with any custom protocols that may be present.
</p>
<p>
Log4j supports access to remote urls that require authentication.
Log4j supports basic authentication
@@ -2023,8 +2023,8 @@ public class AwesomeTest {
<td>LOG4J_CONFIGURATION_ALLOWED_PROTOCOLS</td>
<td> </td>
<td>
- A comma separated list of the protocols that may be used to load a
configuration file. The default is https.
- To completely prevent accessing the configuration via a URL specify a
value of "_none".
+ A comma separated list of the protocols that may be used to load a
configuration file. The default is
+ "https, file, jar". To completely prevent accessing the configuration
via a URL specify a value of "_none".
</td>
</tr>
<tr>
diff --git a/src/site/xdoc/manual/filters.xml b/src/site/xdoc/manual/filters.xml
index 9dc60e954f..52d7275ce9 100644
--- a/src/site/xdoc/manual/filters.xml
+++ b/src/site/xdoc/manual/filters.xml
@@ -407,6 +407,88 @@
</Root>
</Loggers>
</Configuration>]]></pre>
+ </subsection>
+ <a name="MutableThreadContextMapFilter"/>
+ <subsection name="MutableThreadContextMapFilter (or
MutableContextMapFilter)">
+ <p>
+ The MutableThreadContextMapFilter or MutableContextMapFilter
allows filtering against data elements that are in the
+ current context. By default this is the ThreadContext Map. The
values to compare are defined externally and
+ can be periodically polled for changes.
+ </p>
+ <table>
+ <caption align="top">Mutable Context Map Filter
Parameters</caption>
+ <tr>
+ <th>Parameter Name</th>
+ <th>Type</th>
+ <th>Description</th>
+ </tr>
+ <tr>
+ <td>configLocation</td>
+ <td>String</td>
+ <td>A file path or URI that points to the configuration. See
below for a sample configuration.</td>
+ </tr>
+ <tr>
+ <td>pollInterval</td>
+ <td>int</td>
+ <td>The number of seconds to wait before checking to see if the
configuration has been modified. When
+ using HTTP or HTTPS the server must support the
If-Modified-Since header and return a Last-Modified
+ header containing the date and time the file was last
modified. Note that by default only the https,
+ file, and jar protocols are allowed. Support for other
protocols can be enabled by specifying them
+ in the log4j2.Configuration.allowedProtocols system
property</td>
+ </tr>
+ <tr>
+ <td>operator</td>
+ <td>String</td>
+ <td>If the operator is "or" then a match by any one of the
key/value pairs will be considered to be
+ a match, otherwise all the key/value pairs must match.</td>
+ </tr>
+ <tr>
+ <td>onMatch</td>
+ <td>String</td>
+ <td>Action to take when the filter matches. May be ACCEPT, DENY
or NEUTRAL. The default value is NEUTRAL.</td>
+ </tr>
+ <tr>
+ <td>onMismatch</td>
+ <td>String</td>
+ <td>Action to take when the filter does not match. May be
ACCEPT, DENY or NEUTRAL. The default value is
+ DENY.</td>
+ </tr>
+ </table>
+ <p>
+ A configuration containing the MutableContextMapFilter might look
like:
+ </p>
+ <pre class="prettyprint linenums"><![CDATA[<?xml version="1.0"
encoding="UTF-8"?>
+<Configuration status="warn" name="MyApp" packages="">
+ <MutableContextMapFilter onMatch="ACCEPT" onMismatch="NEUTRAL" operator="or"
+ configLocation="http://localhost:8080/threadContextFilter.json"
pollInterval="300">
+ </MutableContextMapFilter>
+ <Appenders>
+ <RollingFile name="RollingFile" fileName="logs/app.log"
+ filePattern="logs/app-%d{MM-dd-yyyy}.log.gz">
+ <BurstFilter level="INFO" rate="16" maxBurst="100"/>
+ <PatternLayout>
+ <pattern>%d %p %c{1.} [%t] %m%n</pattern>
+ </PatternLayout>
+ <TimeBasedTriggeringPolicy />
+ </RollingFile>
+ </Appenders>
+ <Loggers>
+ <Root level="error">
+ <AppenderRef ref="RollingFile"/>
+ </Root>
+ </Loggers>
+</Configuration>]]></pre>
+ <p>
+ The configuration file supplied to the filter should look similar
to:
+ </p>
+ <pre class="prettyprint linenums"><![CDATA[<?xml version="1.0"
encoding="UTF-8"?>
+{
+ "debugIds": {
+ "loginId": ["[email protected]", "[email protected]"],
+ "accountNumber": ["30510263"]
+ }
+}
+ ]]></pre>
</subsection>
<a name="NoMarkerFilter"/>
<subsection name="NoMarkerFilter">