Merge branch 'master' of https://github.com/nataliemeurer/logging-log4j2 into nataliemeurer-master
diff --git a/log4j-redis/pom.xml b/log4j-redis/pom.xml
new file mode 100644
index 0000000..85844eb
--- /dev/null
+++ b/log4j-redis/pom.xml
@@ -0,0 +1,176 @@
+<?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. -->
+
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+  <parent>
+    <groupId>org.apache.logging.log4j</groupId>
+    <artifactId>log4j</artifactId>
+    <version>3.0.0-SNAPSHOT</version>
+  </parent>
+  <modelVersion>4.0.0</modelVersion>
+
+  <artifactId>log4j-redis</artifactId>
+  <name>Apache Log4j Redis</name>
+  <description>
+    Apache Log4j Redis.
+  </description>
+  <properties>
+    <log4jParentDir>${basedir}/..</log4jParentDir>
+    <docLabel>Log4j Redis Documentation</docLabel>
+    <projectDir>/log4j-redis</projectDir>
+    <module.name>org.apache.logging.log4j.redis</module.name>
+  </properties>
+
+  <dependencies>
+    <dependency>
+      <groupId>org.apache.logging.log4j</groupId>
+      <artifactId>log4j-core</artifactId>
+    </dependency>
+    <!-- Used for Redis appender -->
+    <dependency>
+      <groupId>redis.clients</groupId>
+      <artifactId>jedis</artifactId>
+      <version>2.9.0</version>
+    </dependency>
+    <!-- Test Dependencies -->
+    <dependency>
+      <groupId>junit</groupId>
+      <artifactId>junit</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.logging.log4j</groupId>
+      <artifactId>log4j-api</artifactId>
+      <type>test-jar</type>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.logging.log4j</groupId>
+      <artifactId>log4j-core</artifactId>
+      <type>test-jar</type>
+    </dependency>
+    <!-- Mocking framework for use with JUnit -->
+    <dependency>
+      <groupId>org.mockito</groupId>
+      <artifactId>mockito-core</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <!-- Required for AsyncLoggers -->
+    <dependency>
+      <groupId>com.lmax</groupId>
+      <artifactId>disruptor</artifactId>
+      <scope>test</scope>
+    </dependency>
+  </dependencies>
+  <build>
+    <plugins>
+      <plugin>
+        <groupId>org.apache.felix</groupId>
+        <artifactId>maven-bundle-plugin</artifactId>
+        <configuration>
+          <instructions>
+            <Fragment-Host>org.apache.logging.log4j.core.appender.mom.redis</Fragment-Host>
+            <Export-Package>*</Export-Package>
+          </instructions>
+        </configuration>
+      </plugin>
+    </plugins>
+  </build>
+  <reporting>
+    <plugins>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-changes-plugin</artifactId>
+        <version>${changes.plugin.version}</version>
+        <reportSets>
+          <reportSet>
+            <reports>
+              <report>changes-report</report>
+            </reports>
+          </reportSet>
+        </reportSets>
+        <configuration>
+          <issueLinkTemplate>%URL%/show_bug.cgi?id=%ISSUE%</issueLinkTemplate>
+          <useJql>true</useJql>
+        </configuration>
+      </plugin>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-checkstyle-plugin</artifactId>
+        <version>${checkstyle.plugin.version}</version>
+        <configuration>
+          <!--<propertiesLocation>${vfs.parent.dir}/checkstyle.properties</propertiesLocation> -->
+          <configLocation>${log4jParentDir}/checkstyle.xml</configLocation>
+          <suppressionsLocation>${log4jParentDir}/checkstyle-suppressions.xml</suppressionsLocation>
+          <enableRulesSummary>false</enableRulesSummary>
+          <propertyExpansion>basedir=${basedir}</propertyExpansion>
+          <propertyExpansion>licensedir=${log4jParentDir}/checkstyle-header.txt</propertyExpansion>
+        </configuration>
+      </plugin>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-javadoc-plugin</artifactId>
+        <version>${javadoc.plugin.version}</version>
+        <configuration>
+          <bottom><![CDATA[<p align="center">Copyright &#169; {inceptionYear}-{currentYear} {organizationName}. All Rights Reserved.<br />
+            Apache Logging, Apache Log4j, Log4j, Apache, the Apache feather logo, the Apache Logging project logo,
+            and the Apache Log4j logo are trademarks of The Apache Software Foundation.</p>]]></bottom>
+          <!-- module link generation is completely broken in the javadoc plugin for a multi-module non-aggregating project -->
+          <detectOfflineLinks>false</detectOfflineLinks>
+          <linksource>true</linksource>
+        </configuration>
+        <reportSets>
+          <reportSet>
+            <id>non-aggregate</id>
+            <reports>
+              <report>javadoc</report>
+            </reports>
+          </reportSet>
+        </reportSets>
+      </plugin>
+      <plugin>
+        <groupId>com.github.spotbugs</groupId>
+        <artifactId>spotbugs-maven-plugin</artifactId>
+        <configuration>
+          <fork>true</fork>
+          <jvmArgs>-Duser.language=en</jvmArgs>
+          <threshold>Normal</threshold>
+          <effort>Default</effort>
+          <excludeFilterFile>${log4jParentDir}/spotbugs-exclude-filter.xml</excludeFilterFile>
+        </configuration>
+      </plugin>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-jxr-plugin</artifactId>
+        <version>${jxr.plugin.version}</version>
+        <reportSets>
+          <reportSet>
+            <id>non-aggregate</id>
+            <reports>
+              <report>jxr</report>
+            </reports>
+          </reportSet>
+          <reportSet>
+            <id>aggregate</id>
+            <reports>
+              <report>aggregate</report>
+            </reports>
+          </reportSet>
+        </reportSets>
+      </plugin>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-pmd-plugin</artifactId>
+        <version>${pmd.plugin.version}</version>
+        <configuration>
+          <targetJdk>${maven.compiler.target}</targetJdk>
+        </configuration>
+      </plugin>
+    </plugins>
+  </reporting>
+</project>
diff --git a/log4j-redis/src/main/java/org/apache/logging/log4j/redis/appender/LoggingRedisPoolConfiguration.java b/log4j-redis/src/main/java/org/apache/logging/log4j/redis/appender/LoggingRedisPoolConfiguration.java
new file mode 100644
index 0000000..ba3deac
--- /dev/null
+++ b/log4j-redis/src/main/java/org/apache/logging/log4j/redis/appender/LoggingRedisPoolConfiguration.java
@@ -0,0 +1,148 @@
+/*
+ * 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.redis.appender;
+
+import org.apache.logging.log4j.core.Core;
+import org.apache.logging.log4j.core.config.plugins.Plugin;
+import org.apache.logging.log4j.core.config.plugins.PluginBuilderAttribute;
+import org.apache.logging.log4j.core.config.plugins.PluginBuilderFactory;
+import redis.clients.jedis.JedisPoolConfig;
+
+/**
+ * Plugin configuration for {@link redis.clients.jedis.JedisPool} objects, allowing end users to set pool configuration
+ * if desired. If not set, will default to JedisPool defaults.
+ */
+@Plugin(name = "RedisPoolConfiguration", category = Core.CATEGORY_NAME, printObject = true)
+class LoggingRedisPoolConfiguration extends JedisPoolConfig {
+
+    private LoggingRedisPoolConfiguration() {
+        super();
+    }
+
+    static LoggingRedisPoolConfiguration defaultConfiguration() {
+        return LoggingRedisPoolConfiguration.newBuilder().build();
+    }
+
+    /**
+     * Creates a LoggingRedisPoolConfiguration from standard pool parameters.
+     */
+    @PluginBuilderFactory
+    public static Builder newBuilder() {
+        return new LoggingRedisPoolConfiguration.Builder();
+    }
+
+    private static class Builder implements org.apache.logging.log4j.core.util.Builder<LoggingRedisPoolConfiguration> {
+
+        @PluginBuilderAttribute("minIdle")
+        private int minIdle = JedisPoolConfig.DEFAULT_MIN_IDLE;
+
+        @PluginBuilderAttribute("maxIdle")
+        private int maxIdle = JedisPoolConfig.DEFAULT_MAX_IDLE;
+
+        @PluginBuilderAttribute("testOnBorrow")
+        private boolean testOnBorrow = JedisPoolConfig.DEFAULT_TEST_ON_BORROW;
+
+        @PluginBuilderAttribute("testOnReturn")
+        boolean testOnReturn = JedisPoolConfig.DEFAULT_TEST_ON_RETURN;
+
+        @PluginBuilderAttribute("testWhileIdle")
+        boolean testWhileIdle = JedisPoolConfig.DEFAULT_TEST_WHILE_IDLE;
+
+        @PluginBuilderAttribute("testsPerEvictionRun")
+        int testsPerEvictionRun = JedisPoolConfig.DEFAULT_NUM_TESTS_PER_EVICTION_RUN;
+
+        @PluginBuilderAttribute("timeBetweenEvictionRunsMillis")
+        long timeBetweenEvicationRunsMillis = JedisPoolConfig.DEFAULT_TIME_BETWEEN_EVICTION_RUNS_MILLIS;
+
+        @Override
+        public LoggingRedisPoolConfiguration build() {
+            LoggingRedisPoolConfiguration poolConfig = new LoggingRedisPoolConfiguration();
+            poolConfig.setMaxIdle(maxIdle);
+            poolConfig.setMinIdle(minIdle);
+            poolConfig.setTestOnBorrow(testOnBorrow);
+            poolConfig.setTestOnReturn(testOnReturn);
+            poolConfig.setTestWhileIdle(testWhileIdle);
+            poolConfig.setNumTestsPerEvictionRun(testsPerEvictionRun);
+            poolConfig.setTimeBetweenEvictionRunsMillis(timeBetweenEvicationRunsMillis);
+            return poolConfig;
+        }
+
+        public int getMinIdle() {
+            return minIdle;
+        }
+
+        public Builder setMinIdle(int minIdle) {
+            this.minIdle = minIdle;
+            return this;
+        }
+
+        public int getMaxIdle() {
+            return maxIdle;
+        }
+
+        public Builder setMaxIdle(int maxIdle) {
+            this.maxIdle = maxIdle;
+            return this;
+        }
+
+        public boolean isTestOnBorrow() {
+            return testOnBorrow;
+        }
+
+        public Builder setTestOnBorrow(boolean testOnBorrow) {
+            this.testOnBorrow = testOnBorrow;
+            return this;
+        }
+
+        public boolean isTestOnReturn() {
+            return testOnReturn;
+        }
+
+        public Builder setTestOnReturn(boolean testOnReturn) {
+            this.testOnReturn = testOnReturn;
+            return this;
+        }
+
+        public boolean isTestWhileIdle() {
+            return testWhileIdle;
+        }
+
+        public Builder setTestWhileIdle(boolean testWhileIdle) {
+            this.testWhileIdle = testWhileIdle;
+            return this;
+        }
+
+        public int getTestsPerEvictionRun() {
+            return testsPerEvictionRun;
+        }
+
+        public Builder setTestsPerEvictionRun(int testsPerEvictionRun) {
+            this.testsPerEvictionRun = testsPerEvictionRun;
+            return this;
+        }
+
+        public long getTimeBetweenEvicationRunsMillis() {
+            return timeBetweenEvicationRunsMillis;
+        }
+
+        public Builder setTimeBetweenEvicationRunsMillis(long timeBetweenEvicationRunsMillis) {
+            this.timeBetweenEvicationRunsMillis = timeBetweenEvicationRunsMillis;
+            return this;
+        }
+    }
+}
diff --git a/log4j-redis/src/main/java/org/apache/logging/log4j/redis/appender/RedisAppender.java b/log4j-redis/src/main/java/org/apache/logging/log4j/redis/appender/RedisAppender.java
new file mode 100644
index 0000000..4a19717
--- /dev/null
+++ b/log4j-redis/src/main/java/org/apache/logging/log4j/redis/appender/RedisAppender.java
@@ -0,0 +1,252 @@
+/*
+ * 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.redis.appender;
+
+import org.apache.logging.log4j.LoggingException;
+import org.apache.logging.log4j.core.*;
+import org.apache.logging.log4j.core.appender.AbstractAppender;
+import org.apache.logging.log4j.core.appender.AppenderLoggingException;
+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.PluginBuilderAttribute;
+import org.apache.logging.log4j.core.config.plugins.PluginBuilderFactory;
+import org.apache.logging.log4j.core.config.plugins.PluginElement;
+import org.apache.logging.log4j.core.config.plugins.validation.constraints.Required;
+import org.apache.logging.log4j.core.net.ssl.SslConfiguration;
+import org.apache.logging.log4j.spi.AbstractLogger;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Sends log events to a Redis key as a List. All logs are appended to Redis lists via the RPUSH command at keys defined
+ * in the configuration.
+ */
+@Plugin(name = "Redis", category = Node.CATEGORY, elementType = Appender.ELEMENT_TYPE, printObject = true)
+public final class RedisAppender extends AbstractAppender {
+
+    // The default port here is the default port for Redis generally.
+    // For more details, see the full configuration: http://download.redis.io/redis-stable/redis.conf
+    private static final int DEFAULT_REDIS_PORT = 6379;
+    private static final String DEFAULT_REDIS_KEYS = "log-events";
+    private static final int DEFAULT_APPENDER_QUEUE_CAPACITY = 20;
+
+    private final RedisManager manager;
+    private final boolean immediateFlush;
+    private final LinkedBlockingQueue<String> logQueue;
+
+    private RedisAppender(final String name, final Layout<? extends Serializable> layout, final Filter filter,
+                          final boolean ignoreExceptions, boolean immediateFlush, final int queueCapacity, final RedisManager manager) {
+        super(name, filter, layout, ignoreExceptions);
+        this.manager = Objects.requireNonNull(manager, "Redis Manager");
+        this.immediateFlush = immediateFlush;
+        this.logQueue = new LinkedBlockingQueue<>(queueCapacity);
+    }
+
+    /**
+     * Builds RedisAppender instances.
+     * @param <B> The type to build
+     */
+    public static class Builder<B extends Builder<B>> extends AbstractAppender.Builder<B>
+            implements org.apache.logging.log4j.core.util.Builder<RedisAppender> {
+
+        private final String KEY_SEPARATOR = ",";
+
+        @PluginBuilderAttribute("host")
+        @Required(message = "No Redis hostname provided")
+        private String host;
+
+        @PluginBuilderAttribute("keys")
+        private String keys = DEFAULT_REDIS_KEYS;
+
+        @PluginBuilderAttribute("port")
+        private int port = DEFAULT_REDIS_PORT;
+
+        @PluginBuilderAttribute("immediateFlush")
+        private boolean immediateFlush = true;
+
+        @PluginBuilderAttribute("queueCapacity")
+        private int queueCapacity = DEFAULT_APPENDER_QUEUE_CAPACITY;
+
+        @PluginElement("SslConfiguration")
+        private SslConfiguration sslConfiguration;
+
+        @PluginElement("RedisPoolConfiguration")
+        private LoggingRedisPoolConfiguration poolConfiguration = LoggingRedisPoolConfiguration.defaultConfiguration();
+
+        @SuppressWarnings("resource")
+        @Override
+        public RedisAppender build() {
+            return new RedisAppender(
+                    getName(),
+                    getLayout(),
+                    getFilter(),
+                    isIgnoreExceptions(),
+                    isImmediateFlush(),
+                    getQueueCapacity(),
+                    getRedisManager()
+            );
+        }
+
+        String getKeys() {
+            return keys;
+        }
+
+        String getHost() {
+            return host;
+        }
+
+        int getQueueCapacity() {
+            return queueCapacity;
+        }
+
+        boolean isImmediateFlush() {
+            return immediateFlush;
+        }
+
+        SslConfiguration getSslConfiguration() {
+            return sslConfiguration;
+        }
+
+        LoggingRedisPoolConfiguration getPoolConfiguration() {
+            return poolConfiguration;
+        }
+
+        int getPort() {
+            return port;
+        }
+
+        public B setKeys(final String keys) {
+            this.keys = keys;
+            return asBuilder();
+        }
+
+        public B setHost(final String host) {
+            this.host = host;
+            return asBuilder();
+        }
+
+        public B setPort(final int port) {
+            this.port = port;
+            return asBuilder();
+        }
+
+        public B setQueueCapacity(final int queueCapacity) {
+            this.queueCapacity = queueCapacity;
+            return asBuilder();
+        }
+
+        public B setPoolConfiguration(final LoggingRedisPoolConfiguration poolConfiguration) {
+            this.poolConfiguration = poolConfiguration;
+            return asBuilder();
+        }
+
+        public B setSslConfiguration(final SslConfiguration ssl) {
+            this.sslConfiguration = ssl;
+            return asBuilder();
+        }
+
+        public B setImmediateFlush(final boolean immediateFlush) {
+            this.immediateFlush = immediateFlush;
+            return asBuilder();
+        }
+
+        RedisManager getRedisManager() {
+            return new RedisManager(
+                    getConfiguration().getLoggerContext(),
+                    getName(),
+                    getKeys().split(KEY_SEPARATOR),
+                    getHost(),
+                    getPort(),
+                    getSslConfiguration(),
+                    getPoolConfiguration()
+            );
+        }
+    }
+
+    /**
+     * Creates a builder for a RedisAppender.
+     * @return a builder for a RedisAppender.
+     */
+    @PluginBuilderFactory
+    public static <B extends Builder<B>> B newBuilder() {
+        return new Builder<B>().asBuilder();
+    }
+
+    @Override
+    public void append(final LogEvent event) {
+        final Layout<? extends Serializable> layout = getLayout();
+        if (event.getLoggerName() != null && AbstractLogger.getRecursionDepth() > 1) {
+            LOGGER.warn("Recursive logging from [{}] for appender [{}].", event.getLoggerName(), getName());
+        } else if (layout instanceof StringLayout) {
+            String serializedEvent = ((StringLayout)layout).toSerializable(event);
+            while (!logQueue.offer(serializedEvent)) {
+                tryFlushQueue();
+            }
+            if (shouldFlushLogQueue(event.isEndOfBatch())) {
+                tryFlushQueue();
+            }
+        } else {
+            throw new AppenderLoggingException("The Redis appender only supports StringLayouts.");
+        }
+    }
+
+    private boolean shouldFlushLogQueue(boolean endOfBatch) {
+        return immediateFlush || endOfBatch;
+    }
+
+    private void tryFlushQueue() {
+        List<String> logEvents = new ArrayList<>();
+        logQueue.drainTo(logEvents);
+        manager.sendBulk(logEvents);
+    }
+
+    @Override
+    public void start() {
+        setStarting();
+        manager.startup();
+        setStarted();
+    }
+
+    @Override
+    public boolean stop(final long timeout, final TimeUnit timeUnit) {
+        setStopping();
+        if (logQueue.size() > 0) {
+            tryFlushQueue();
+        }
+        boolean stopped = super.stop(timeout, timeUnit, false);
+        stopped &= manager.stop(timeout, timeUnit);
+        setStopped();
+        return stopped;
+    }
+
+    @Override
+    public String toString() {
+        return "RedisAppender{" +
+            "name=" + getName() +
+            ", host=" + manager.getHost() +
+            ", port=" + manager.getPort() +
+            ", keys=" + manager.getKeysAsString() +
+            ", immediateFlush=" + this.immediateFlush +
+            '}';
+    }
+}
diff --git a/log4j-redis/src/main/java/org/apache/logging/log4j/redis/appender/RedisManager.java b/log4j-redis/src/main/java/org/apache/logging/log4j/redis/appender/RedisManager.java
new file mode 100644
index 0000000..84eaadc
--- /dev/null
+++ b/log4j-redis/src/main/java/org/apache/logging/log4j/redis/appender/RedisManager.java
@@ -0,0 +1,123 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache license, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the license for the specific language governing permissions and
+ * limitations under the license.
+ */
+
+package org.apache.logging.log4j.redis.appender;
+
+import org.apache.logging.log4j.core.LoggerContext;
+import org.apache.logging.log4j.core.appender.AbstractManager;
+import org.apache.logging.log4j.core.net.ssl.SslConfiguration;
+import redis.clients.jedis.Jedis;
+import redis.clients.jedis.JedisPool;
+import redis.clients.jedis.JedisPoolConfig;
+import redis.clients.jedis.exceptions.JedisConnectionException;
+
+import java.net.URI;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * AutoCloseable wrapper class around a Redis client connection.
+ * Enables the transport of data to Redis lists via RPUSH to preconfigured keys.
+ */
+class RedisManager extends AbstractManager {
+
+    private final String[] keys;
+    private final String host;
+    private final int port;
+    private final SslConfiguration sslConfiguration;
+    private final JedisPoolConfig poolConfiguration;
+    private JedisPool jedisPool;
+
+    RedisManager(LoggerContext loggerContext, String name, String[] keys, String host, int port,
+                 SslConfiguration sslConfiguration, LoggingRedisPoolConfiguration poolConfiguration) {
+        super(loggerContext, name);
+        this.keys = keys;
+        this.host = host;
+        this.port = port;
+        this.sslConfiguration = sslConfiguration;
+        if (poolConfiguration == null) {
+            this.poolConfiguration = LoggingRedisPoolConfiguration.defaultConfiguration();
+        } else {
+            this.poolConfiguration = poolConfiguration;
+        }
+    }
+
+    JedisPool createPool(String host, int port, SslConfiguration sslConfiguration) {
+        if (sslConfiguration != null) {
+            return new JedisPool(
+                    poolConfiguration,
+                    URI.create(host + ":" + port),
+                    sslConfiguration.getSslSocketFactory(),
+                    sslConfiguration.getSslContext().getSupportedSSLParameters(),
+                    null
+            );
+        } else {
+            return new JedisPool(poolConfiguration, host, port, false);
+        }
+
+    }
+
+    public void startup() {
+        jedisPool = createPool(host, port, sslConfiguration);
+    }
+
+    public void sendBulk(List<String> logEvents) {
+        try (Jedis jedis = jedisPool.getResource()) {
+            if (!logEvents.isEmpty()) {
+                send(jedis, logEvents.toArray(new String[0]));
+            }
+        } catch (JedisConnectionException e) {
+            LOGGER.error("Unable to connect to redis. Please ensure that it's running on {}:{}", host, port, e);
+        }
+    }
+
+    public void send(String value) {
+        try (Jedis jedis = jedisPool.getResource()) {
+            try {
+                send(jedis, value);
+            } catch (JedisConnectionException e) {
+                LOGGER.error("Unable to connect to redis. Please ensure that it's running on {}:{}", host, port, e);
+            }
+        }
+    }
+
+    private void send(Jedis jedis, String... value) {
+        for (String key: keys) {
+            jedis.rpush(key, value);
+        }
+    }
+
+    @Override
+    protected boolean releaseSub(final long timeout, final TimeUnit timeUnit) {
+        if (jedisPool != null) {
+            jedisPool.destroy();
+        }
+        return true;
+    }
+
+    String getHost() {
+        return host;
+    }
+
+    int getPort() {
+        return port;
+    }
+
+    String getKeysAsString() {
+        return String.join(",", keys);
+    }
+}
diff --git a/log4j-redis/src/site/manual/index.md b/log4j-redis/src/site/manual/index.md
new file mode 100644
index 0000000..e371232
--- /dev/null
+++ b/log4j-redis/src/site/manual/index.md
@@ -0,0 +1,29 @@
+<!-- vim: set syn=markdown : -->
+<!--
+    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.
+-->
+
+# Apache Log4j Redis module
+
+## Requirements
+
+Some features may require optional
+[dependencies](../runtime-dependencies.html). These dependencies are specified in the
+documentation for those features.
+
+Some Log4j features require external dependencies.
+See the [Dependency Tree](dependencies.html#Dependency_Tree)
+for the exact list of JAR files needed for these features.
diff --git a/log4j-redis/src/site/site.xml b/log4j-redis/src/site/site.xml
new file mode 100644
index 0000000..6d4cddc
--- /dev/null
+++ b/log4j-redis/src/site/site.xml
@@ -0,0 +1,52 @@
+<!--
+ 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.
+
+-->
+<project name="Log4j Core"
+         xmlns="http://maven.apache.org/DECORATION/1.4.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/DECORATION/1.4.0 http://maven.apache.org/xsd/decoration-1.4.0.xsd">
+  <body>
+    <links>
+      <item name="Apache" href="http://www.apache.org/" />
+      <item name="Logging Services" href="http://logging.apache.org/"/>
+      <item name="Log4j" href="../index.html"/>
+    </links>
+
+    <!-- Component-specific reports -->
+    <menu ref="reports"/>
+
+	<!-- Overall Project Info -->
+    <menu name="Log4j Project Information" img="icon-info-sign">
+      <item name="Dependencies" href="../dependencies.html" />
+      <item name="Dependency Convergence" href="../dependency-convergence.html" />
+      <item name="Dependency Management" href="../dependency-management.html" />
+      <item name="Project Team" href="../team-list.html" />
+      <item name="Mailing Lists" href="../mail-lists.html" />
+      <item name="Issue Tracking" href="../issue-tracking.html" />
+      <item name="Project License" href="../license.html" />
+      <item name="Source Repository" href="../source-repository.html" />
+      <item name="Project Summary" href="../project-summary.html" />
+    </menu>
+
+    <menu name="Log4j Project Reports" img="icon-cog">
+      <item name="Changes Report" href="../changes-report.html" />
+      <item name="JIRA Report" href="../jira-report.html" />
+      <item name="Surefire Report" href="../surefire-report.html" />
+      <item name="RAT Report" href="../rat-report.html" />
+    </menu>
+  </body>
+</project>
diff --git a/log4j-redis/src/test/java/org/apache/logging/log4j/redis/appender/RedisAppenderTest.java b/log4j-redis/src/test/java/org/apache/logging/log4j/redis/appender/RedisAppenderTest.java
new file mode 100644
index 0000000..530a192
--- /dev/null
+++ b/log4j-redis/src/test/java/org/apache/logging/log4j/redis/appender/RedisAppenderTest.java
@@ -0,0 +1,192 @@
+/*
+ * 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.redis.appender;
+
+import org.apache.logging.log4j.Level;
+import org.apache.logging.log4j.core.LifeCycle;
+import org.apache.logging.log4j.core.impl.Log4jLogEvent;
+import org.apache.logging.log4j.core.layout.PatternLayout;
+import org.apache.logging.log4j.message.SimpleMessage;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mockito;
+import redis.clients.jedis.JedisPool;
+
+import java.util.concurrent.TimeUnit;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyList;
+import static org.mockito.Mockito.when;
+
+/**
+ * Tests {@link RedisAppender}.
+ */
+public final class RedisAppenderTest {
+
+    private RedisAppender appender;
+    private Log4jLogEvent logEvent;
+    private RedisManager manager;
+
+    private String DESTINATION_KEY = "destination";
+    private String HOST = "localhost";
+    private int PORT = 6379;
+    private String MESSAGE = "Important Message";
+
+    @Before
+    public void setUp() {
+        initMocks();
+
+        appender = new AppenderTestRedisAppenderBuilder()
+                        .withName("RedisAppender")
+                        .setKeys(DESTINATION_KEY)
+                        .setHost(HOST)
+                        .setPort(PORT)
+                        .setImmediateFlush(true)
+                        .withLayout(PatternLayout.createDefaultLayout())
+                        .build();
+        logEvent = createLogEvent();
+    }
+
+    private void initMocks() {
+        manager = Mockito.mock(RedisManager.class);
+        when(manager.createPool(HOST, PORT, null)).thenReturn(Mockito.mock(JedisPool.class));
+    }
+
+    @Test
+    public void testAppenderStartsProperly() {
+        appender.start();
+        Mockito.verify(manager, Mockito.times(1)).startup();
+        assertEquals(appender.getState(), LifeCycle.State.STARTED);
+    }
+
+    @Test
+    public void testAppenderStopsProperly() {
+        appender.stop(500, TimeUnit.HOURS);
+        Mockito.verify(manager, Mockito.times(1)).stop(500, TimeUnit.HOURS);
+        assertEquals(appender.getState(), LifeCycle.State.STOPPED);
+    }
+
+    @Test
+    public void testAppendLogEvent() {
+        appender.append(logEvent);
+        Mockito.verify(manager, Mockito.times(1)).sendBulk(any());
+    }
+
+    @Test
+    public void testQueuesLogEvents() {
+        appender = new AppenderTestRedisAppenderBuilder()
+                .withName("RedisAppender")
+                .setKeys(DESTINATION_KEY)
+                .setHost(HOST)
+                .setPort(PORT)
+                .setQueueCapacity(2)
+                .setImmediateFlush(false)
+                .withLayout(PatternLayout.newBuilder().withPattern("%m").build())
+                .build();
+
+        appender.append(logEvent);
+        Mockito.verify(manager, Mockito.times(0)).sendBulk(any());
+    }
+
+    @Test
+    public void testAttemptsSendWhenQueueReachesCapacity() {
+        appender = new AppenderTestRedisAppenderBuilder()
+                .withName("RedisAppender")
+                .setKeys(DESTINATION_KEY)
+                .setHost(HOST)
+                .setPort(PORT)
+                .setQueueCapacity(1)
+                .setImmediateFlush(false)
+                .withLayout(PatternLayout.newBuilder().withPattern("%m").build())
+                .build();
+
+        appender.append(logEvent);
+        appender.append(logEvent);
+        appender.append(logEvent);
+        Mockito.verify(manager, Mockito.times(2)).sendBulk(anyList());
+    }
+
+    @Test
+    public void testFlushesQueueAtExceededCapacity() {
+        appender = new AppenderTestRedisAppenderBuilder()
+                .withName("RedisAppender")
+                .setKeys(DESTINATION_KEY)
+                .setHost(HOST)
+                .setPort(PORT)
+                .setQueueCapacity(1)
+                .setImmediateFlush(false)
+                .withLayout(PatternLayout.newBuilder().withPattern("%m").build())
+                .build();
+
+        appender.append(logEvent);
+        appender.append(logEvent);
+        Mockito.verify(manager, Mockito.times(1)).sendBulk(any());
+    }
+
+    @Test
+    public void testFlushesQueueAtEndOfBatch() {
+        appender = new AppenderTestRedisAppenderBuilder()
+                .withName("RedisAppender")
+                .setKeys(DESTINATION_KEY)
+                .setHost(HOST)
+                .setPort(PORT)
+                .setImmediateFlush(false)
+                .withLayout(PatternLayout.newBuilder().withPattern("%m").build())
+                .build();
+        logEvent = createPartialLogEvent().setEndOfBatch(true).build();
+
+        appender.append(logEvent);
+        Mockito.verify(manager, Mockito.times(1)).sendBulk(any());
+    }
+
+    @Test
+    public void testFlushesQueueOnAppenderStop() {
+        appender = new AppenderTestRedisAppenderBuilder()
+                .withName("RedisAppender")
+                .setKeys(DESTINATION_KEY)
+                .setHost(HOST)
+                .setPort(PORT)
+                .setImmediateFlush(false)
+                .withLayout(PatternLayout.newBuilder().withPattern("%m").build())
+                .build();
+        appender.append(logEvent);
+        Mockito.verify(manager, Mockito.times(0)).sendBulk(any());
+        appender.stop(100, TimeUnit.DAYS);
+        Mockito.verify(manager, Mockito.times(1)).sendBulk(any());
+    }
+
+    private Log4jLogEvent createLogEvent() {
+        return createPartialLogEvent().build();
+    }
+
+    private Log4jLogEvent.Builder createPartialLogEvent() {
+        return Log4jLogEvent.newBuilder()
+                .setLoggerName(RedisAppenderTest.class.getName())
+                .setLoggerFqcn(RedisAppenderTest.class.getName())
+                .setLevel(Level.INFO)
+                .setMessage(new SimpleMessage(MESSAGE));
+    }
+
+    private class AppenderTestRedisAppenderBuilder extends RedisAppender.Builder<AppenderTestRedisAppenderBuilder> {
+        @Override
+        RedisManager getRedisManager() {
+            return manager;
+        }
+    }
+}
diff --git a/log4j-redis/src/test/java/org/apache/logging/log4j/redis/appender/RedisManagerTest.java b/log4j-redis/src/test/java/org/apache/logging/log4j/redis/appender/RedisManagerTest.java
new file mode 100644
index 0000000..0a68f9c
--- /dev/null
+++ b/log4j-redis/src/test/java/org/apache/logging/log4j/redis/appender/RedisManagerTest.java
@@ -0,0 +1,111 @@
+/*
+ * 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.redis.appender;
+
+import org.apache.logging.log4j.core.LoggerContext;
+import org.apache.logging.log4j.core.net.ssl.SslConfiguration;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mockito;
+import redis.clients.jedis.Jedis;
+import redis.clients.jedis.JedisPool;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.when;
+
+public class RedisManagerTest {
+
+    private JedisPool mockJedisPool;
+    private Jedis mockJedis;
+    private RedisManager manager;
+
+    private String KEYS = "abc,def";
+
+    @Before
+    public void setUp() {
+        initMocks();
+        manager = new ManagerTestRedisAppenderBuilder()
+                .setHost("localhost")
+                .setPort(6379)
+                .setKeys(KEYS)
+                .getRedisManager();
+        manager.startup();
+    }
+
+    private void initMocks() {
+        mockJedisPool = Mockito.mock(JedisPool.class);
+        mockJedis = Mockito.mock(Jedis.class);
+        when(mockJedisPool.getResource()).thenReturn(mockJedis);
+        when(mockJedis.rpush(anyString(), anyString())).thenReturn(1L);
+    }
+
+    @Test
+    public void testSendsValuesToAllKeys() {
+        manager.send("value");
+        Mockito.verify(mockJedisPool).getResource();
+        Mockito.verify(mockJedis, Mockito.times(KEYS.split(",").length)).rpush(anyString(), anyString());
+        for (String k: KEYS.split(",")) {
+            Mockito.verify(mockJedis, Mockito.times(1)).rpush(eq(k), anyString());
+        }
+    }
+
+    @Test
+    public void testReleasePoolResources() {
+        manager.stop(100, TimeUnit.HOURS);
+        Mockito.verify(mockJedisPool).destroy();
+    }
+
+    @Test
+    public void testSendsAllValuesInBulk() {
+        List<String> logs = new ArrayList<>();
+        logs.add("value1");
+        logs.add("value2");
+        manager.sendBulk(logs);
+        Mockito.verify(mockJedis, Mockito.times(KEYS.split(",").length)).rpush(anyString(), eq("value1"), eq("value2"));
+    }
+
+    private class TestRedisManager extends RedisManager {
+
+        TestRedisManager(LoggerContext loggerContext, String name, String keys, String host, int port) {
+            super(loggerContext, name, keys.split(","), host, port, null, null);
+        }
+
+        @Override
+        JedisPool createPool(String host, int port, SslConfiguration ssl) {
+            return mockJedisPool;
+        }
+    }
+
+    private class ManagerTestRedisAppenderBuilder extends RedisAppender.Builder<ManagerTestRedisAppenderBuilder> {
+        @Override
+        RedisManager getRedisManager() {
+            return new TestRedisManager(
+                    LoggerContext.getContext(),
+                    getName(),
+                    getKeys(),
+                    getHost(),
+                    getPort()
+            );
+        }
+    }
+}
diff --git a/pom.xml b/pom.xml
index 04239367..55b372e 100644
--- a/pom.xml
+++ b/pom.xml
@@ -1557,6 +1557,7 @@
     <module>log4j-jeromq</module>
     <module>log4j-jms</module>
     <module>log4j-kafka</module>
+    <module>log4j-redis</module>
     <module>log4j-couchdb</module>
     <module>log4j-mongodb3</module>
     <module>log4j-mongodb4</module>
diff --git a/src/site/asciidoc/manual/appenders.adoc b/src/site/asciidoc/manual/appenders.adoc
index 9476517..5fc0bf3 100644
--- a/src/site/asciidoc/manual/appenders.adoc
+++ b/src/site/asciidoc/manual/appenders.adoc
@@ -2268,6 +2268,97 @@
 </Configuration>
 ----
 
+
+[#RedisAppender]
+== RedisAppender
+
+Log4j2 supports a RedisAppender as part of the `log4j2-redis` module. The RedisAppender logs events to a https://redis.io/[Redis] queue. Each log event is appended via the standard
+https://redis.io/commands/rpush[RPUSH] command.
+
+.RedisAppender Parameters
+[cols=",,",options="header",]
+|=======================================================================
+|Parameter Name |Type |Description
+
+|name |String |The name of the Appender. Required.
+
+|layout |Layout |The Layout to use to format the LogEvent. Required,
+there is no default. _New since 2.9, in previous versions <PatternLayout
+pattern="%m"/> was default._
+
+|filter |Filter |A Filter to determine if the event should be handled by
+this Appender. More than one Filter may be used by using a
+CompositeFilter.
+
+|host |String |The hostname on which Redis is running (required).
+
+|port |int |The port on which Redis is running. Default: 6379.
+
+|keys |String |The keys of the queue to which each log message should be sent, separated by commas (e.g. "key-one,key-two,key-three"). Default: "logEvents".
+
+|ignoreExceptions |boolean |The default is `true`, causing exceptions
+encountered while appending events to be internally logged and then
+ignored. When set to `false` exceptions will be propagated to the
+caller, instead. You must set this to `false` when wrapping this
+Appender in a link:#FailoverAppender[FailoverAppender].
+
+|immediateFlush | boolean | Whether to immediately send logs to redis rather than sending at the end of a batch. Default: true.
+
+|queueCapacity | int | The maximum number of logs to hold in memory before sending to Redis. Default: 20.
+
+|SslConfiguration |SslConfiguration |Contains the configuration for the KeyStore and
+TrustStore. See link:#SSL[SSL].
+
+|RedisPoolConfiguration |LoggingRedisPoolConfiguration |Granular configuration of the resource pool for the Redis
+client connection. Available parameters include maxIdle, minIdle, testOnBorrow, testOnReturn, testWhileIdle, numTestsPerEvictionRun, and timeBetweenEvictionRunsMillis.
+|=======================================================================
+
+Here is a sample RedisAppender configuration snippet in XML:
+
+[source,prettyprint,linenums]
+----
+<?xml version="1.0" encoding="UTF-8"?>
+  ...
+  <Appenders>
+    <Redis name"RedisAppend" host="localhost" port=6379 keys="key-one,key-two" immediateFlush=false>
+      <PatternLayout>
+        <Pattern>%m</Pattern>
+      </PatternLayout>
+    </Redis>
+    <Async name="Async">
+      <AppenderRef ref="RedisAppend"/>
+    </Async>
+  </Appenders>
+----
+You can also configure the appender easily in YML:
+[source,prettyprint,linenums]
+----
+Configuration:
+  Appenders:
+    Redis:
+      name: RedisAppend
+      host: localhost
+      port: 6379
+      Keys: key-one,key-two
+      PatternLayout:
+        pattern: "%m"
+----
+
+
+This appender operates synchronously by default and will block until the record
+has been acknowledged by the Redis server. Wrap with
+http://logging.apache.org/log4j/2.x/manual/appenders.html#AsyncAppender[Async
+appender] and/or set syncSend to `false` to log asynchronously (as in the first example above).
+
+=== Redis vs. Kafka
+
+In a logging context, Redis may play a similar role to Kafka, serving as a message broker
+in logging pipelines. In general, Redis may be preferred to Kafka in situations where
+an in-memory queue is desired. A fairly simple breakdown of the diffferent approaches can be found
+https://stackoverflow.com/questions/37990784/difference-between-redis-and-kafka[on StackOverflow here].
+
+If you prefer to use Kafka, please refer to the supported <<KafkaAppender>>.
+
 [#RewriteAppender]
 == RewriteAppender
 
diff --git a/src/site/asciidoc/runtime-dependencies.adoc b/src/site/asciidoc/runtime-dependencies.adoc
index c0407f7..e13b82c 100644
--- a/src/site/asciidoc/runtime-dependencies.adoc
+++ b/src/site/asciidoc/runtime-dependencies.adoc
@@ -172,7 +172,7 @@
 |http://lmax-exchange.github.io/disruptor/[LMAX Disruptor]
 
 |Kafka Appender
-a|http://kafka.apache.org/[Kafka client library].
+|http://kafka.apache.org/[Kafka client library].
 [NOTE]
 ====
 You need to use a version of the Kafka client library matching the Kafka server used.
@@ -200,6 +200,9 @@
 |NoSQL Appender with Apache CouchDB provider
 |LightCouch CouchDB client library and Log4j CouchDB library
 
+|Redis Appender
+|https://github.com/xetorthio/jedis[Jedis client]
+
 |Cassandra Appender
 |Datastax Cassandra driver and Log4j Cassandra library
 
diff --git a/src/site/site.xml b/src/site/site.xml
index 49437e8..48a6d3e 100644
--- a/src/site/site.xml
+++ b/src/site/site.xml
@@ -181,6 +181,7 @@
         <item name="NoSQL for CouchDB" href="/manual/appenders.html#NoSQLAppenderCouchDB"/>
         <item name="Output Stream" href="/manual/appenders.html#OutputStreamAppender"/>
         <item name="Random Access File" href="/manual/appenders.html#RandomAccessFileAppender"/>
+        <item name="Redis" href="/manual/appenders.html#RedisAppender"/>
         <item name="Rewrite" href="/manual/appenders.html#RewriteAppender"/>
         <item name="Rolling File" href="/manual/appenders.html#RollingFileAppender"/>
         <item name="Rolling Random Access File" href="/manual/appenders.html#RollingRandomAccessFileAppender"/>